2013 年 5 月 9 日

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

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

[Google Cloud Messageing]

前回は、GCMを利用するためのGoogle API consoleからサーバーキーを生成するところを説明しました。GCMはクライアントのアプリ(以後、クライアントアプリとします)だけではなく、メッセージを送信するサーバーサイドのプログラム(以後、GCMサーバと区別するためサーバアプリとします)が必要です。

サーバアプリのプログラムはPHPを利用します。どちらから説明すればいいのかは、にわとりが先か卵が先かのような話になりますが、まずは、クライアントアプリの実装から説明します。GCMのサンプルとして、androids-sdkにgcm-demo-clientがありますので、このソースコードを解説する形で説明していきます。

【ご注意 14.11.26】

この記事の実装方法はDeprecatedされています。アンドロイドアプリからGoogle Cloud Messagingを使う方法(第2回)改訂版をご覧ください。

ヘルパーライブラリのインストール

SDK MangerからExtra/Google Cloud Messaging for Android Libraryをインストールします。

サンプルアプリのインポート

Eclipseのワークスペースにサンプルアプリをインポートします。今回はこのサンプルを元にクライアントアプリを作成します。このサンプルを改修して、一度動作させてみると理解が早いと思います。EclipseのFileメニューから、New -> Project… -> Android Project from existing codeを選択して、android-sdk/extras/google/gcm/samples/gcm-demo-clientをインポートします。

クライアントアプリを作成

特に特別な設定は必要ありませんが、Minumum Required SDKはAPI 8を、Target SDKはAPI 17とします。

GCMライブラリをインポート

GCMライブラリをインポートします。私は、android-sdk/extras/google/gcm/samples/gcm-demo-client/gcm.jarをインポートしました。

マニフェストに追記

マニフェストに以下のものを追加します。

<!-- インターネット接続 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Googleアカウントの取得 -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<!-- メッセージを受信したらスリープ解除 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- GCMからのメッセージ受信 -->
<permission android:name="jp.co.notice.android.gcm.permission.C2D_MESSAGE" android:protectionLevel="signature" />
<uses-permission android:name="jp.co.notice.android.gcm.permission.C2D_MESSAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

applicationタグ内にレシーバとサービスのタグを追加します。

<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" />
    <!-- レジストレーションIDの受信 -->
    <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
    <category android:name="jp.co.notice.android.gcm" />
  </intent-filter>
</receiver>
<!--
  メッセージ受信するサービス
  クラス名のデフォルトはGCMIntentServiceとします。
  変更する場合は、カスタムBroadcastReceiverを利用して名前を再定義します。
-->
<service android:name=".GCMIntentService" />

ソースコードの構成

サンプルと同様のソースコード構成とします。

CommonUtilities.java
ServerUtilities.java
GCMIntentService.java
MainActivity.java

CommonUtilitiesクラス

CommonUtilitiesクラスには、このサンプルで利用する定数や共通のメソッド(メッセージ表示のみ)があります。重要なのは、SERVER_URLとSENDER_IDです。SERVER_URLはサーバアプリのURLを指定します。こちらはまたサーバアプリの実装時に説明します。SENDER_IDは、プロジェクトを生成したときのProject Numberを指定します。

public class CommonUtilities {

  // 端末のレジストレーションキーを登録・解除するサーバアプリのURLを指定します。
  static final String SERVER_URL = "http://www.notice.co.jp/gcm";

  // 前回作成したプロジェクトのProject Numberを指定します。
  public static final String SENDER_ID = "445593860212";

  // 以下、割愛します。
}

ServerUtilitiesクラス

ServerUtilitiesクラスは、GCMから端末が登録されたり、解除されるタイミングで、サーバアプリにレジストレーションIDを送信するクラスです。登録の場合、先ほどのCommonUtilities.SERVER_URLに、”register.php”を付加したもの、解除の場合は、”unregister.php”を呼び出すようになっています。このサンプルでは、以下のようなURLとなります。

実際のアプリでは、ユーザーIDとともに、レジストレーションIDを渡すということが必要かもしれません。

http://www.notice.co.jp/gcm/register.php&regId=レジストレーションID // 登録
http://www.notice.co.jp/gcm/unregister.php&regId=レジストレーションID // 解除

クライアントアプリが起動すると、GCMに登録されていない端末の場合、GCMへの登録処理が実行されます。それと合わせてサーバアプリに登録通知されます。サーバアプリでは、端末のレジストレーションIDを利用して、指定の端末へメッセージを送信します。以下にregisterメソッドとunregisterメソッドに注釈をつけたものを掲載します。

  public static boolean register(final Context context, final String regId) {
    Log.i(TAG, "registering device (regId = " + regId + ")");

    // サーバアプリの登録URLを設定する。
    String serverUrl = SERVER_URL + "/register.php";

    // 端末のレジストレーションIDをパラメータとして付加する。
    Map<String, String> params = new HashMap<String, String>();
    params.put("regId", regId);

    // 空き時間間隔(exponential backoff:繰り返すごとに空き時間を大きくして、送信タイミングを分散する)を持って、リトライします。
    long backoff = BACKOFF_MILLI_SECONDS + random.nextInt(1000);
    for (int i = 1; i <= MAX_ATTEMPTS; i++) {
      Log.d(TAG, "Attempt #" + i + " to register");
      try {
        displayMessage(context, context.getString(R.string.server_registering, i, MAX_ATTEMPTS));

        // サーバアプリにポストします。
        post(serverUrl, params);

        // サーバアプリに登録したことをGCMに登録します。
        GCMRegistrar.setRegisteredOnServer(context, true);

        // 登録メッセージを表示します。
        String message = context.getString(R.string.server_registered);
        CommonUtilities.displayMessage(context, message);

        // 成功
        return true;

      } catch (IOException e) {
        Log.e(TAG, "Failed to register on attempt " + i, e);
        if (i == MAX_ATTEMPTS) {
            break;
        }
        try {
            Log.d(TAG, "Sleeping for " + backoff + " ms before retry");
            Thread.sleep(backoff);
        } catch (InterruptedException e1) {
            // 完了前に、アクティビティが終了
            Log.d(TAG, "Thread interrupted: abort remaining retries!");
            Thread.currentThread().interrupt();
            return false;
        }
        // 待ち時間を増加
        backoff *= 2;
      }
    }    
    // エラーメッセージを表示する。
    String message = context.getString(R.string.server_register_error, MAX_ATTEMPTS);
    CommonUtilities.displayMessage(context, message);
    return false;
  }

  public static void unregister(final Context context, final String regId) {
    Log.i(TAG, "unregistering device (regId = " + regId + ")");

    // 端末のレジストレーションIDをパラメータとして付加する。
    String serverUrl = SERVER_URL + "/unregister.php";

    // 端末のレジストレーションIDをパラメータとして付加する。
    Map<String, String> params = new HashMap<String, String>();
    params.put("regId", regId);

    try {

      // サーバアプリにポストします。
      post(serverUrl, params);

      // サーバアプリから解除したことをGCMに登録します。
      GCMRegistrar.setRegisteredOnServer(context, false);

      // 解除メッセージを表示します。
      String message = context.getString(R.string.server_unregistered);
      CommonUtilities.displayMessage(context, message);

    } catch (IOException e) {
       // この時点では、GCMからは解除できているが、サーバアプリからは解除されていないが、再度、解除を実行する必要はない。
       // サーバがメッセージ送信するとき、その端末は未登録エラーとなり、その端末の解除が実行される。
      String message = context.getString(R.string.server_unregister_error, e.getMessage());
      CommonUtilities.displayMessage(context, message);
    }
  }

GCMIntentServiceクラス

GCMIntentServiceクラスは、IntentServiceから派生したGCMBaseIntentServiceを継承しています。GCMからのIntentに対して応答するクラスです。このサービスはバックグランドでGCMから受信して、レジストレーションIDの登録・削除したり、メッセージを受信して、クライアントアプリとステータスバーに通知します。オーバーライドしないといけない主要なメソッドは次のものです。

protected void onRegistered(Context context, String registrationId)
protected void onUnregistered(Context context, String registrationId)
protected void onMessage(Context context, Intent intent)
public void onError(Context context, String errorId)

onRegisteredとonUnregisteredは、それぞれServerUtilitiesクラスのメソッドを使って、登録・解除を実行します。onMessageは今回実装するサーバからのメッセージを処理します。実際はGCMサーバからメッセージが配信されます。onErrorは端末の登録・解除でGCMでエラーが発生したときの処理を実行します。

メインアクティビティ

では、メインアクティビティの実装を見てみましょう。

ブロードキャストレシーバーの定義

GCMIntentServiceからのメッセージを受信するレシーバーです。アクティビティからメッセージを表示するときも、このレシーバーで受信します。

  private final BroadcastReceiver mHandleMessageReceiver = new BroadcastReceiver() {

    @Override
    public void onReceive(Context context, Intent intent) {
      String newMessage = intent.getExtras().getString(EXTRA_MESSAGE);
      Log.i(TAG, newMessage);
      mMessage.append(newMessage + "\n");
    }
    
  };

初期処理

onCreateでは以下のような処理を実行します。checkDeviceメソッドは、この端末がGCMを実行できる環境かどうかを調べます。checkManifiestメソッドは、このアプリのマニフェストがGCM実行の要件を満たしているか調べます。その後、端末のレジストレーションIDがGCMに登録されているか調べて、未登録なら、登録処理を実行します。そのときサーバアプリに未通知なら、サーバアプリに対して登録処理を呼び出します。

protected void initGCM() {

  // この端末がGCMを実行できる環境かどうかを調べる。
  GCMRegistrar.checkDevice(this);
  // マニフェストがGCM実行の要件を満たしているか調べる。
  GCMRegistrar.checkManifest(this);

  mMessage = (TextView) findViewById(R.id.message);

  // メッセージハンドラの登録
  registerReceiver(mHandleMessageReceiver, new IntentFilter(DISPLAY_MESSAGE_ACTION));

  // GCMに登録されているレジストレーションIDを取得する。
  final String regId = GCMRegistrar.getRegistrationId(this);

  if (regId.equals("")) {
    // レジストレーションIDがないので、GCMに登録する。
    GCMRegistrar.register(this, SENDER_ID);

  } else {
    // サーバアプリに登録済みか
    if (GCMRegistrar.isRegisteredOnServer(this)) {
      // 登録済みなら、その旨を表示する。
      mMessage.append(getString(R.string.already_registered) + "\n");
    } else {
      // 登録されていないなら、サーバアプリへ登録を試みる。
      final Context context = this;
      // GUIスレッドではHTTP通信できないので、非同期で実行する。
      mRegisterTask = new AsyncTask<Void, Void, Void>() {

        @Override
        protected Void doInBackground(Void... params) {
            // サーバアプリへ登録実行
            boolean registered = ServerUtilities.register(context, regId);
            if (!registered) {
                // 登録できなかったので、解除する。
                GCMRegistrar.unregister(context);
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            mRegisterTask = null;
        }
    };
    mRegisterTask.execute(null, null, null);
  }
}    

ライフサイクルメソッド

onDestoryで、GCMのリソースを廃棄します。

@Override
protected void onDestroy() {
  if (mRegisterTask != null) {
    mRegisterTask.cancel(true);
  }
  unregisterReceiver(mHandleMessageReceiver);
  GCMRegistrar.onDestroy(this);
  super.onDestroy();
}

クライアントアプリの実装が終わったところで、今回は終了です。次回はクライアントアプリを動作させながら、サーバアプリの実装を説明します。

関連記事

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