我们知道目前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
这样服务器就搭建好了。