Google Cloud Messagingでハマったこと


皆さんが最新の情報や面白い情報を投稿する中で、
Tipsで恐縮ですが、19日目の投稿をします。

GoogleCloudMessagingを導入するにあたって、gcm.jarを利用する際にハマったことと、その対策を共有したいと思います。
始めに必要なProject IDやjarの入手方法は[こちら](http://www.techdoctranslator.com/android/guide/google/gcm)を参考にしてください。
公式は[こちら(英語)](http://developer.android.com/google/gcm/index.html)です。


#僕がハマった点4つ
 1. Registration IDの再取得
 2. GCMBroadcastReceiverクラスは固定値を返す
 3. 単体テストの方法
 4. GCMRegistrar.setRegisteredOnServerには期限がある

###1. Registration IDの再取得
 > * Application update
 > * Backup and restore

上記の条件のときは、発行されるRegistration IDが変わる可能性があります。
そのため、バージョンを確認して、前回起動時と異なっていた場合、
`GCMRegiatrar.register(Context context, String senderId)`
を叩くというロジックを入れなければなりません。

 * バージョンアップしたら、gcm.jar内のプリファで保存しているRegistration IDは自動で消去されます。

```java:MainActivity.java
public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		int currentVersionCode = getCurrentVersionCode();
		/** バージョンが挙がった場合はregIdが異なる可能性がある。 */
		if (isNewVersion(currentVersionCode)) {
			/** regIdに変更があるかもしれないのでサーバの登録状態を解除する
			 * regIdも空になる */
			GCMRegistrar.setRegisteredOnServer(this, false);
		}
		setVersionCode(currentVersionCode);
		final String regId = GCMRegistrar.getRegistrationId(this);
		
		/** regIdが空の場合、GCMサーバに登録できていないので、
		 * GCMRegistrar.registerを叩く。 */
		((TextView)findViewById(R.id.gcm_text)).setText(regId);
		if (TextUtils.isEmpty(regId)) {
			/** 結果をGCMIntentService.javaで受け取る */
			GCMRegistrar.register(this, Const.SENDER_ID);
		}else {
			/** GCM登録が問題ないので、自社サーバ の登録期間を延長する*/
			GCMRegistrar.setRegisteredOnServer(this, true);
			GCMRegistrar.setRegisterOnServerLifespan(this, GCMRegistrar.DEFAULT_ON_SERVER_LIFESPAN_MS);
		}
	}

	
	@Override
	protected void onDestroy() {
		GCMRegistrar.onDestroy(this);
		super.onDestroy();
	}


	/**
	 * 現在のバージョンコードを取得*/
	private int getCurrentVersionCode(){
		PackageInfo packageInfo = null;
		try {
			packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_ACTIVITIES);
		} catch (NameNotFoundException e) {
			e.printStackTrace();
		}
		return packageInfo.versionCode;
	}
	
	
	/**
	 * 現在のバージョンコードをプリファに保存*/
	private void setVersionCode(int currentVersionCode){
		SharedPreferences pref = getSharedPreferences("gcmexample", MODE_PRIVATE);
		pref.edit().putInt("versionCode", currentVersionCode).commit();
	}
	
	
	/**
	 * 現在のバージョンコードとプリファに保存された値を比較*/
	private boolean isNewVersion(int currentVersionCode){
		SharedPreferences pref = getSharedPreferences("gcmexample", MODE_PRIVATE);
		int oldVersionCode = pref.getInt("versionCode", 0);
		if (currentVersionCode > oldVersionCode) {
			return true;
		}
		return false;
	}
	
	
}
```
 `GCMRegistrar.getRegistrationId(Context context)`の値が空のときは、
`GCMRegiatrar.register(Context context, String senderId)`
を呼び、Registration IDを発行しなければなりません。
 
##2. GCMBroadcastReceiverクラスは固定値を返す
`GCMRegiatrar.register(Context context, String senderId)`が呼ばれると、
`com.google.android.c2dm.intent.REGISTRATION`のactionが投げられるので、
`GCMBroadcastReceiver`を継承したクラスで受け取ります。
`GCMBroadcastReceiver`を継承したクラスでは、`GCMBaseIntentService`を起動させます。
その際、packageName直下の階層に`.GCMIntentService`
をくっ付けたクラスを起動しようとするので、異なる階層に`GCMBaseIntentService`を継承したクラスを作成していた場合、
Serviceが起動しません。

 >  ```java:GCMBroadcastReceiver.java
   protected String getGCMIntentServiceClassName(Context context) {
        return getDefaultIntentServiceClassName(context);
    }
   static final String getDefaultIntentServiceClassName(Context context) {
        String className = context.getPackageName() +
                DEFAULT_INTENT_SERVICE_CLASS_NAME;// = ".GCMIntentService";
        return className;
    }
```


そこで、`GCMBroadcastReceiver`クラスを次のように拡張します。

```java:GCMReceiver.java
public class GCMReceiver extends GCMBroadcastReceiver{

	@Override
	protected String getGCMIntentServiceClassName(Context context) {
		/**
		 * GCMBaseIntentServiceを継承しているクラス名を渡す*/
		return GCMIntentService.class.getName();
	}

}
```

こうすることで、Serviceを無事起動できます。

## 3. 単体テストの方法
Serviceのどのmethodが呼ばれるかは、`GCMBaseIntentService`がハンドリングしてくれます。
Pushを受け取ったときは、`GCMBaseIntentService`の`onMessge`が呼ばれます。
テストのために、adb で broadcastを投げても、そのままでは
`GCMBroadcastReceiver`が拾ってくれません。
テストの時には、次の一文をコメントアウトする必要があります。

`android:permission="com.google.android.c2dm.permission.SEND"`

```xml:AndroidManifest.xml
        <receiver
            android:name="com.sakebook.android.gcmexample.GCMReceiver" 
            <!--android:permission="com.google.android.c2dm.permission.SEND"  -->>
            <intent-filter>
                <!-- Receives the actual messages. -->
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <!-- Receives the registration id. -->
                <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
                <category android:name="com.sakebook.android.gcmexample" />
            </intent-filter>
        </receiver>
```
こうすることで、
`com.google.android.gsf`以外からのbroadcastも受け取るようになります。
今回の例だと、次のようなintentを投げます。
`adb shell am broadcast -a com.google.android.c2dm.intent.RECEIVE -n 
com.sakebook.android.gcmexample/com.sakebook.android.gcmexample.GCMReceiver
 --es "hoge" "huga"`


## 4. GCMRegistrar.setRegisteredOnServerには期限がある

`GCMRegistrar.setRegisteredOnServer(Context context, boolean flag)`	
は、自社サーバの登録の状態を設定できるmethodです。
このmethodをフラグとして、使う場合には注意が必要です。
一度trueに設定した場合でも、設定してから`DEFAULT_ON_SERVER_LIFESPAN_MS`(=7日間)がたった場合、このmethodはfalseを返すようになります。
永続的に自社サーバの登録状態の管理をしたい場合は、
`GCMRegistrar.setRegisterOnServerLifespan(Context context, long lifespan)`
で、lifespanを延長させてあげなければなりません。
もしくは、期間限定の利用を設定する際にも使えます。
GCMサーバの状態とは直接の関係はないので、ユーティリティとしての使い方をするのが正しい使い方だと思います。

--
以上です。
流れに沿って説明したので、GCMの使う際の参考になれば幸いです。

サンプルのソースコードは[GitHub](https://github.com/sakebook/android-google-gcm-GCMExample)にあるので、ご自由にどうぞ。と言っても、クライアント側だけなので実際に使うにはサーバ側の設定が必要ですのでお忘れなく。