我们知道目前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. |
我们可以利用我们的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”的源码在:https://github.com/liu-xiao-guo/example-client
整个server的源码在地址:https://github.com/liu-xiao-guo/example-server
为了能够运行应用服务器,我们必须在服务器上安装相应的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
这样服务器就搭建好了。