要如何開發出能接收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欄位為推播失敗的裝置數量。

PHP範例下載

實作支援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,將接收到的推播訊息顯示在本地端的通知欄中。