要如何開發出能接收Server通知訊息(如活動消息、聊天訊息)的Android App呢?Google提供了Google Cloud Messaging(GCM)服務,能將您想要推送給客戶端裝置的訊息交給GCM伺服器來處理,Google的推播伺服器會將收到的訊息推播給Android客戶端裝置,接收到訊息的裝置可以將訊息處理後並顯示在通知欄,達成通知使用者的目的。
Google Cloud Messaging 運作機制
使用GCM來實作Android推播功能,運作方式如下圖,可以分為三個部份:一、註冊API Key(紫色部份)
如果要使用Google API,一定要先到Google Developers Console中拿到API Key。若在Console上還沒有專案,必須先新增一個。
接著到這個專案中,將Google Cloud Messaging for Android的API設定為啟用。
然後替這個專案加入一個能讓App Server使用的Public API Key,並將API Key存到App Server中,再將Client ID(或是Project number)存到要接收推播的Android App中,作為Sender ID。
做這個部份的用意是要讓App Server有權限能夠存取Google伺服器,並且能夠使用GCM。將Client ID存入Android App中,是為了提供給下一個部份使用。
二、Android客戶端裝置註冊GCM(藍色部份)
為了使GCM能夠辨識出訊息要傳給哪些裝置,必須讓這些裝置先向GCM註冊。因此Android裝置必須提供Client ID(Sender ID)給GCM,來要求使用GCM的功能(提供Sender ID是為了要讓GCM知道是由哪個上個部份提到的Console專案所發出的通知訊息)。如果GCM同意讓Android裝置使用它的功能,會回傳一個獨特的,代表這個Android裝置的Registration ID。這個Registration ID應該要交由App Server來儲存,讓App Server知道底下有哪些Android裝置能夠進行訊息推播。三、App Server推播訊息給Android裝置(橘色部份)
一旦App Server有了第一部份和第二部份所拿到的Public API Key和Registration ID,就可以向GCM傳送要推播給指定Android裝置的訊息。GCM在接收到App Server傳來的訊息後,會去判斷傳入的Registration ID(s)對應到哪個(哪些)裝置,在不固定的時間內將訊息推送出去。換句話說,Android裝置並不一定會「立刻」收到App Server所發出的訊息。事實上,Android裝置也可以傳送訊息給GCM,然後反推給App Server,只不過傳送的方式並非一般的HTTP,實際上也不是那麼常使用,所以就不針對這個部份討論。
實作支援GCM的App Server
要向GCM發送訊息,必須透過下面這個HTTPS連線網址:https://android.googleapis.com/gcm/send在傳送Request的時候,Header必須包含以下屬性:
Authorization: key= 第一部份拿到的public API Key Content-Type: application/json資料要以POST的方式傳送,資料格式須為Json,且Json格式如下:
{ "registration_ids":["registration_id_1","registration_id_2",...,,"registration_id_n"], "data":{"attr_1":value_1,"attr_2":value_2,...,"attr_n":value_n}, }發送給GCM Request後,GCM將回傳一個Json回來,格式如下:
{"multicast_id":0000000000,"success":2,"failure":0,"canonical_ids":0,"results":[{"message_id":"0:000000000000"}success欄位為推播能夠成功的裝置數量,failure欄位為推播失敗的裝置數量。
實作支援GCM的Android Client
以下是實作GCM的Android Client,必須注意的項目:- 需要加入到Android專案的檔案有:GcmBroadcastReceiver.java, MagicLenGCM.java。
- 需要加入到Android專案的Library有:google-play-services_lib。
- Android Target必須包含Google API。
- Android Client必須執行在有安裝Google Play App並且已經設置Google帳戶的Android環境。
以下是每個檔案中,需要新增或是修改的程式碼。
AndroidManifest.xml
permission
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
application
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<receiver
android:name="org.magiclen.gcmclient.GcmBroadcastReceiver"
android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="org.magiclen.gcmclient" />
</intent-filter>
</receiver>
以上的「org.magiclen.gcmclient」記得改成自己的package名稱。
MagicLenGCM.java
import java.io.IOException;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.AsyncTask;
import android.support.v4.app.NotificationCompat;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.gcm.GoogleCloudMessaging;
/**
* GCM相關類別.
*
* @author magiclen
*
*/
public class MagicLenGCM {
// ----------類別常數(須自行修改)----------
/**
* Google Developers Console 的 Project Number
*/
public final static String SENDER_ID = "664551525302";
// ----------類別列舉----------
public static enum PlayServicesState {
SUPPROT, NEED_PLAY_SERVICE, UNSUPPORT;
}
public static enum GCMState {
PLAY_SERVICES_NEED_PLAY_SERVICE, PLAY_SERVICES_UNSUPPORT, NEED_REGISTER, AVAILABLE;
}
// ----------類別介面----------
public static interface MagicLenGCMListener {
/**
* GCM註冊結束
*
* @param successfull
* 是否註冊成功
* @param regID
* 傳回註冊到的regID
*/
public void gcmRegistered(boolean successfull, String regID);
/**
* GCM註冊成功,將結果寫入App Server
*
* @param regID
* 傳回註冊到的regID
* @return 是否傳送App Server成功
*/
public boolean gcmSendRegistrationIdToAppServer(String regID);
}
// ----------類別常數----------
/**
* 用來當作SharedPreferences的Key.
*/
private static final String PROPERTY_REG_ID = "registration_id";
private static final String PROPERTY_APP_VERSION = "appVersion";
/**
* 使用MagicLenGCM的Activity可以實作這個ActivityResult號碼
*/
public final static int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
// ----------類別方法----------
/**
* 發出Local端的通知(顯示在通知欄上)
*
* @param context
* Context
* @param notifyID
* 通知ID(重複會被覆蓋)
* @param drawableSmallIcon
* 小圖示(用Drawable ID來設定)
* @param title
* 標題
* @param msg
* 訊息
* @param info
* 附加文字
* @param autoCancel
* 是否按下後就消失
* @param pendingIntent
* 按下後要使用什麼Intent
*/
public static void sendLocalNotification(Context context, int notifyID,
int drawableSmallIcon, String title, String msg, String info,
boolean autoCancel, PendingIntent pendingIntent) {
NotificationManager mNotificationManager = (NotificationManager) context
.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
context).setSmallIcon(drawableSmallIcon).setContentTitle(title)
.setContentText(msg).setAutoCancel(autoCancel)
.setContentInfo(info).setDefaults(Notification.DEFAULT_ALL);
if (msg.length() > 10) {
mBuilder.setStyle(new NotificationCompat.BigTextStyle()
.bigText(msg));
}
mBuilder.setContentIntent(pendingIntent);
mNotificationManager.notify(notifyID, mBuilder.build());
}
// ----------物件變數----------
private Activity activity;
private MagicLenGCMListener listener;
// ----------建構子----------
public MagicLenGCM(Activity activity) {
this(activity, null);
}
public MagicLenGCM(Activity activity, MagicLenGCMListener listener) {
this.activity = activity;
setMagicLenGCMListener(listener);
}
// ----------物件方法----------
/**
* 取得Activity
*
* @return 傳回Activity
*/
public Activity getActivity() {
return activity;
}
public void setMagicLenGCMListener(MagicLenGCMListener listener) {
this.listener = listener;
}
/**
* 開始接上GCM
*
* @return 傳回GCM狀態
*/
public GCMState startGCM() {
return openGCM();
}
/**
* 開始接上GCM
*
* @return 傳回GCM狀態
*/
public GCMState openGCM() {
switch (checkPlayServices()) {
case SUPPROT:
String regid = getRegistrationId();
if (regid.isEmpty()) {
registerInBackground();
return GCMState.NEED_REGISTER;
} else {
return GCMState.AVAILABLE;
}
case NEED_PLAY_SERVICE:
return GCMState.PLAY_SERVICES_NEED_PLAY_SERVICE;
default:
return GCMState.PLAY_SERVICES_UNSUPPORT;
}
}
public String getRegistrationId() {
final SharedPreferences prefs = getGCMPreferences();
String registrationId = prefs.getString(PROPERTY_REG_ID, "");
if (registrationId.isEmpty()) {
return "";
}
// 檢查程式是否有更新過
int registeredVersion = prefs.getInt(MagicLenGCM.PROPERTY_APP_VERSION,
Integer.MIN_VALUE);
int currentVersion = getAppVersion();
if (registeredVersion != currentVersion) {
return "";
}
return registrationId;
}
public int getAppVersion() {
try {
PackageInfo packageInfo = activity.getPackageManager()
.getPackageInfo(activity.getPackageName(), 0);
return packageInfo.versionCode;
} catch (NameNotFoundException e) {
// 不可能會發生
throw new RuntimeException("Could not get package name: " + e);
}
}
private SharedPreferences getGCMPreferences() {
return activity.getSharedPreferences(activity.getClass()
.getSimpleName(), Context.MODE_PRIVATE);
}
/**
* 檢查Google Play Service可用狀態
*
* @return 傳回Google Play Service可用狀態
*/
public PlayServicesState checkPlayServices() {
int resultCode = GooglePlayServicesUtil
.isGooglePlayServicesAvailable(activity);
if (resultCode != ConnectionResult.SUCCESS) {
if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) {
GooglePlayServicesUtil.getErrorDialog(resultCode, activity,
PLAY_SERVICES_RESOLUTION_REQUEST).show();
return PlayServicesState.NEED_PLAY_SERVICE;
} else {
return PlayServicesState.UNSUPPORT;
}
}
return PlayServicesState.SUPPROT;
}
/**
* 在背景註冊GCM
*/
private void registerInBackground() {
new AsyncTaskRegister().execute();
}
private final class AsyncTaskRegister extends AsyncTask<Void, Void, String> {
@Override
protected String doInBackground(Void... params) {
String regid = "";
try {
GoogleCloudMessaging gcm = GoogleCloudMessaging
.getInstance(activity);
regid = gcm.register(SENDER_ID);
if (regid == null || regid.isEmpty()) {
return "";
}
// 儲存regID
storeRegistrationId(regid);
if (listener != null) {
if (!listener.gcmSendRegistrationIdToAppServer(regid)) {
storeRegistrationId("");
return "";
}
}
} catch (IOException ex) {
}
return regid;
}
@Override
protected void onPostExecute(String msg) {
if (listener != null) {
listener.gcmRegistered(!msg.isEmpty(), msg.toString());
}
}
}
private void storeRegistrationId(String regId) {
final SharedPreferences prefs = getGCMPreferences();
int appVersion = getAppVersion();
SharedPreferences.Editor editor = prefs.edit();
editor.putString(PROPERTY_REG_ID, regId);
editor.putInt(PROPERTY_APP_VERSION, appVersion);
editor.commit();
}
}
GcmBroadcastReceiver.java
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.content.WakefulBroadcastReceiver;
import android.util.Log;
import com.google.android.gms.gcm.GoogleCloudMessaging;
/**
* 接收來自GCM的訊息
*
* @author magiclen
*
*/
public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {
public static final int NOTIFICATION_ID = 0;
@Override
public void onReceive(Context context, Intent intent) {
Bundle extras = intent.getExtras();
GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(context);
String messageType = gcm.getMessageType(intent);
if (!extras.isEmpty()) {
if (GoogleCloudMessaging.MESSAGE_TYPE_SEND_ERROR
.equals(messageType)) {
Log.i(getClass() + " GCM ERROR", extras.toString());
} else if (GoogleCloudMessaging.MESSAGE_TYPE_DELETED
.equals(messageType)) {
Log.i(getClass() + " GCM DELETE", extras.toString());
} else if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE
.equals(messageType)) {
Log.i(getClass() + " GCM MESSAGE", extras.toString());
Intent i = new Intent(context, MainActivity.class);
i.setAction("android.intent.action.MAIN");
i.addCategory("android.intent.category.LAUNCHER");
MagicLenGCM.sendLocalNotification(context, NOTIFICATION_ID,
R.drawable.ic_launcher, "GCM 通知", extras
.getString("message"), "magiclen.org", false,
PendingIntent.getActivity(context, 0, i,
PendingIntent.FLAG_CANCEL_CURRENT));
}
}
setResultCode(Activity.RESULT_OK);
}
}
使用MagicLenGCM可以很快地完成GCM註冊以及取得Registration ID的功能。在GcmBroadcastReceiver內可以使用MagicLenGCM的sendLocalNotification,將接收到的推播訊息顯示在本地端的通知欄中。