这次作业主要是针对服务端进行完善,让app可以接受推送,接收到推送以后点击通知能够跳转到正确的activity。
首先要知道一个叫Firebase Cloud Messaging (FCM)的工具,我们是利用这个工具向装有app的每个手机广播推送的。
我们要做的主要工作有三:
Step1
一个谷歌账号,创立一个项目,设置好Firebase Cloud Messaging API。
这一步主要是前期的准备工作,具体操作流程见<a href="http://iems5722.albertauyeung.com/files/assignments/iems5722-assignment-04.pdf">传送门</a>
ps: 可以在AS里的tools中找到 firebase,通过这个方法来链接回节省一些添加依赖的步骤。
Step2
在AS上改写app,让它支持FCM。这部分又分为三个小工作:
2.1
得到一个本机token。
这一步主要是编写 MyFirebaseInstanceIDService.java里的onTokenRefresh()方法。
2.2
上传这个token给服务器,服务器存到数据库。
这部分要分为两步,一个是client端,一个是服务器端。app上要继续编写MyFirebaseInstanceIDService.java,与上一小步不同的是写的是sendRegistrationToServer(String token)方法
直接贴代码吧:
import android.app.Service;
import android.util.Log;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.FirebaseInstanceIdService;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
/**
* Created by huangkai on 2017/3/28.
*/
public class MyFirebaseInstanceIDService extends FirebaseInstanceIdService {
// @Override
private static final String TAG = "MyFirebaseIIDService";
private final String STUDENT_ID = "1155084531";
// This function will be invoked when Android assigns a token to the app
@Override
public void onTokenRefresh() {
String refreshedToken = FirebaseInstanceId.getInstance().getToken();
Log.d(TAG, "Refreshed token: " + refreshedToken);
sendRegistrationToServer(refreshedToken);
}
private void sendRegistrationToServer(String token) { // Submit Token to your server (e.g. using HTTP) // (Implement your own logic ...)
RequestBody reqBody = new FormBody.Builder()
.add("token", token)
.add("user_id", STUDENT_ID)
.build();
String url="http://www.therealhk.top/api/assgn4/submit_push_token";
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.post(reqBody)
.build();
Log.d(TAG, url);
try {
Response res = client.newCall(request).execute();
String resData = res.body().string();
JSONObject json = new JSONObject(resData);
Log.d("push token status:", String.valueOf(json.get("status")));
} catch (IOException e) {
e.printStackTrace();
Log.d(TAG, "sendRegistrationToServer: error");
} catch (JSONException e) {
e.printStackTrace();
Log.d(TAG, "sendRegistrationToServer: error");
}
}
}
服务器端要做什么呢?
服务器端要处理接收到的server,要在server 上新建一个table 用来存放user_id 和token。
CREATE TABLE push_tokens (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` VARCHAR(11) NOT NULL,
`token` VARCHAR(256) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
然后我们在上次的api的基础上增加一个部分:submit_push_token()
@app.route('/api/assgn4/submit_push_token', methods=['POST'])
def submit_push_token():
my_db = md.MyDatabase()
user_id =request.form.get("user_id")
token = request.form.get("token")
if token == None or user_id ==None:
return jsonify(status="ERROR", message="missing parameters")
query = "INSERT INTO push_tokens (user_id,token) values(%s,%s)"
parameters = (user_id,token)
my_db.cursor.execute(query,parameters)
my_db.db.commit()
return jsonify(status="OK")
2.3
app正确接收FCM的message
这部分主要是写MyFirebaseMessagingService.java
代码如下:
package a1_1155084531.iems5722.ie.cuhk.edu.hk.a1_1155084531;
import com.google.firebase.messaging.FirebaseMessagingService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.google.firebase.messaging.RemoteMessage;
/**
* Created by huangkai on 2017/3/28.
*/
public class MyFirebaseMessagingService extends FirebaseMessagingService {
private static final String TAG = "MyFirebaseMsgService";
private static int count =0;
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
// [START_EXCLUDE]
// There are two types of messages data messages and notification messages. Data messages are handled
// here in onMessageReceived whether the app is in the foreground or background. Data messages are the type
// traditionally used with GCM. Notification messages are only received here in onMessageReceived when the app
// is in the foreground. When the app is in the background an automatically generated notification is displayed.
// When the user taps on the notification they are returned to the app. Messages containing both notification
// and data payloads are treated as notification messages. The Firebase console always sends notification
// messages. For more see: https://firebase.google.com/docs/cloud-messaging/concept-options
// [END_EXCLUDE]
// TODO(developer): Handle FCM messages here.
// Not getting messages here? See why this may be: https://goo.gl/39bRNJ
Log.d(TAG, "From: " + remoteMessage.getFrom());
// Check if message contains a notification payload.
if (remoteMessage.getNotification() != null) {
Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
sendNotification(remoteMessage.getNotification().getTitle(),
remoteMessage.getNotification().getTag(),
remoteMessage.getNotification().getBody());
count = count +1;
}
// Also if you intend on generating your own notifications as a result of a received FCM
// message, here is where that should be initiated. See sendNotification method below.
}
// [END receive_message]
/**
* Create and show a simple notification containing the received FCM message.
*
* @param messageBody FCM message body received.
*/
private void sendNotification(String chatroom_name,String chatroom_id,String messageBody) {
//以下是保证点击通知跳转正确到activity的代码
Intent intent = new Intent(this, Chat_Activity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra("chatroom_id",chatroom_id);
intent.putExtra("chatroom_name",chatroom_name);
PendingIntent pendingIntent = PendingIntent.getActivity(this, count /* Request code */, intent,
PendingIntent.FLAG_ONE_SHOT);
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_stat_ic_notification)
.setContentTitle(chatroom_name)
.setContentText(messageBody)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent);
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(count/* ID of notification */, notificationBuilder.build());
}
}
这一部分和server端有关,取决于你server端Title,Tag,Body里取出来的是什么。下一部分会有介绍。
Step3
扩展server的功能使之能够实现某用户发送一个消息了以后其他所有人都收到FCM消息。
这里要首先介绍一个叫做celery的东西,它是一个消息队列的管理器,我们利用它来创建异步的任务,把消息转发给FCM,系统结构如下。
server端与上次不同,还需要写一个task.py,写之前安装RabbitMQ 和 Celery 以及 task.py 中需要的 requests。
$ sudo apt-get install rabbitmq-server
$ sudo pip install celery
$ sudo pip install requests
task.py 代码如下
from celery import Celery
from flask import Flask
import requests
def make_celery(app):
celery = Celery(app.import_name, broker=app.config['CELERY_BROKER_URL'])
celery.conf.update(app.config)
TaskBase = celery.Task
class ContextTask(TaskBase):
abstract = True
def __call__(self, *args, **kwargs):
return TaskBase.__call__(self, *args, **kwargs)
celery.Task = ContextTask
return celery
app = Flask(__name__)
app.config.update(
CELERY_BROKER_URL='amqp://guest@localhost'
)
celery = make_celery(app)
@celery.task()
def NoififyEveryOne(chatroom_name,chatroom_id,name,msg,token):
api_key = 'AAAAmQpTpkU:APA91bGwonrhhB1lPoqIlkAwTnV9dDNF100o5aKo3J13gc3ZesxnkSTrShDNCnE8CZbXTVXOlM1yoqtas_GiSRIej1cX52z8thv6F9o-p6ShDy0dWR-9i-w7t0shWVZOe1cZ1ETEa1nX'
url = 'https://fcm.googleapis.com/fcm/send'
headers = {
'Authorization': 'key=' + api_key,
'Content-Type': 'application/json'
}
device_token = token
payload = {'to': device_token,'notification':{
"title":chatroom_name,
"tag":chatroom_id,
"body":name +": "+msg
}}
response = requests.post(url,headers =headers,json=payload)
if response.status_code == 200:
print "Send to FCM sucessfully!"
可以看到
title里放的是chatroom_name
tag放的是chatroom_id,
body放的是name +": "+msg
出来的通知大概长这样:
我们要实现的功能是某用户发送一个消息了以后其他所有人都收到FCM消息,那肯定要在原本发送信息的api里调用上面写的NoififyEveryOne.delay()方法,故对API的发送部分做如下修改:
@app.route('/api/assgn3/send_message',methods=['POST'])
def send_message():
my_db = md.MyDatabase()
msg = request.form.get("message")
name = request.form.get("name")
chatroom_id = request.form.get("chatroom_id")
user_id = request.form.get("user_id")
# Get chatroom name
select_chatroomname_query = "SELECT name from CHATROOMS where id = %s" % chatroom_id
my_db.cursor.execute(select_chatroomname_query)
chatroom_name_json = my_db.cursor.fetchone()
chatroom_name = chatroom_name_json['name']
if msg == None or chatroom_id == None or name == None or user_id == None :
return jsonify(status="ERROR", message="missing parameters")
query = "INSERT INTO messages (chatroom_id,user_id,name,timestamp,message) values(%s,%s,%s,%s,%s)"
parameters = (chatroom_id,user_id,name,time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()+8*3600)),msg)
my_db.cursor.execute(query,parameters)
my_db.db.commit()
select_token_query = "SELECT token FROM push_tokens"
my_db.cursor.execute(select_token_query)
token_json = my_db.cursor.fetchone()
while token_json != None: #发送给每一台机子
token = token_json['token']
NoififyEveryOne.delay(chatroom_name,chatroom_id,name,msg,token)
token_json = my_db.cursor.fetchone()
return jsonify(status="OK")
在做下一步以前,可以开两个终端并更改app里写的API地址。分别运行
$ python api.py
$ celery -A task.celery worker --loglevel=DEBUG
进行调试,一切OK后进入下一步。
Step4 配置服务器
4.1 Supervisor的配置
在上次的.conf文件(etc目录下的),添加新的program配置信息,command 一行即运行 celery worker的命令
[program:iems5722_2]
command = celery -A task.celery worker
directory = /home/ubuntu/api
user = ubuntu
autostart = true
autorestart = true
stdout_logfile = /home/ubuntu/api/task.log
redirect_stderr = true
然后shutdown并重载supervisor。具体指令如下:
$ supervisord -c /etc/supervisord.conf # 启动supervisor
$ supervisorctl reload # 重新加载配置文件
$ supervisorctl update
4.2 Nginx的配置
配置文件在 /etc/nginx/sites-available/ 下面,配置完了之后软链接一份到 /etc/nginx/sites-enabled/ghost.conf 下面(原本的软链接删除)。
配置文件需要修改:
server {
listen 80;
listen [::]:80;
root /home/ubuntu/api;
index index.php index.html index.htm;
server_name 0.0.0.0;
location /api/ {
proxy_pass http://0.0.0.0:8000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_http_version 1.1;
proxy_redirect off;
proxy_buffering off;
}
}
做软链接:
$ sudo ln -s /etc/nginx/sites-available/iems5722.conf /etc/nginx/sites-enabled/iems5722.conf
最后重启nginx服务:
$sudo netstat -nlp | grep :80
$sudo kill xxxx
$ sudo service nginx restart
References
<a href="https://github.com/leoymr/Android-self-learning/blob/master/android-server%E9%85%8D%E7%BD%AE.md">Android server 端配置问题汇总</a>
<a href="https://github.com/leoymr/Android-Instant-MSG">安卓即时通信软件</a>