vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【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の送受信についてあれこれやってみたことをまとめます。

環境

受信側

API key

GCMの使用にはGoogleMapと同じくAPI keyが必要となります。
今回はMessageを受信するAndroidと、サーバーのものがそれぞれ必要です。

API Keyの取得自体はGoogle API Consoleから行うことができるのですが、
それだけだとgcm - android - google-services - Google Samples - GitHubでは「google_app_id」として使用される、SenderIDが取得できません。

そのため、AndroidAPI Keyの登録後に以下のページの「GET A CONFIGURATION FILE」ボタンを押して、必要な情報を取得します。

https://developers.google.com/cloud-messaging/android/start

  1. 「App name」にGoogle API Consoleで登録したアプリ名、「Android package name」にAndroidプロジェクトのパッケージ名を入力する。
  2. 「Choose and configure services」を押す。
  3. 「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

LocalBroadcastManager