1. 程式人生 > >GCM(谷歌雲推送)客戶端伺服器端開發全指南(客戶端)

GCM(谷歌雲推送)客戶端伺服器端開發全指南(客戶端)

由於谷歌雲推送GCM升級成為FCM,所以此部落格能容僅供參考 ————2016.12.2更新

最近因為手頭上的工作所以耽誤了一下指南客戶端篇的編寫,而且客戶端的功能實現是比較複雜的,處理的邏輯也是比較多的,所以也花了點時間去研究了一下。

沒有看我之前兩篇部落格的朋友這個demo是實現不了的,建議先去看了再來看這篇

現在我已經假設你的push伺服器已經搭建好了,API KEY和SENDER也已經拿到手了,這樣就可以繼續我們今天的開發,因為Google已經放棄了Eclipse,所以在GCM的官網上你已經找不到有關Eclipse的教程,但是配置檔案的設定等方法我們還是能參照的。

好的,我們正式開始客戶端的開發,首先你需要的是gcm.jar這個jar包,這個jar包提供regID的註冊、訊息的處理等方法,樓主曾經在百度上找,但找到的都是比較舊的版本,在結合我的demo後會出現預料之外的錯誤,所以樓主決定從SDK Manager上去下載。

找到你androidSDK的目錄,並點選進去,找到SDK Manager.exe並開啟
這裡寫圖片描述

在Extras中找到Google Cloud Messaging for Android Library並安裝,如果你沒有這個選項,把Obsolete勾選上你就能看到了,這個單詞是“過時的”意思,我想你應該能猜到什麼原因了吧。

當你下載安裝好了之後,你就能在
andrioidSDK\sdk\extras\google\gcm\samples\gcm-demo-client\libs
找到gcm.jar這個jar包
這裡寫圖片描述

把這個jar包拷貝到你的專案libs資料夾裡面就可以使用了
這裡寫圖片描述

然後我們就可以開始寫程式碼了,首先我把註冊的流程劃分了一下
這裡寫圖片描述

經過以上流程圖的分析,相信大家對我註冊regID的流程有一定的瞭解

以下是我的MianActivity類:

package com.x.gcmdemo;

import com.google.android.gcm.GCMRegistrar;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import
android.os.Bundle; import android.util.Log; import android.widget.TextView; public class MainActivity extends Activity { TextView messagetext; private boolean supperGCM = false; private String regId = ""; private SaveRegIdThead saveRegIdThead; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); CheckGoogleService.mContext = getApplicationContext(); //檢驗是否支援GCM,若支援則在最後賦true給supperGCM if(CheckGoogleService.checkDevice()){if(CheckGoogleService.checkManifest()){supperGCM = true;}} setContentView(R.layout.activity_main); findById(); //判斷是否支援GCM,支援則開始註冊 if(supperGCM){goRegister();} } private void goRegister() { //註冊一個全域性廣播,播放收到的訊息 registerReceiver(mGcmConntionStatuReceiver,new IntentFilter(CommonUtilitie.getInstance().getSendBroadAction())); //獲取regID regId = GCMRegistrar.getRegistrationId(this); Log.v("MainActivity", "regId = " + regId); if(regId.equals("")){ //如果regID獲取的值為空,則去註冊一個 GCMRegistrar.register(this, CommonUtilitie.getInstance().getSenderId()); //其實到這裡我們的裝置已經可以接收到google推送的訊息了,但是根據google 的建議,我們最好把 regID儲存到我們的伺服器中,不要儲存到本地,這裡有個安全的問題 }else{ //GCMRegistrar提供了一個方法可以檢查GCM是否已經儲存到自己的server if(GCMRegistrar.isRegisteredOnServer(this)){ //若已經儲存,則通知在等待訊息 messagetext.setText(R.string.registered + "\n"); }else{ //若沒有儲存,啟動儲存執行緒,儲存regID messagetext.setText(R.string.registered + "\n"); saveRegIdThead = new SaveRegIdThead(this); saveRegIdThead.execute(regId); } } } private void findById() { messagetext = (TextView) findViewById(R.id.messagetext); } private final BroadcastReceiver mGcmConntionStatuReceiver = new BroadcastReceiver(){ @Override public void onReceive(Context context, Intent intent) { //這裡的全域性廣播用於顯示儲存regID到伺服器的進度以及一些提示 String Message = intent.getExtras().getString(CommonUtilitie.getInstance().getIntentMessage()); messagetext.setText(messagetext.getText().toString() + Message + "\n"); } }; @Override protected void onDestroy() { //登出所有的例項以騰出記憶體 if(supperGCM){ if (saveRegIdThead != null) { saveRegIdThead.cancel(true); } GCMRegistrar.onDestroy(this); } unregisterReceiver(mGcmConntionStatuReceiver); super.onDestroy(); } }

程式碼片上我都添加了詳細的註釋,這裡大概說明一下我呼叫的類和他們處理的事情:

  • CheckGoogleService
    這個類是攻來檢查裝置支不支援GCM的

  • GCMRegistrar
    這個類是gcm.jar提供給我們用來處理regID的邏輯的,包括註冊、登出、記錄等。

  • CommonUtilitie
    CommonUtilitie是我自己寫的一個全域性單例工具類,可以讓任意類使用,裡面維護著一些要用到的資訊。

  • SaveRegIdThead
    SaveRegIdThead是用於非同步儲存regID的類.

然後讓我們先看一下CheckGoogleService

package com.x.gcmdemo;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.google.android.gcm.GCMConstants;

import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;


/**
 *這個類是檢查當前裝置支不支援google service,<br>
 * Google自帶的類如果檢測到不支援就會 Throw the exception<br>
 * 我讀過他的原始碼後發現不支援的情況下最多隻是收不到訊息,<br>
 * 並不會影響程式的執行,所以決定rewrite他的程式碼,<br>
 * 但是考慮到每個人想到的邏輯可能不一樣,google的程式設計師選擇丟擲異常一定有他的理由<br>
 * 所以rewrite寫成返回true or false,false就不進行註冊<br>
 * 
 * @author xie
 * */
public class CheckGoogleService {

    static Context mContext;
    private static final String GSF_PACKAGE = "com.google.android.gsf";
    private static String TAG = "CheckGoogleService";


    //檢查當前手機裝置是否支援,Android2.2以上
    public static boolean checkDevice(){
        int version = Build.VERSION.SDK_INT;
        if(version < 8){
            Toast.makeText(mContext, "Your phone's version is too low.", Toast.LENGTH_LONG).show();
            return false;
        }

        PackageManager packageManager = mContext.getPackageManager();

        try {
            //嘗試提取google包資訊
            packageManager.getPackageInfo(GSF_PACKAGE, 0);
        } catch (NameNotFoundException e) {
            Toast.makeText(mContext, "Your phone isn't install google service.", Toast.LENGTH_LONG).show();
            return false;
        }
        return true;
    }

    public static boolean checkManifest(){
        PackageManager packageManager = mContext.getPackageManager();
        String packageName = mContext.getPackageName();
        String permissionName = packageName + ".permission.C2D_MESSAGE";
        // 檢查是否已經添加了許可權
        try {
            packageManager.getPermissionInfo(permissionName,
                    PackageManager.GET_PERMISSIONS);
        } catch (NameNotFoundException e) {
            Toast.makeText(mContext, "No permission to access,are you add 'permission.C2D_MESSAGE' in AndroidManifest.xml?", Toast.LENGTH_LONG).show();
            return false;
        }       

        // 檢查接收器是否正常工作
        PackageInfo receiversInfo = null;
        try {
            receiversInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_RECEIVERS);
        } catch (NameNotFoundException e) {
            Toast.makeText(mContext, "RECEIVERS has some exception", Toast.LENGTH_LONG).show();
            return false;
        }

        ActivityInfo[] receivers = receiversInfo.receivers;
        if (receivers == null || receivers.length == 0) {

            return false;
        }
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "number of receivers for " + packageName + ": " +
                    receivers.length);
        }


        Set<String> allowedReceivers = new HashSet<String>();
        for (ActivityInfo receiver : receivers) {
            if (GCMConstants.PERMISSION_GCM_INTENTS.equals(receiver.permission)) {
                allowedReceivers.add(receiver.name);
            }
        }
        if (allowedReceivers.isEmpty()) {
            Toast.makeText(mContext, "No receiver allowed to receive " + GCMConstants.PERMISSION_GCM_INTENTS, Toast.LENGTH_LONG).show();
            return false;
        }

        checkReceiver(mContext, allowedReceivers,
                GCMConstants.INTENT_FROM_GCM_REGISTRATION_CALLBACK);
        checkReceiver(mContext, allowedReceivers,
                GCMConstants.INTENT_FROM_GCM_MESSAGE);


        return true;
    }


    private static void checkReceiver(Context context,
            Set<String> allowedReceivers, String action) {
        PackageManager pm = context.getPackageManager();
        String packageName = context.getPackageName();
        Intent intent = new Intent(action);
        intent.setPackage(packageName);
        List<ResolveInfo> receivers = pm.queryBroadcastReceivers(intent,
                PackageManager.GET_INTENT_FILTERS);
        if (receivers.isEmpty()) {
            throw new IllegalStateException("No receivers for action " +
                    action);
        }
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "Found " + receivers.size() + " receivers for action " +
                    action);
        }
        // make sure receivers match
        for (ResolveInfo receiver : receivers) {
            String name = receiver.activityInfo.name;
            if (!allowedReceivers.contains(name)) {
                Toast.makeText(context,"Receiver " + name +" is not set with permission " +GCMConstants.PERMISSION_GCM_INTENTS, Toast.LENGTH_LONG).show();
            }
        }
    }

}

這個類涉及的是關於包的資訊的提取,這裡不再多闡述,我們繼續看一下CommonUtilitie

package com.x.gcmdemo;

import android.content.Context;
import android.content.Intent;

/**
 * 
 * 建立一個工具類,工具類維護著處理regid、訊息儲存的url等邏輯,作為一個工具,可以被多人使用,所以用單例模式建立
 * */
public class CommonUtilitie {

        private static CommonUtilitie instance = null;
        //儲存regID的伺服器的URL
        private final String SERVICE_URL = "http://192.168.1.114:9002/saveRegID.asmx";
        //SenderId
        private final String SENDER_ID = "你的SENDERID";
        //傳送接受廣播使用的action
        private final String SENDBROADACTION = "textchangemessage";

        private final String INTENTMESSAGE = "com.x.gcmdemo.INTENTMESSAGE";

        public static CommonUtilitie getInstance(){
            if(instance==null){
                synchronized(CommonUtilitie.class){
                    if(instance==null){
                        instance=new CommonUtilitie();
                    }
                }
            }
            return instance;
        }


        public String getIntentMessage(){
            return INTENTMESSAGE;
        }

        public String getSendBroadAction(){
            return SENDBROADACTION;
        }

        public String getServiceUrl(){
            return SERVICE_URL;
        }

        public String getSenderId(){
            return SENDER_ID;
        }

        public void SendMessageToMessageText(Context context,String message){
            Intent intent = new Intent(SENDBROADACTION);
            intent.putExtra(INTENTMESSAGE, message);
            context.sendBroadcast(intent);
        }
}

工具類也在上面已經說明了,而且根據單詞的意思也能看出具體用途,這裡也不多說,讓我們繼續看下去,接著是SaveRegIdThead類:

package com.x.gcmdemo;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Map.Entry;

import com.google.android.gcm.GCMRegistrar;

import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;

/**
 * <p>這個類繼承了AsyncTask並實現非同步訪問網路</p>
 * 
 * */
public class SaveRegIdThead extends AsyncTask<String,String, String>{

    private final int MAX_ATTEMPTS_CONNECTION = 5;

    private final String TAG = "SaveRegIdThead";

    private Context mContext;

    private static final int BACKOFF_MILLI_SECONDS = 2000;

    private static final Random random = new Random();

    SaveRegIdThead(Context context){
        this.mContext = context;
    }

    @Override
    protected String doInBackground(String... regId) {

        String result="";
        String serverUrl = CommonUtilitie.getInstance().getServiceUrl();
        Map<String, String> params = new HashMap<String, String>();
        params.put("regid", regId[0]);

        publishProgress("準備網路.....");
        //防止被網路抖動的原因影響到我們的訪問,我們這裡設定一個數,讓執行緒至少2秒之後再去訪問網路
        long backoff = BACKOFF_MILLI_SECONDS + random.nextInt(1000);

        for(int i = 1; i <=MAX_ATTEMPTS_CONNECTION ; i++){
            Log.v(TAG, backoff + "");
            try{
                publishProgress(mContext.getString(R.string.server_connect_tip, i, MAX_ATTEMPTS_CONNECTION-i));
                publishProgress("正在連結.....");
                result = goSave(serverUrl, params);
                //成功儲存到伺服器通知google
                GCMRegistrar.setRegisteredOnServer(mContext, true);
                publishProgress("成功儲存到伺服器");
            }catch(IOException e){

                if(i==MAX_ATTEMPTS_CONNECTION){
                    publishProgress("多次嘗試失敗,請檢查網路連結");
                    break;
                }
                try {
                    Thread.sleep(backoff);
                } catch (InterruptedException e1) {
                    Thread.currentThread().interrupt();
                }
                backoff *= 2;
            }
        }

        return result;
    }

    @Override
    protected void onProgressUpdate(String... values){
        CommonUtilitie.getInstance().SendMessageToMessageText(mContext,values[0]);
    }

    @Override
    protected void onPostExecute(String result){
        if(result!=null&&result.equals("")){
            CommonUtilitie.getInstance().SendMessageToMessageText(mContext,"regID儲存處理完成,返回的程式碼是:" + result);
        }
    }

    private String goSave(String serverUrl, Map<String, String> params) throws IOException{
        URL url;
        String result = "";
        try {
            //這裡的URL是需要自己開發儲存regID功能的服務,如果沒有也不影響我們功能的實現
            url = new URL(serverUrl);
        } catch (MalformedURLException e) {
            Toast.makeText(mContext,"invalid url: " + serverUrl, Toast.LENGTH_LONG).show();
            return "";
        }

        //使用迭代器一個個設定好httpbody的內容
        StringBuilder httpbodyBuilder = new StringBuilder();
        Iterator<Entry<String, String>> iterator = params.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<String, String> param = iterator.next();
            httpbodyBuilder.append(param.getKey()).append('=')
                    .append(param.getValue());
            if (iterator.hasNext()) {
                httpbodyBuilder.append('&');
            }
        }

        String httpbody = httpbodyBuilder.toString();
        byte[] bytes = httpbody.getBytes();

        //定義HttpURLConnection,google從android5.0開始就不再支援HttpClient了,習慣一下使用HttpURLConnection是好事
        HttpURLConnection conn = null;
        Log.v(TAG, "開始連線");
        try {
            conn = (HttpURLConnection) url.openConnection();
            conn.setDoOutput(true);
            conn.setUseCaches(false);
            conn.setFixedLengthStreamingMode(bytes.length);
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type","application/x-www-form-urlencoded;charset=UTF-8");
            OutputStream out = conn.getOutputStream();
            out.write(bytes);
            out.close();

            int status = conn.getResponseCode();
            if (status != 200) {
              throw new IOException("requect fail ,error is" + status);
            }
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
        return result;
    }

}

由於我們只需要一次訪問網路(除非網路出了問題)我就沒有使用第三方的網路訪問框架,只是自己寫了一個簡單的網路訪問方法。

到這裡如果你的程式執行沒問題的話,你已經能接收到訊息了,現在我們來寫接收訊息的services,

package com.x.gcmdemo;

import com.google.android.gcm.GCMBaseIntentService;

import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import android.util.Log;


/**
 * <p>以下包含的是GCM返回會呼叫到的方法:
 * <ol>
 *   <li>onRegistered(Context context, String regId): 收到註冊Intent後此方法會被呼叫,GCM分配的註冊ID會做為引數傳遞到裝置/應用程式對。通常,你應該傳送regid到你的伺服器,這樣伺服器就可以根據這個regid發訊息到裝置上。
 *   <li>onUnregistered(Context context, String regId): 當裝置從GCM登出時會被呼叫。通常你應該傳送regid到伺服器,這樣就可以登出這個裝置了。
 *   <li>onMessage(Context context, Intent intent): 當你的伺服器傳送了一個訊息到GCM後會被呼叫,並且GCM會把這個訊息傳送到相應的裝置。如果這個訊息包含有效負載資料,它們的內容會作為Intent的extras被傳送。
 *   <li> onError(Context context, String errorId): 當裝置試圖註冊或登出時,但是GCM返回錯誤時此方法會被呼叫。通常此方法就是分析錯誤並修復問題而不會做別的事情。
 *   <li>onRecoverableError(Context context, String errorId): 當裝置試圖註冊或登出時,但是GCM伺服器無效時。GCM庫會使用應急方案重試操作,除非這個方式被重寫並返回false。這個方法是可選的並且只有當你想顯示資訊給使用者或想取消重試操作的時候才會被重寫。
 * </ol>
 * </p>
 * */
public class GCMIntentService extends GCMBaseIntentService{


     private static final String TAG = "GCMIntentService";

        public GCMIntentService() {
            super(CommonUtilitie.getInstance().getSenderId());
        }

        @Override
        protected void onRegistered(Context context, String registrationId) {
            CommonUtilitie.getInstance().SendMessageToMessageText(context, "GCM註冊成功!!");
        }

        @Override
        protected void onUnregistered(Context context, String registrationId) {
            //這個函式是你登出regID時呼叫的,一般用不到,Google有規定一個regID預設能使用的時間是7天,7天后regID自動無效,需要重新註冊
            //這個時間也可以自己設定
        }


        @Override
        protected void onMessage(Context context, Intent intent) {
            //這個是接收到訊息的回撥方法
            String message = intent.getStringExtra("message");
            // 使用通知欄進行推送
            generateNotification(context, message);
        }


        @Override
        public void onError(Context context, String errorId) {
            CommonUtilitie.getInstance().SendMessageToMessageText(context, "註冊錯誤,錯誤程式碼是:" + errorId);
        }

        @Override
        protected boolean onRecoverableError(Context context, String errorId) {
            CommonUtilitie.getInstance().SendMessageToMessageText(context, "onRecoverableError錯誤,錯誤程式碼是:" + errorId);
            return super.onRecoverableError(context, errorId);
        }

        /**
         * Issues a notification to inform the user that server has sent a message.
         */
        private static void generateNotification(Context context, String message) {

            NotificationManager notificationManager = (NotificationManager)
                    context.getSystemService(Context.NOTIFICATION_SERVICE);

            NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context);
            mBuilder.setContentTitle(context.getString(R.string.app_name))
                    .setSmallIcon(R.drawable.ic_launcher)
                    .setTicker("你有一條新訊息")
                    .setStyle(new NotificationCompat.BigTextStyle())
                    .setContentText(message)
                    .setDefaults(Notification.DEFAULT_ALL)
                    .setWhen(System.currentTimeMillis())
                    .setAutoCancel(true);
            notificationManager.notify(0, mBuilder.build());

        }

}

另外,這裡我們還有一些許可權是需要新增給google service的,這在GCM的官網上有說明,然後以下就是AndroidManifest.xml的內容:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.x.gcmdemo"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="21" />

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<permission android:name="com.x.gcmdemo.permission.C2D_MESSAGE" android:protectionLevel="signature" />
<uses-permission android:name="com.x.gcmdemo.permission.C2D_MESSAGE" /> 
<!-- 允許App接收來自google的訊息 -->
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

<!-- 獲取google賬號需要的許可權 -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<!-- 即使在鎖屏狀態也能收到訊息 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />


    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>


        <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="com.x.gcmdemo" />
          </intent-filter>
</receiver> 
        <service android:name="com.x.gcmdemo.GCMIntentService" />

    </application>

</manifest>

好了,到這裡你已經能接收來自GCM的訊息了,趕快試一下
這裡寫圖片描述

接下來又是原始碼時刻了

至此,GCM開發指南就全部講完了,有沒有自己的水平又提升了的感覺?有就對了,要趁著這個勢頭,繼續向CSDN裡的大神學習。