2012-11-01

【Android】使用 GCM(Google Cloud Messaging)

** GCM 快要停用了,請儘快轉換到 FCM(Firebase Cloud Messaging | Firebase)


Google 已宣佈停用 C2DM(Cloud to Device Messaging),原 C2DM 的 app 應立即移植到 GCM。

GCMGoogle 提供的一項服務,可讓 Android app 的開發者從自己的伺服器傳送訊息到安裝在 Android 設備的 appGCM 僅提供上限 4kb 的輕量資料傳遞


關於 GCM 的運用:
可由您的官方(網站) 發送活動訊息給有安裝 appAndroid 裝置。
開發社群 app,例如甲上傳的最新動態照片,朋友乙便會知道。
您可能會聯想到即時通訊,不過 Google 並不保證每封訊息的依序發送,並且有 4KB 的上限,所以您若要利用 GCM 開發即時通訊, 您自己的 server 可能要擔負蠻多工作的,例如甲乙兩人對話內容的先後順序,及較大資料量 或非文字(如照片) 的傳送。

開啟 Google APIs Console page (還沒 google 帳戶的,快去申請一個吧......)

建立新 project


輸入 project 名稱:


建立 project 後,原 project 下拉功能表會顯示新建立的 project 名(此例為 test),並指在 Services,另外,您會在網址列看到如下:

 
https://code.google.com/apis/console/#project:4815162342 

4815162342 就是您的 project ID,也是 GCM 的 sender ID,之後的 CommonUtilities.java 會用到。
而在畫面右方,是各種 Google API service 的啟用狀態,
 
找到 Google Cloud Messaging for Android
OFF 變更為 ON,第一次啟用 Google API services 會出現使用條約,必須勾選同意。
 






您也可以在 Active 頁籤 看到 GCM 的狀態為 ON




切換到 API Access 選項,您應該會看到一個自動產生的 "Simple API Access" key",
若沒有,則點擊 "Create new Server key..."






直接按左下角的 Create






您會看到 API key




這樣就會重新產生新的 API key,只不過新的 key 不會馬上生效,生效時間顯示在標題為 Activated on 那一列,而左下最後二列則是舊的 key 及其到期時間,另外,其時間可能和您的時區不同,所以您可以這樣估算,舊 key 可使用期限約為您產生新 key 後 24 小時。





再來看 Android 端要做的工作,因為 GCM 會使用到 Google API,所以我們先開啟 SDK Manager 下載安裝 Google APIs,只要勾選您要開發的目標設備 Android APIs 版本平台的 Google APIs 安裝就可以了。











您在 AVDManager(Android Virtual Device Manager) 中建立的 Android 模擬器必須要能支援 Google APIs,大多數人應該一開始都沒有建立支援 Google APIs 的模擬器,所以就另外建立新的模擬器吧;我想我要開發的 app 多會用到,所以就一次將所有的模擬器都改為使用 Google API 的了。








新增的 Android 模擬器中,還要輸入已註冊的 google 帳戶




接下來在 Eclipse 打開 SDK Manager,安裝 Extras > Google Cloud Messaging for Android Library
SDK Manager 會在您的 /android sdk 安裝目錄/extras/google/ 下建立一個 gcm 目錄,且在 gcm 目錄下包含
gcm-client, gcm-server, samples/gcm-demo-client, samples/gcm-demo-server, samples/gcm-demo-appengine 幾個子目錄。



/android sdk 安裝目錄/extras/google/gcm/gcm-client/dist 目錄下找到 gcm.jar 並複製到您 app 的 /libs/ 下

接下來修改您程式專案的 AndroidManifest.xml
 
<!-- 注意 minSdkVersion 要 8 以上 -->
<uses-sdk android:minsdkversion="8" android:targetsdkversion="17" />

...
...    

<permission android:name="完整package名稱.permission.C2D_MESSAGE" android:protectionLevel="signature" />
<uses-permission android:name="完整package名稱.permission.C2D_MESSAGE" />

...
... 
 
<!-- 使用GCM -->
<uses-permission android:name="完整package名稱.permission.C2D_MESSAGE" />
<!-- 存取 internet -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- App receives GCM messages. -->
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<!-- GCM requires a Google account. -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<!-- Keeps the processor from sleeping when a message is received. -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- 手機震動 -->
<uses-permission android:name="android.permission.VIBRATE" />
    
...
...
 
<application>
...
...
    <!-- 接收 GCM 的 receiver -->
    <receiver 
        android:name="com.google.android.gcm.GCMBroadcastReceiver" 
        android:permission="com.google.android.c2dm.permission.SEND">
        
        <intent-filter>
            <action android:name="com.google.android.c2dm.intent.RECEIVE" />
            <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
            <category android:name="完整的 app package 名稱" />
        </intent-filter>
    </receiver>
  
    <!-- GCM service -->
    <service android:name=".GCMIntentService" />
</application>
...
...
 


直接拿 Google 提供的範例程式來改,到 /android sdk 安裝目錄/extras/google/gcm/samples/gcm-demo-client/src/com/google/android/gcm/demo/app/ 下找到 GCMIntentService.java 複製到您程式的 /src/ 下,
並加以修改,主要覆寫修改 onMessage,generateNotification 幾個函式。
 
@Override
protected void onMessage(Context context, Intent intent)
{
    Log.i(TAG, "Received message");
    // 接收 GCM server 傳來的訊息
    Bundle bData = intent.getExtras();

    // 處理 bData 內含的訊息
    // 在本例中, 我的 server 端程式 gcm_send.php 傳來了 message, campaigndate, title, description 四項資料
    String message = bData.getString("message");
    String campaigndate = bData.getString("campaigndate");
    String title = bData.getString("title");
    String description = bData.getString("description");
    ...
    ...
    // 通知 user
    generateNotification(context, bData);
}
 
...
...

// 注意這裡我不是直接改寫原範例的generateNotification()
// 範例的 generateNotification() 傳入的參數是 Context, String
// 利用 Java 的 函數重載 特性, 我保留了原 generateNotification(), 說不定以後會用到
// 另外增加了一個傳入參數為 Context, Bundle 的 generateNotification()
private static void generateNotification(Context context, Bundle data)
{
    int icon = R.drawable.ic_launcher;
    long when = System.currentTimeMillis();
    NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    Intent ni = new Intent(context, MainActivity.class);
    ni.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
    PendingIntent intent = PendingIntent.getActivity(context, 0, ni, 0);
    // 如果您想讓通知的內容有動態的變化
    // 就可以運用傳進來的參數 -- Bundle 型別 data
    // 取出您要的欄位填入 setContentTitle() 和 setContentText()
    // ;-)
    Notification noti = new NotificationCompat.Builder(context)
                                              .setContentTitle("收到 GCM 通知囉")
                                              .setContentText("太棒了! 大成功!!")
                                              .setContentIntent(intent)
                                              .setDefaults(Notification.DEFAULT_ALL)
                                              .setSmallIcon(icon)
                                              .setWhen(when)
                                              .build();
    nm.notify(0, noti);
}
 
複製範例程式的 strings.xml 中與 GCM 有關的常數:
 
<!-- GCM(Google Cloud Messaging) -->
<string name="error_config">Please set the %1$s constant and recompile the app.</string>
<string name="already_registered">Device is already registered on server.</string>
<string name="gcm_registered">From GCM: device successfully registered!</string>
<string name="gcm_unregistered">From GCM: device successfully unregistered!</string>
<string name="gcm_message">From GCM: you got message!</string>
<string name="gcm_error">From GCM: error (%1$s).</string>
<string name="gcm_recoverable_error">From GCM: recoverable error (%1$s).</string>
<string name="gcm_deleted">From GCM: server deleted %1$d pending messages!</string>
<string name="server_registering">Trying (attempt %1$d/%2$d) to register device on Demo Server.</string>
<string name="server_registered">From Demo Server: successfully added device!</string>
<string name="server_unregistered">From Demo Server: successfully removed device!</string>
<string name="server_register_error">Could not register device on Demo Server after %1$d attempts.</string>
<string name="server_unregister_error">Could not unregister device on Demo Server (%1$s).</string>
<string name="options_register">Register</string>
<string name="options_unregister">Unregister</string>
<string name="options_clear">Clear</string>
<string name="options_exit">Exit</string>
 
另外再複製 CommonUtilities.java,修改
 
...
...
static final String SERVER_URL = "http://您的網址";
static final String SENDER_ID = "481????342";
static final String DISPLAY_MESSAGE_ACTION = "完整package名稱.DISPLAY_MESSAGE";
...
...
 
複製並修改 ServerUtilites.java
 
static boolean register(final Context context, final String regId)
{
    Log.i(TAG, "registering device (regId = " + regId + ")");
    String serverUrl = SERVER_URL + "/接收gcm註冊.php";    // 就是文後的 gcm_register.php
    ...
    ...
}

...
...

// GCM 的註銷流程已變更,這段應不需要了
public static void unregister(final Context context, final String regId)
{
    Log.i(TAG, "unregistering device (regId = " + regId + ")");
    String serverUrl = SERVER_URL + "/註銷gcm.php";    // 就是文後的 gcm_unregister.php
    ...
    ...
}
 
主程式 activity 內容
 
@Override
protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    new GCMTask().execute();
    ....
    ....
}

...
...
 
private class GCMTask extends AsyncTask<Void, Void, Void>
{
    protected Void doInBackground(Void... params)
    {
        Log.d(TAG, "檢查裝置是否支援 GCM");
        // 檢查裝置是否支援 GCM
        GCMRegistrar.checkDevice(MainActivity.this);
        GCMRegistrar.checkManifest(MainActivity.this);
        final String regId = GCMRegistrar.getRegistrationId(MainActivity.this);
        if (regId.equals(""))
        { 
            Log.d(TAG, "尚未註冊 Google GCM, 進行註冊");
            GCMRegistrar.register(MainActivity.this, SENDER_ID);
        }
        return null;
    }
}
 


server 端建置

Google 稱開發者的 serverthird party serverthird party server 通常就是您的網站,至少要具備 2 個功能:
一是接收 Android 裝置上傳 GCM 註冊成功的 regId
一是負責處理註銷 regId

另外有個功能是用來發送訊息給 Android client 端 或 接收 client 端上傳的訊息,如果只有需要發送訊息的功能,那麼這支程式您可以放在您的網站(third party server)上 或是 放在公司內某個部門的電腦內;如果還要接收 client 端上傳的訊息,那這支程式就得放在您的網站上。

要納入考慮的是,當安裝您 appAndroid 裝置數達成千上萬時,執行網站上的 php 可能會有效率上的問題。

不過,本篇筆記的重點目的是傳送訊息給 client 端,所以這支發送訊息的程式(gcm_send.php) 仍是以 php 撰寫,並放在網站上,做為網站的後台程式。

接收註冊的程式 gcm_register.php
 
if(isset($_POST['regId']))
{
    $regId = $_POST['regId'];
    $sql = "INSERT INTO 資料表 (gcm_id,...) VALUES ('$regId',...)";
    $pdo->exec($sql);
}
 


GCM 運作流程

安裝好的 app 第一次執行時,app 會向 Google 註冊並取得 regIdapp 成功取得 regId 後便將 regId 傳送給您網站 gcm_register.php,您可在 ADTlog 中看到取得的 regIdregId 長達 162 字元, 如果您想將 regId 儲存於資料庫系統內,則您需要建立一個 table 存放 Android 裝置傳上來的 regId,存放 regId 的欄位長度最好大於 162 字元,因為以 Android 設備爆炸性成長的速度來看,如果愈來愈多開發人員採用 GCM,那麼 regId 長度勢必再增加...

當您的網站(third party server) 接收並儲存註冊 GCM 成功的 regId,爾後您的網站便可以發送訊息到已註冊的 app。

當您的 third party server 要發送訊息給有安裝您 appAndroid 裝置時,您的 server 是將訊息發送給 Google GCM server,由 Google GCM server 再將訊息轉發給您 指定的 regId

而當 Android 裝置解除安裝您的 app 時,Google GCM server 並不會立即通知您的 thrid party server,而是在下一次您發送訊息給該 Android 裝置時,Google GCM server 才會回應給您的 third party server 錯誤,錯誤的內容是該裝置並未註冊,所以您的 third party server 要在此時將該裝置的 regId 從您的資料庫中刪除。


Google GCM server 會傳回發送給每個 Android 裝置訊息的結果,回傳的結果也是 json 格式,由 json 轉為陣列,各元素帶有 element 名稱,如下。
我們需要的是 results 陣列,而 results 陣列內,又是兩維陣列 -- errormessage_id,(關於 PHP 陣列的使用請自行參考 PHP 官網 )
 
{
  "multicast_id":575?????46362470343,
  "success":117,
  "failure":83,
  "canonical_ids":0,
  "results":[
              {"error":"NotRegistered"},
              {"error":"NotRegistered"},
              {"message_id":"0:13957176817?????%fcbf9d11f9fd7ecd"},
             
              ...
              ...

              {"message_id":"0:13957176817?????%fcbf9d11f9fd7ecd"}
           ]
}
 

其他 GCM server 的回應碼請參考 -- GCM HTTP Connection Server - Response format



發送訊息 gcm_send.php
 
// 這支程式就是將自己模擬成 client 端,
// 發送 POST 給 Google GCM server
// 並在 Google GCM server 回傳某 Android 裝置已移除您的 app 時
// 做對應的處理

$apiKey = "AIzaSyBFy-blablabla-ye3K-3qxovUXTvvG99-bla";

// 列出要發送的 user 端 Android 裝置
$sql = "SELECT * FROM gcm_client_資料表";
$rs = $pdo->query($sql);

$recnt = count($rs);
 
// 總記錄數/每批發送數, 若不能整除, 則發送訊息迴圈數 +1
$SendMax = 1000;     // 每批發送訊息給 Android 裝置數
$SendLoop = ceil($recnt/$SendMax);

$pkey = -1;
for($x=0;$x<$SendLoop;$x++)
{
    $aRegID = array();
    
    for($y=0;$y<$SendMax;$y++)
    {
        $index = ($x*$SendMax) + $y;
        if($index<$recnt)
            {
                $row = $rs[$index];
                array_push($aRegID, $row['gcm_id']);
            }
        else
            {
                break;
            }
    }
        
    // Set POST variables
    $url = 'https://android.googleapis.com/gcm/send';

    // 要發送的訊息內容
    // 例如我要發送 message, campaigndate, title, description 四樣資訊
    // 就將這 4 個組成陣列
    // 您可依您自己的需求修改
    $fields = array('registration_ids'  => $aRegID,
                    'data'              => array( 'message' => $message,
                                                    'campaigndate' => $campaigndate,
                                                    'title' => $title,
                                                    'description' => $description
                                                  )
                  );

    $headers = array('Content-Type: application/json',
                     'Authorization: key='.$apiKey
                    );

    // Open connection
    $ch = curl_init();
    // Set the url, number of POST vars, POST data
    curl_setopt($ch, CURLOPT_URL, $url );
    curl_setopt($ch, CURLOPT_POST, true );
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true );
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));

    // 送出 post, 並接收回應, 存入 $result
    $result = curl_exec($ch);
    
    // 由回傳結果, 取得已解除安裝的 regID
    // 自資料庫中刪除
    $aGCMresult = json_decode($result,true);
    $aUnregID = $aGCMresult['results'];
    $unregcnt = count($aUnregID);
    for($i=0;$i<$unregcnt;$i++)
    {
        $aErr = $aUnregID[$i];
        if($aErr['error']=='NotRegistered')
        {
            $sqlTodel = "DELETE FROM gcmclient
                             WHERE gcm_id='".$aRegID[$i]."' ";
            $pdo->query($sqlTodel);
        }
    }

    // Close connection
    curl_close($ch);
    // GCM end -------
    unset($aRegID);
}
 

發送訊息的限制

GCM server 在發送每一則訊息的人數上限為 1000,請參考 -- Send Multicast Messages Efficiently


相關筆記 ----
【Android Studio】GCM client 端設定 - 速成篇
【Android Studio】從 GCM 移植到 FCM