如何在Ubuntu QML应用中使用Push Notification

我们知道目前Ubuntu手机平台有些类似iPhone平台,是一个单任务的操作系统,虽然系统本身具有多任务的功能。如果当前的应用被推到后台的话,应用将会被自动挂起,而不会被系统所运行。在这个时候如果我们的应用需要等待一个消息,比如就想微信之类的信息,我们就要使用Ubuntu平台所提供的Push Notification机制来实现我们的类似多任务的东西。当通知被收到后,我们就可以直接点击接受到的通知,应用又会被重新运行到前台。


关于Push notification,在我们的开发者网站上,有一篇文章(client)和一篇文章(server)详细介绍了它的机制。这里我不想讲太多的东西。有兴趣的同学们可以详读那篇文章。今天在这里,我来和大家分析一个具体的实例,以更好地了解如何在Ubuntu手机上实现这个功能。


技术分享


在上述的图中可以看出来,整个系统的组成分两部分:客户端及服务器端。在服务器端,又分为一个PushSever (https://push.ubuntu.com)及一个App Server。App server是用来管理我们的用户的Nick Name及Token的。在它的里面,有一个数据库。


为了测试,开发者必须有一个Ubuntu One的账号。我们需要在手机的“系统设置”里的账号中创建这个账号。


当一个QML应用在使用:


import Ubuntu.PushNotifications 0.1

PushClient {
    id: pushClient
    Component.onCompleted: {
        notificationsChanged.connect(messageList.handle_notifications)
        error.connect(messageList.handle_error)
    }
    appId: "com.ubuntu.developer.push.hello_hello"
}


当我们使用上面的API后,push server将向我们的客户端发送一个token。这个token依赖于手机自己的参数及上面所看到的“appId”。利用这个token,我们可以向我们的应用服务器注册,并存于应用服务器端中。当我们需要发送信息的时候,我们必须注册一个类似nickname的东西。这个nickname将和我们手机客户端的token绑定。每当另外一个nickname想像我们发送信息时,应用服务器端可以通过数据库的查询来得到我们的token,从而更进一步通过push server来向我们的客服端推送信息。如果我们的客户端想向其它的客户端发送信息,这其中的道理,也是和刚才一样。


目前,在我们的开发者网站并没有PushClient的具体的介绍。我们可以使用在文章“ 如何得到QML package的详细API接口”中的方法来了解这个API。

Push server是用来推送信息。它位于 https://push.ubuntu.com。它只有一个endpoint:/notify。为了向一个用户发送推送信息。应用服务器可以向Push Sever发送一个含有“Content-type: application/json”的HTTP POST信息来推送我们的信息。下面是一个POST body的一个样板内容:


{
        "appid": "com.ubuntu.music_music",
        "expire_on": "2014-10-08T14:48:00.000Z",
        "token": "LeA4tRQG9hhEkuhngdouoA==",
        "clear_pending": true,
        "replace_tag": "tagname",
        "data": {
                "message": "foobar",
                "notification": {
                        "card": {
                                "summary": "yes",
                                "body": "hello",
                                "popup": true,
                                "persist": true
                        }
                        "sound": "buzz.mp3",
                        "tag": "foo",
                        "vibrate": {
                                "duration": 200,
                                "pattern": (200, 100),
                                "repeat": 2
                        }
                        "emblem-counter": {
                                "count": 12,
                                "visible": true
                        }
                }
        }
}



appid: ID of the application that will receive the notification, as described in the client side documentation.
expire_on: Expiration date/time for this message, in ISO8601 Extendend format
token: The token identifying the user+device to which the message is directed, as described in the client side documentation.
clear_pending: Discards all previous pending notifications. Usually in response to getting a "too-many-pending" error.
replace_tag: If there‘s a pending notification with the same tag, delete it before queuing this new one.
data: A JSON object.
从上面的信息格式,我们可以看出来,token是非常重要的一个信息。有了它,我们就可以向我们需要的终端发送推送信息。


我们可以利用我们的SDK来创建一个简单的例程。下面简单介绍一下我们的主要的文件main.qml:


import QtQuick 2.0
import Qt.labs.settings 1.0
import Ubuntu.Components 0.1
import Ubuntu.Components.ListItems 0.1 as ListItem
import Ubuntu.PushNotifications 0.1
import "components"

MainView {
    id: "mainView"
    // objectName for functional testing purposes (autopilot-qt5)
    objectName: "mainView"

    // Note! applicationName needs to match the "name" field of the click manifest
    applicationName: "com.ubuntu.developer.ralsina.hello"

    automaticOrientation: true
    useDeprecatedToolbar: false

    width: units.gu(100)
    height: units.gu(75)

    Settings {
        property alias nick: chatClient.nick
        property alias nickText: nickEdit.text
        property alias nickPlaceholder: nickEdit.placeholderText
        property alias nickEnabled: nickEdit.enabled
    }

    states: [
        State {
            name: "no-push-token"
            when: (pushClient.token == "")
            PropertyChanges { target: nickEdit; readOnly: true}
            PropertyChanges { target: nickEdit; focus: true}
            PropertyChanges { target: messageEdit; enabled: false}
            PropertyChanges { target: loginButton; enabled: false}
            PropertyChanges { target: loginButton; text: "Login"}
        },
        State {
            name: "push-token-not-registered"
            when: ((pushClient.token != "") && (chatClient.registered == false))
            PropertyChanges { target: nickEdit; readOnly: false}
            PropertyChanges { target: nickEdit; text: ""}
            PropertyChanges { target: nickEdit; focus: true}
            PropertyChanges { target: messageEdit; enabled: false}
            PropertyChanges { target: loginButton; enabled: true}
            PropertyChanges { target: loginButton; text: "Login"}
        },
        State {
            name: "registered"
            when: ((pushClient.token != "") && (chatClient.registered == true))
            PropertyChanges { target: nickEdit; readOnly: true}
            PropertyChanges { target: nickEdit; text: "Your nick is " + chatClient.nick}
            PropertyChanges { target: messageEdit; focus: true}
            PropertyChanges { target: messageEdit; enabled: true}
            PropertyChanges { target: loginButton; enabled: true}
            PropertyChanges { target: loginButton; text: "Logout"}
        }
    ]

    state: "no-push-token"

    ChatClient {
        id: chatClient
        onError: {messageList.handle_error(msg)}
        token: {
            var i = {
                "from" : "",
                "to" :  "",
                "message" : "Token: " + pushClient.token
            }

            if ( pushClient.token )
                messagesModel.insert(0, i);
            console.log("token is changed!");
            return pushClient.token;
        }
    }

    PushClient {
        id: pushClient
        Component.onCompleted: {
            notificationsChanged.connect(messageList.handle_notifications)
            error.connect(messageList.handle_error)
            onTokenChanged: {
                console.log("token: +" + pushClient.token );
                console.log("foooooo")
            }
        }
        appId: "com.ubuntu.developer.ralsina.hello_hello"

    }

    TextField {
        id: nickEdit
        placeholderText: "Your nickname"
        inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText | Qt.ImhPreferLowercase
        anchors.left: parent.left
        anchors.right: loginButton.left
        anchors.top: parent.top
        anchors.leftMargin: units.gu(.5)
        anchors.rightMargin: units.gu(1)
        anchors.topMargin: units.gu(.5)
        onAccepted: { loginButton.clicked() }
    }

    Button {
        id: loginButton
        anchors.top: nickEdit.top
        anchors.right: parent.right
        anchors.rightMargin: units.gu(.5)
        onClicked: {
            if (chatClient.nick) { // logout
                chatClient.nick = ""
            } else { // login
                chatClient.nick = nickEdit.text
            }
        }
    }

    TextField {
        id: messageEdit
        inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText | Qt.ImhPreferLowercase
        anchors.right: parent.right
        anchors.left: parent.left
        anchors.top: nickEdit.bottom
        anchors.topMargin: units.gu(1)
        anchors.rightMargin: units.gu(.5)
        anchors.leftMargin: units.gu(.5)
        placeholderText: "Your message"
        onAccepted: {
            console.log("sending " + text)
            var idx = text.indexOf(":")
            var nick_to = text.substring(0, idx).trim()
            var msg = text.substring(idx+1, 9999).trim()
            var i = {
                "from" :  chatClient.nick,
                "to" :  nick_to,
                "message" : msg
            }
            var o = {
                enabled: annoyingSwitch.checked,
                persist: persistSwitch.checked,
                popup: popupSwitch.checked,
                sound: soundSwitch.checked,
                vibrate: vibrateSwitch.checked,
                counter: counterSlider.value
            }
            chatClient.sendMessage(i, o)
            i["type"] = "sent"
            messagesModel.insert(0, i)
            text = ""
        }
    }
    ListModel {
        id: messagesModel
        ListElement {
            from: ""
            to: ""
            type: "info"
            message: "Register by typing your nick and clicking Login."
        }
        ListElement {
            from: ""
            to: ""
            type: "info"
            message: "Send messages in the form \"destination: hello\""
        }
        ListElement {
            from: ""
            to: ""
            type: "info"
            message: "Slide from the bottom to control notification behaviour."
        }
    }

    UbuntuShape {
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: notificationSettings.bottom
        anchors.top: messageEdit.bottom
        anchors.topMargin: units.gu(1)
        ListView {
            id: messageList
            model: messagesModel
            anchors.fill: parent
            delegate: Rectangle {
                MouseArea {
                    anchors.fill: parent
                    onClicked: {
                        if (from != "") {
                            messageEdit.text = from + ": "
                            messageEdit.focus = true
                        }
                    }
                }
                height: label.height + units.gu(2)
                width: parent.width
                Rectangle {
                    color: {
                        "info": "#B5EBB9",
                        "received" : "#A2CFA5",
                        "sent" : "#FFF9C8",
                        "error" : "#FF4867"}[type]
                    height: label.height + units.gu(1)
                    anchors.fill: parent
                    radius: 5
                    anchors.margins: units.gu(.5)
                    Text {
                        id: label
                        text: "<b>" + ((type=="sent")?to:from) + ":</b> " + message
                        wrapMode: Text.Wrap
                        width: parent.width - units.gu(1)
                        x: units.gu(.5)
                        y: units.gu(.5)
                        horizontalAlignment: (type=="sent")?Text.AlignRight:Text.AlignLeft
                    }
                }
            }

            function handle_error(error) {
                messagesModel.insert(0, {
                     "from" :  "",
                     "to" :  "",
                     "type" :  "error",
                     "message" : "<b>ERROR: " + error + "</b>"
                })
            }

            function handle_notifications(list) {
                list.forEach(function(notification) {
                    var item = JSON.parse(notification)
                    item["type"] = "received"
                    messagesModel.insert(0, item)
                })
            }
        }
    }

    Panel {
        id: notificationSettings
        anchors {
            left: parent.left
            right: parent.right
            bottom: parent.bottom
        }
        height: item1.height * 9
        UbuntuShape {
            anchors.fill: parent
            color: Theme.palette.normal.overlay
            Column {
                id: settingsColumn
                anchors.fill: parent
                ListItem.Header {
                    text: "<b>Notification Settings</b>"
                }
                ListItem.Standard {
                    id: item1
                    text: "Enable Notifications"
                    control: Switch {
                        id: annoyingSwitch
                        checked: true
                    }
                }
                ListItem.Standard {
                    text: "Enable Popup"
                    enabled: annoyingSwitch.checked
                    control: Switch {
                        id: popupSwitch
                        checked: true
                    }
                }
                ListItem.Standard {
                    text: "Persistent"
                    enabled: annoyingSwitch.checked
                    control: Switch {
                        id: persistSwitch
                        checked: true
                    }
                }
                ListItem.Standard {
                    text: "Make Sound"
                    enabled: annoyingSwitch.checked
                    control: Switch {
                        id: soundSwitch
                        checked: true
                    }
                }
                ListItem.Standard {
                    text: "Vibrate"
                    enabled: annoyingSwitch.checked
                    control: Switch {
                        id: vibrateSwitch
                        checked: true
                    }
                }
                ListItem.Standard {
                    text: "Counter Value"
                    enabled: annoyingSwitch.checked
                    control: Slider {
                        id: counterSlider
                        value: 42
                    }
                }
                Button {
                    text: "Set Counter Via Plugin"
                    onClicked: { pushClient.count = counterSlider.value; }
                }
                Button {
                    text: "Clear Persistent Notifications"
                    onClicked: { pushClient.clearPersistent([]); }
                }
            }
        }
    }
}

这里,在上面创建一个nickname的输入框及一个login按钮。紧接着,我们创建一个输入信息的对话框。再紧接着,我们创建了一个listview来显示状态,提示信息,或来往的信息。


技术分享


ChatClient.qml文件的定义如下:


import QtQuick 2.0
import Ubuntu.Components 0.1

Item {
    property string nick
    property string token
    property bool registered: false
    signal error (string msg)
    onNickChanged: {
        if (nick) {
            console.log("Nick is changed!");
            register()
        } else {
            registered = false
        }
    }
    onTokenChanged: {
        console.log("Token is changed!");
        register()
    }

    function register() {
        console.log("registering ", nick, token);
        if (nick && token) {
            console.log("going to make a request!");

            var req = new XMLHttpRequest();
            req.open("post", "http://direct.ralsina.me:8001/register", true);
//            req.open("post", "http://127.0.0.1:8001/register", true);
            req.setRequestHeader("Content-type", "application/json");
            req.onreadystatechange = function() { // Call a function when the state changes.
                if(req.readyState == 4) {
                    if (req.status == 200) {
                        console.log("response: " + JSON.stringify(req.responseText));
                        registered = true;
                    } else {
                        error(JSON.parse(req.responseText)["error"]);
                    }
                }
            }

            console.log("content: " + JSON.stringify(JSON.stringify({"nick" : nick.toLowerCase(),
                                                                     "token": token
                                                                    })));
            req.send(JSON.stringify({
                "nick" : nick.toLowerCase(),
                "token": token
            }))
        }
    }

    /* options is of the form:
      {
          enabled: false,
          persist: false,
          popup: false,
          sound: "buzz.mp3",
          vibrate: false,
          counter: 5
      }
    */
    function sendMessage(message, options) {
        var to_nick = message["to"]
        var data = {
            "from_nick": nick.toLowerCase(),
            "from_token": token,
            "nick": to_nick.toLowerCase(),
            "data": {
                "message": message,
                "notification": {}
            }
        }
        if (options["enabled"]) {
            data["data"]["notification"] = {
                "card": {
                    "summary": nick + " says:",
                    "body": message["message"],
                    "popup": options["popup"],
                    "persist": options["persist"],
                    "actions": ["appid://com.ubuntu.developer.ralsina.hello/hello/current-user-version"]
                }
            }
            if (options["sound"]) {
                data["data"]["notification"]["sound"] = options["sound"]
            }
            if (options["vibrate"]) {
                data["data"]["notification"]["vibrate"] = {
                    "duration": 200
                }
            }
            if (options["counter"]) {
                data["data"]["notification"]["emblem-counter"] = {
                    "count": Math.floor(options["counter"]),
                    "visible": true
                }
            }
        }
        var req = new XMLHttpRequest();
        req.open("post", "http://direct.ralsina.me:8001/message", true);
        req.setRequestHeader("Content-type", "application/json");
        req.onreadystatechange = function() {//Call a function when the state changes.
            if(req.readyState == 4) {
                if (req.status == 200) {
                    registered = true;
                } else {
                    error(JSON.parse(req.responseText)["error"]);
                }
            }
        }
        req.send(JSON.stringify(data))
    }
}

这个是用来向应用服务器发送注册信息及发送信息的。这里我们使用了一个已经建立好的应用服务器在http://direct.ralsina.me:8001。


这里,我们必须在手机或者我们的模拟器中创建一个Ubuntu One的账号,否则应用将不会运行成功。


我们同时运行我们的手机和模拟器,我们可以看到如下的画面:

技术分享   技术分享


技术分享  技术分享


整个“hello”的源码在:git clone https://gitcafe.com/ubuntu/example-client.git


整个server的源码在地址:git clone https://gitcafe.com/ubuntu/example-server.git


为了能够运行应用服务器,我们必须在服务器上安装相应的component,并选好自己的口地址(比如8001),这个在服务器代码中的config.js中可以找到:


module.exports = config = {
    "name" : "pushAppServer"
    ,"app_id" : "appEx"
    ,"listen_port" : 8000
    ,"mongo_host" : "localhost"
    ,"mongo_port" : 27017
    ,"mongo_opts" : {}
    ,"push_url": "https://push.ubuntu.com"
    ,"retry_batch": 5
    ,"retry_secs" : 30
    ,"happy_retry_secs": 5
    ,"expire_mins": 120
    ,"no_inbox": true
    ,"play_notify_form": true
}

然后运行:

$nodejs server.js

这样服务器就搭建好了。





郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。