2014 年 12 月 1 日

アンドロイドアプリからGoogle Cloud Messagingを使う方法(第2回)第2版

アンドロイドアプリからGoogle Cloud Messagingを使う方法(第2回)第2版

com.google.android.gcmパッケージがdeprecatedされて、現在、Google Cloud Messagingを利用するには、com.google.android.gms.gcmパッケージを利用する必要があります。
旧APIも当面は運用され続けているようですが、今後の開発はcom.google.android.gms.gcmパッケージを利用した方がよいでしょう。

このパッケージを利用するには、Google Play Services SDKが必要です。

新しいパッケージでは、XMPP(Extensible Messaging and Presence Protocol)がサポートされており、プッシュ通知のようなdownstreamへの送信だけなく、upstreamに向けてメッセージを送信することができます。

XMPPをサポートしたGCM Cloud Connection Serverに接続すれば、双方向メッセージ通信(device-to-cloud, cloud-to-device)が可能となっているようです。これを利用してプッシュ通知で受け取った内容に関して返信できるようで、プッシュ通知に対するフィードバックがほしい場合に利用できそうです。また、アンドロイド端末の同士のチャット機能の実装もできそうです。

目次

  1. Google Play Service SDKのインポート
  2. 必要なパーミッション
  3. レシーバーとサービスの登録
  4. Google Play services APKの検査
  5. GCMの初期処理
  6. レジストレーションIDの有無
  7. レジストレーションIDの取得
  8. レジストレーションIDの保存
  9. レシーバーの実装
  10. サービスの実装

Google Play Service SDKのインポート

インポート方法は、下記のサイトに書かれていますので、ここでは割愛します。

Add Google Play Services to Your Project

最新バージョンは現在(2014/12/1)6.1となっています。

Google Play Services

ライブラリをワークスペースにインポートした後、自分のアンドロイドアプリプロジェクトから参照します。

忘れてはいけないのは、AndroidManifest.xmlのapplication要素に次のメタデータを追加しておくことです。

<meta-data android:name="com.google.android.gms.version"
        android:value="@integer/google_play_services_version" />

プロガードの難読化を利用する際は、Create a Proguard Exceptionのところにも注意した方がよさそうです。

また、端末にGoogle Play services APKが存在しないと動作しません。利用においては必ずGoogle Play services APKが存在するかの確認が必要となります。

アプリケーション側で検査を実装する必要があるようで、ヘルパーメソッドが用意されています。Google Play services APKの有無やバージョンなどが検査されるようです。

この検査はonResume()で行うことがベストだとされています。しかし、レジストレーションIDの処理が必要なので、実際はonCreate時にも行います。

必要なパーミッション

少なくとも、以下の5つのパーミッションが必要です。パッケージ名からの相対パスで書いておく方が、後々保守がしやすいと思います。
GET_ACCOUNTSはAPI Level15以上が利用できるなら、必要ありませんが、含めておきます。

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

<permission android:name=".permission.C2D_MESSAGE" android:protectionLevel="signature" />
<uses-permission android:name=".permission.C2D_MESSAGE" />

レシーバーとサービスの登録

また、application要素には、プッシュ通知を受信するレシーバと通知されたメッセージを処理するサービスを登録します。
ここでは、それぞれGcmBroadcastReceiver,GcmIntentServiceという名前で登録しています。

<application ...>
  <receiver
    android:name=".GcmBroadcastReceiver"
    android:permission="com.google.android.c2dm.permission.SEND" >
    <intent-filter>
      <action android:name="com.google.android.c2dm.intent.RECEIVE" />
      <category android:name="com.example.gcm" />
    </intent-filter>
  </receiver>
  <service android:name=".GcmIntentService" />
</application>

後はソースコードを実装していきます。以降では、Implementing GCM Clientのサンプルコードを元に、実際に動作させたものを利用して説明します。

Google Play services APKの検査

サンプルでは、以下のように実装されています。MainActivity(最初に起動されるアクティビティ)で実装しておけばよいでしょう。

  private boolean checkPlayServices() {
    try {
      int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
      if (resultCode != ConnectionResult.SUCCESS) {
        if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) {
          GooglePlayServicesUtil.getErrorDialog(resultCode, this, PLAY_SERVICES_RESOLUTION_REQUEST).show();
        } else {
          Log.i(TAG, "This device is not supported.");
          finish();
        }
        return false;
      }
    } catch (Exception e) {
      Log.d(TAG, e.toString());
    }
    return true;
  }

GCMの初期処理

MainActivityのonCreateで、Google Play services APKの有無を確認し、存在すれば、レジストレーションIDを取得します。
レジストレーションIDが取得できたとき、アプリケーションのプリファレンスに保存しておいて、以降はこの保存したレジストレーションIDを利用します。

このようなメソッドを作成しておき、MainActivityのonCreateから呼び出せばよいでしょう。

  private void initGCM() {
    
    if (checkPlayServices()) {
      gcm = GoogleCloudMessaging.getInstance(this);
      regid = getRegistrationId(this);

      if (regid.isEmpty()) {
        registerInBackground();
      } else {
        Log.i(TAG, "registrationID = " + regid);
      }
    } else {
      Log.i(TAG, "No valid Google Play Services APK found.");
    }    
  }

次にgetRegistrationIdメソッドとregisterInBackgroundメソッドの内容を見ていきます。

レジストレーションIDの有無

プレファレンスに保存したレジストレーションIDがあれば、それを返します。なければ、空文字列を返します。
レジストレーションIDはアプリのバージョン番号とともに保存し、以後アプリのバージョンが上がれば、新しいレジストレーションIDを取得するために、空文字列を返します。

  private String getRegistrationId(Context context) {
    final SharedPreferences prefs = getGCMPreferences(context);
    String registrationId = prefs.getString(PROPERTY_REG_ID, "");
    if (registrationId.isEmpty()) {
      Log.i(TAG, "Registration not found.");
      return "";
    }
    // Check if app was updated; if so, it must clear the registration ID
    // since the existing regID is not guaranteed to work with the new
    // app version.
    int registeredVersion = prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE);
    int currentVersion = getAppVersion(context);
    if (registeredVersion != currentVersion) {
      Log.i(TAG, "App version changed.");
      return "";
    }
    return registrationId;
  }
  // プリファレンスを取得
  private SharedPreferences getGCMPreferences(Context context) {
    return getSharedPreferences(MainActivity.class.getSimpleName(),
              Context.MODE_PRIVATE);
  }
  // アプリのバージョン番号を取得
  private static int getAppVersion(Context context) {
    try {
      PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
      return packageInfo.versionCode;
    } catch (NameNotFoundException e) {
        // should never happen
        throw new RuntimeException("Could not get package name: " + e);
    }
  }

レジストレーションIDの取得

GCMからレジストレーションIDを取得した後、アプリのサーバサイドへレジストレーションIDを送信し、サーバサイドで記録しておきます。サーバサイドはこのレジストレーションIDとAPIキーを利用して、メッセージ送信します。

  private void registerInBackground() {
    new AsyncTask<Void, Void, String>() {
      @Override
      protected String doInBackground(Void... params) {
        String msg = "";
        try {
          if (gcm == null) {
              // gcmはインスタンス変数として保持し、毎回生成しないようにする。
              gcm = GoogleCloudMessaging.getInstance(MainActivity.this);
          }
          regid = gcm.register(SENDER_ID);
          msg = "Device registered, registration ID=" + regid;

          sendRegistrationIdToBackend(); 
          // このメソッドは任意のものでアプリケーションのサーバへレジストレーションIDを送信します。
          // regidを渡していないのはインスタンス変数だからですが、パラメータとして渡しもよいでしょう。
          // 必要ならレジストレーションID以外に、ユーザーを特定するようなキーを同時に送信します。

          // 取得したレジストレーションIDをプリファレンスへ保存し、毎回取得しないようにします。
          storeRegistrationId(MainActivity.this, regid);
        } catch (IOException ex) {
          msg = "Error :" + ex.getMessage();
        }
        return msg;
      }

      @Override
      protected void onPostExecute(String msg) {
          Log.i(TAG, msg + "\n");
      }
    }.execute(null, null, null);
  }

レジストレーションIDが取得できても、sendRegistrationIdToBackendメソッドでサーバサイドへの送信が失敗する場合があるでしょう。
その場合はリトライするか、ユーザーに再登録を促す必要があるかもしれません。

リトライする場合の時間間隔はexponential back-offと呼ばれる方法で行う方がよいでしょう。リトライするごとに時間間隔をx2していく方法です。
以前のAPIでは、サーバサイドへの送信が成功したことを通知したり、失敗したときは、登録解除の処理が必要だったのですが、新しいAPIでは必要なくなったようです。この辺りは簡潔になっているようです。

レジストレーションIDの保存

取得したレジストレーションIDは、アプリのバージョン番号とともにプリファレンスへ保存します。

  private void storeRegistrationId(Context context, String regId) {
      final SharedPreferences prefs = getGCMPreferences(context);
      int appVersion = getAppVersion(context);
      Log.i(TAG, "Saving regId on app version " + appVersion);
      SharedPreferences.Editor editor = prefs.edit();
      editor.putString(PROPERTY_REG_ID, regId);
      editor.putInt(PROPERTY_APP_VERSION, appVersion);
      editor.commit();
  }

本体処理の実装はこれで完了です。次にレシーバーとサービスを実装します。

レシーバの実装

新しいAPIのレシーバはWakefulBroadcastReceiverを継承したものを実装する必要あるようです。

WakefulBroadcastReceiverは端末がスリープ時にも受信できるブロードキャストレシーバクラスです。
メッセージが到着するとonReceiveが呼出されるので、メッセージを表示したり、何かのアクションを実行するサービスを起動します。

ここでは、GcmIntentServiceを起動します。

public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
      // Explicitly specify that GcmIntentService will handle the intent.
      ComponentName comp = new ComponentName(context.getPackageName(),
              GcmIntentService.class.getName());
      // Start the service, keeping the device awake while it is launching.
      startWakefulService(context, (intent.setComponent(comp)));
      setResultCode(Activity.RESULT_OK);
  }
}

サービスの実装

IntentサービスのonHandleIntentで、メッセージ受信のステータスやメッセージの種別を調べて、メッセージを取り出します。
ここでは、受信したメッセージをノーティフィケーションします。
メッセージの処理が完了したら、最後にGcmBroadcastReceiver.completeWakefulIntentを呼び出しておきます。
onHandleIntentでのメッセージ処理のみ実装すればいいので、旧APIと比べて、非常にわかりやすくなりました。

public class GcmIntentService extends IntentService {
  
  static final private String TAG = GcmIntentService.class.getSimpleName();

  public GcmIntentService() {
    super(GcmIntentService.class.getSimpleName());
  }

  @Override
  protected void onHandleIntent(Intent intent) {

    Bundle extras = intent.getExtras();
    GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(this);

    // The getMessageType() intent parameter must be the intent you received
    // in your BroadcastReceiver.
    String messageType = gcm.getMessageType(intent);

    if (!extras.isEmpty()) {  // has effect of unparcelling Bundle
 
      if (GoogleCloudMessaging.
              MESSAGE_TYPE_SEND_ERROR.equals(messageType)) {
          sendNotification("Send error: " + extras.toString());
      } else if (GoogleCloudMessaging.
              MESSAGE_TYPE_DELETED.equals(messageType)) {
          sendNotification("Deleted messages on server: " +
                  extras.toString());
      // If it's a regular GCM message, do some work.
      } else if (GoogleCloudMessaging.
              MESSAGE_TYPE_MESSAGE.equals(messageType)) {
        // Post notification of received message.
        
        sendNotification("Received: " + extras.get("message"));
        Log.i(TAG, "Received: " + extras.toString());
      }
    }
    // Release the wake lock provided by the WakefulBroadcastReceiver.
    GcmBroadcastReceiver.completeWakefulIntent(intent);
  }

  // Put the message into a notification and post it.
  // This is just one simple example of what you might choose to do with
  // a GCM message.
  private void sendNotification(String msg) {
    int NOTIFICATION_ID = 1;

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

    PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0);
    NotificationCompat.Builder builder =
            new NotificationCompat.Builder(this)
    .setSmallIcon(R.drawable.ic_launcher)
    .setContentTitle("GCM Notification")
    .setStyle(new NotificationCompat.BigTextStyle()
    .bigText(msg))
    .setContentText(msg);

    builder.setContentIntent(contentIntent);
    notificationManager.notify(NOTIFICATION_ID, builder.build());
  }

}

これで一通り実装が完了しました。
以前のAPIより、簡単に実装できるようになっています。APIキーやSenderIDの取得、サーバサイドの処理は変わっていません。
今後、新たにGCMを実装する場合は、新しいAPIを利用すべきでしょう。

サンプルでは、アプリからCCSへ送信するサンプルプログラムもあります。見た限りエコーバックしてくれるのかと思いましたが、動作しませんでした。

try {
    Bundle data = new Bundle();
        data.putString("my_message", "Hello World");
        data.putString("my_action",
                "com.google.android.gcm.demo.app.ECHO_NOW");
        String id = Integer.toString(msgId.incrementAndGet());
        gcm.send(SENDER_ID + "@gcm.googleapis.com", id, data);
        msg = "Sent message";
} catch (IOException ex) {
    msg = "Error :" + ex.getMessage();
}

gcm.gooleapis.comは実在しているようですが、やはりサーバサイドでCCSサーバとの接続が必要なのかもしれません。XMPPを使ったメッセージ通信はまた機会があれば、試してみたいと思います。

Run one of the demo servers (Java or Python) provided in Implementing an XMPP-based App Server. 
Whichever demo server you choose, 
don't forget to edit its code before running it to supply your sender ID and API key.

ref.Google Cloud Messaging GCM CCS with XMPP

関連記事

  1. アンドロイドアプリからGoogle Cloud Messagingを使う方法(第1回)- 準備編 第1版
  2. アンドロイドアプリからGoogle Cloud Messagingを使う方法(第1回)- 準備編 第2版
  3. アンドロイドアプリからGoogle Cloud Messagingを使う方法(第2回)- クライアントアプリ編 第1版
  4. アンドロイドアプリからGoogle Cloud Messagingを使う方法(第2回)- クライアントアプリ編 第2版
  5. アンドロイドアプリからGoogle Cloud Messagingを使う方法(第3回)- サーバーアプリ編