【Android】【Kotlin】【Ruby】Google Cloud Messaging(GCM)を受信する 1 (受信側)
はじめに
Android ver.6.0から追加された新機能の一つにDozeモードがあります。
これは1.画面Offの状態で 2.端末を移動させず 3.長時間操作をしない 場合に、WakelockやJobScheduler、Wi-Fiのスキャンをストップしたりすることで省電化を実現する機能です。
では、これを解除するにはどうするか?というと、端末を手に持って操作する、という以外に、Google Cloud Messaging(以下GCM)を送信する、という方法があります。
今回はこのGCMの送受信についてあれこれやってみたことをまとめます。
環境
受信側
- Android 6.0.1
- Android Studio 2.0
- Kotlin 1.0.1-2
API key
GCMの使用にはGoogleMapと同じくAPI keyが必要となります。
今回はMessageを受信するAndroidと、サーバーのものがそれぞれ必要です。
API Keyの取得自体はGoogle API Consoleから行うことができるのですが、
それだけだとgcm - android - google-services - Google Samples - GitHubでは「google_app_id」として使用される、SenderIDが取得できません。
そのため、AndroidのAPI Keyの登録後に以下のページの「GET A CONFIGURATION FILE」ボタンを押して、必要な情報を取得します。
https://developers.google.com/cloud-messaging/android/start
- 「App name」にGoogle API Consoleで登録したアプリ名、「Android package name」にAndroidプロジェクトのパッケージ名を入力する。
- 「Choose and configure services」を押す。
- 「Select which Google services you'd like to add to your app below.」以下に「Cloud Messaging」があるので、これを有効にする。
準備
受信側のコードは以下を参考に作成します。
まずプロジェクト直下のbuild.gradleに以下を追記します。
build.gradle
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.0.0' classpath 'com.google.gms:google-services:2.0.0' } } ~省略~
app/build.gradleに、以下を追記します。
app/build.gradle
~省略~ dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.google.android.gms:play-services-gcm:8.4.0' testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:23.3.0' compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" } ~省略~
- Google Samplesのコードでは、最下部に「apply plugin: 'com.google.gms.google-services'」が追加されていますが、 これを入れると「google-services.json」が見つからないとエラーになるため(該当のファイルをプロジェクト直下に置いても)、外しています。
実装する機能
受信側で必要な機能は以下のとおりです。
1. RegistrationIntentService
- IntentServiceを使ってGCMの送信に必要なTokenを取得する。
- Activityで「startService(Intent)」を実行して開始する。
2. GcmReceiver
- Messageの受信に必要。
- WakefulBroadcastReceiverを継承しており、AndroidManifest.xmlに記述すれば自分でコードを書く必要はない。
3. GCM Listener Service
- サーバー側で送信されたMessageを受信する(「onMessageReceived(from: String, data: Bundle?)」が呼ばれる)。
- NotificationManagerを使って通知を作成・表示する。
4. InstanceIDListenerService
- TokenはGCMによって不定期で変更され、変更があった場合は「onTokenRefresh()」が呼ばれる。そのためRegistrationIntentServiceを呼び出してTokenを再取得する。
Permission
必要な権限は以下のとおり。位置情報のようにRequestPermissionで権限を取得する必要はありません。
AndroidManifest.xml
< uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" / > < uses-permission android:name="android.permission.WAKE_LOCK" / >
AndroidManifest
上記から、AndroidManifest.xmlは以下のような内容になります。
AndroidManifest.xml
< ?xml version="1.0" encoding="utf-8"? > < manifest xmlns:android="http://schemas.android.com/apk/res/android" package="jp.wakeupapplication" > < uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" / > < uses-permission android:name="android.permission.WAKE_LOCK" / > < application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme" > < activity android:name=".MainActivity" > < 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.gms.gcm.GcmReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND" > < intent-filter > < action android:name="com.google.android.c2dm.intent.RECEIVE" / > < category android:name="jp.wakeupapplication" / > < /intent-filter > < /receiver > < service android:name="jp.wakeupapplication.MessageListenerService" android:exported="false" > < intent-filter > < action android:name="com.google.android.c2dm.intent.RECEIVE" / > < /intent-filter > < /service > < service android:name="jp.wakeupapplication.RefreshTokenListenerService" android:exported="false" > < intent-filter > < action android:name="com.google.android.gms.iid.InstanceID"/ > < /intent-filter > < /service > < service android:name="jp.wakeupapplication.RegistrationIntentService" android:exported="false" > < /service > < /application > < /manifest >
- 「android:exported」をtrueにすることで、外部Activityから呼び出すことが可能になります。
実装
0. MainActivity
画面の表示及び各処理を呼び出すActivityを作成します。
MainActivity.kt
package jp.wakeupapplication import android.content.Intent import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.support.v7.app.AlertDialog import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) if (checkPlayServices()) { // IntentServiceを開始して、Tokenを取得する. val intent = Intent(this, RegistrationIntentService::class.java) startService(intent) } } /** * GooglePlayServices APKが使用可能かをチェック * */ private fun checkPlayServices(): Boolean { val apiAvailability = GoogleApiAvailability.getInstance() val resultCode = apiAvailability.isGooglePlayServicesAvailable(this) if (resultCode != ConnectionResult.SUCCESS) { if (apiAvailability.isUserResolvableError(resultCode)) { // ユーザーの操作によってエラーが発生した場合はDialogでエラーコード表示. apiAvailability.getErrorDialog(this, resultCode, 9000).show() } else { // その他GooglePlayServices APKが使用不可能ならDialog表示. val alert = AlertDialog.Builder(this) alert.setTitle("Not Supported") alert.setMessage("This device is not supported.") alert.setPositiveButton(getString(android.R.string.ok), null) alert.show() } return false } return true } }
- Google Play Servicesが使用可能かをチェックして、可能であればサーバーからMessageを送信するためのTokenを取得するRegistrationIntentServiceを開始しています。
1. RegistrationIntentService
RegistrationIntentService.kt
package jp.wakeupapplication import android.app.IntentService import android.content.Intent import android.util.Log import com.google.android.gms.gcm.GoogleCloudMessaging import com.google.android.gms.iid.InstanceID class RegistrationIntentService: IntentService("Registration Service") { override fun onHandleIntent(intent: Intent){ try { var instanceID = InstanceID.getInstance(this); var token: String = instanceID.getToken(getString(R.string.google_app_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null); // 取得したTokenをServer側に渡し、それを元にMessageを送信する. Log.i("WUA", "GCM Registration Token: " + token); } catch (e:Exception) { Log.d("WUA", "Failed to get token", e); } } }
- IntentServiceは非同期で実行することから、Thread名を渡してやる必要があります(内容は任意)。
- 「instanceID.getToken()」に、API keyで取得したAndroid側のKeyを渡してやることでTokenを取得できます。
- 今回サーバー側はWEBrickを使ったお手軽実装のためLogに出力したTokenの中身をコピペで渡していますが、本来はここでサーバー側にTokenを渡す必要があります。
3. GCM Listener Service
MessageListenerService.kt
package jp.wakeupapplication import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.media.RingtoneManager import android.os.Bundle import android.support.v7.app.NotificationCompat import android.util.Log import com.google.android.gms.gcm.GcmListenerService class MessageListenerService : GcmListenerService(){ override fun onMessageReceived(from: String, data: Bundle?) { // サーバーからMessageを受け取ったら実行されるので、Notificationを表示する. var message = data?.getString("message"); sendNotification(message) } private fun sendNotification(message: String?) { // 受け取ったメッセージからNotificationを作成. var intent = Intent(this, MainActivity::class.java) // どのActivityを開いていてもMainActivity上にNotificationが表示されるようにする. intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) var pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT) // Notification用の音のUri取得. var defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); var notificationBuilder = NotificationCompat.Builder(this) .setContentTitle("GCM Message") .setSmallIcon(R.mipmap.ic_launcher) .setContentText(message) .setAutoCancel(true) .setSound(defaultSoundUri) .setContentIntent(pendingIntent) var notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.notify(0, notificationBuilder.build()); } }
- 「onMessageReceived」の引数である「from」にはMessageを送信したサーバーのAPI Keyが渡されます。
- もう一つの引数である「data」は連想配列の形で(多分)サーバーから送った内容が含まれているため、必要な情報を抜き出してNotificationで表示しています。
- NotificationBuilderでIconに適当な画像をセットしていますが、何もセットしていないとエラーでクラッシュします。
4. InstanceIDListenerService
RefreshTokenListenerService.kt
package jp.wakeupapplication import android.content.Intent import com.google.android.gms.iid.InstanceIDListenerService class RefreshTokenListenerService : InstanceIDListenerService(){ // TokenはGCMによって不定期で変更されるため、変更があればTokenを再取得する. override fun onTokenRefresh() { var intent = Intent(this, RegistrationIntentService::class.java) startService(intent); } }
- TokenがGCMによって更新された時に呼ばれるので、MainActivityと同じようにRegistrationIntentServiceを開始して、Tokenを取得します。
長くなってきたので、サーバー側は次回。
参考
Google Cloud Messaging
- gcm - android - google-services - Google Samples - GitHub
- GCM 2.0から3.0に移行した話(Androidとサーバー) - Qiita
- Google Play Servicesを使ってアプリにGCMを導入する - mokelab tech sheets
- 【android】プッシュ通知を実装する 2016年2月時点 - なおしむ論
- アンドロイドアプリからGoogle Cloud Messagingを使う方法(第2回)第2版 - 大阪のアンドロイド/iOS・ Webアプリ開発会社 ノーティス
- Amazon SNSを使ってGCM経由でプッシュ通知 - Qiita
- ruby から gcm を使って android 端末へメッセージを送信する - Qiita