Amazon Pinpointをモバイルアプリに導入して、とりあえずプッシュ通知が送れるようになるまで：Android編

## はじめに
Amazon Pinpointを導入してみた際の忘備録です。

### 本記事のスコープ
`AWS Mobile SDK for Android` を導入して、AndroidNative側でどのように設定をすればプッシュ通知を受け取れるのかまでをスコープとします。
AWSの設定に関しては、[担当をしてくれた弊社エンジニアの記事](https://qiita.com/keinuma/items/9e33eb1690d2a298c43b#_reference-df7dc632cb59b730c6c0) を参照してください。

### サンプルレポジトリ
https://github.com/ginyolith/aws-pinpoint-sample-android

各種設定ファイルは各自取得・配置してください。
設定ファイルに関しては後ほど触れます。

### Amazon Pinpointとは
ざっくりと紹介すると
- プッシュ通知等が簡単に送れる
- セグメントを定義して、対象のセグメントに対してのみプッシュ通知を送るなども可能
- 各種アナリティクス（イベント送信・DAUなど）

以下、公式開発者ガイドより引用。
> Amazon Pinpoint は、複数のメッセージングチャネル間で顧客とやり取りするために使用できる AWS サービスです。キャンペーンの> 目的に応じて、プッシュ通知、E メール、またはテキストメッセージ (SMS) を送信できます。

## 用意する事・ファイル
- Firebase Cloud Messagingを導入
    - [公式ドキュメント](https://firebase.google.com/docs/cloud-messaging/?hl=ja) など参照。
- 設定ファイルである `awsconfiguration.json` を用意して、 `root/app/src/main/res/raw` 配下に配置
    - `awsconfiguration.json` を生成する方法は [サーバー側の担当をしてくれた弊社エンジニアの記事](TODO) を参照

## Pinpoint導入
だいたい、[公式のガイド](https://aws-amplify.github.io/docs/android/push-notifications)に沿って設定しています。

### 1. AWS Mobile SDK　を導入する
build.gradleを編集

```groovy:build.gradle

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'

    implementation 'com.google.firebase:firebase-core:16.0.1'
    implementation 'com.google.firebase:firebase-messaging:17.3.0'
    implementation 'com.google.android.gms:play-services-auth:15.0.1'
    
    // 以下がAmazon Pinpointに必要な依存関係
    implementation 'com.amazonaws:aws-android-sdk-pinpoint:2.7.+'
    implementation('com.amazonaws:aws-android-sdk-mobile-client:2.7.+@aar') { transitive = true }
}
```


### 2. FCM　のプッシュ通知を受け付けるサービスを設定

```xml:AndroidManifest.xml
<service
     android:name=".PushListenerService">
     <intent-filter>
         <action android:name="com.google.firebase.MESSAGING_EVENT"/>
     </intent-filter>
 </service>
```

```kotlin:PushListenerService.kt
import android.content.Intent
import android.os.Bundle
import android.support.v4.content.LocalBroadcastManager
import android.util.Log

import com.amazonaws.mobileconnectors.pinpoint.targeting.notification.NotificationClient
import com.amazonaws.mobileconnectors.pinpoint.targeting.notification.NotificationDetails
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import jp.ginyolith.sandbox.MainActivity

import java.util.HashMap

class PushListenerService : FirebaseMessagingService() {


    override fun onNewToken(token: String?) {
        super.onNewToken(token)

        Log.d(TAG,"Registering push notifications token: " + token!!)
        MainActivity.getPinpointManager(applicationContext)?.notificationClient?.registerDeviceToken(token)
    }

    override fun onMessageReceived(remoteMessage: RemoteMessage?) {
        super.onMessageReceived(remoteMessage)
        Log.d(TAG,"Message: " + remoteMessage?.data)

        // プッシュ通知の内容を取得する
        val notificationClient = MainActivity.getPinpointManager(applicationContext)?.notificationClient

        val notificationDetails = NotificationDetails.builder()
                .from(remoteMessage?.from)
                .mapData(remoteMessage?.data)
                .intentAction(NotificationClient.FCM_INTENT_ACTION)
                .build()

        val pushResult = notificationClient?.handleCampaignPush(notificationDetails)

        if (NotificationClient.CampaignPushResult.NOT_HANDLED != pushResult) {
            /** アプリがフォアグラウンドの場合、 alertを表示する */
            if (NotificationClient.CampaignPushResult.APP_IN_FOREGROUND == pushResult) {
                /* Create a message that will display the raw data of the campaign push in a dialog. */
                val dataMap = HashMap(remoteMessage?.data)
                broadcast(remoteMessage?.from, dataMap)
            }
            return
        }
    }

    private fun broadcast(from: String?, dataMap: HashMap<String, String>) {
        val intent = Intent(ACTION_PUSH_NOTIFICATION)
        intent.putExtra(INTENT_SNS_NOTIFICATION_FROM, from)
        intent.putExtra(INTENT_SNS_NOTIFICATION_DATA, dataMap)
        LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
    }

    companion object {
        val TAG = PushListenerService.javaClass.simpleName

        // Intent action used in local broadcast
        val ACTION_PUSH_NOTIFICATION = "push-notification"

        // Intent keys
        val INTENT_SNS_NOTIFICATION_FROM = "from"
        val INTENT_SNS_NOTIFICATION_DATA = "data"

        /**
         * Helper method to extract push message from bundle.
         *
         * @param data bundle
         * @return message string from push notification
         */
        fun getMessage(data: Bundle): String {
            return (data.get("data") as HashMap<*, *>).toString()
        }
    }
}
```

### 3. Activity側でpinpoint等のSDKの初期化を行う

```kotlin:MainActivity.kt
import android.content.*
import android.os.Bundle
import android.support.v4.content.LocalBroadcastManager
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import com.amazonaws.auth.AWSCredentialsProvider
import com.amazonaws.mobile.client.AWSMobileClient
import com.amazonaws.mobile.config.AWSConfiguration
import com.amazonaws.mobileconnectors.pinpoint.PinpointConfiguration
import com.amazonaws.mobileconnectors.pinpoint.PinpointManager
import com.amazonaws.mobileconnectors.pinpoint.targeting.endpointProfile.EndpointProfileUser
import com.google.firebase.iid.FirebaseInstanceId
import java.util.*


class MainActivity : AppCompatActivity() {
    private var credentialsProvider: AWSCredentialsProvider? = null
    private var configuration: AWSConfiguration? = null

    private val notificationReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            Log.d(TAG, "Received notification from local broadcast. Display it in a dialog.")

            val bundle = intent.extras
            val message = PushListenerService.getMessage(bundle!!)

            AlertDialog.Builder(this@MainActivity)
                    .setTitle("Push notification")
                    .setMessage(message)
                    .setPositiveButton(android.R.string.ok, null)
                    .show()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // AWS Mobile Client インスタンスを初期化
        AWSMobileClient.getInstance().initialize(this) {
            // AWSCredentialsProvider及びAWSConfigurationオブジェクトへの参照を取得しておく
            credentialsProvider = AWSMobileClient.getInstance().credentialsProvider
            configuration = AWSMobileClient.getInstance().configuration
        }.execute()

        val config = PinpointConfiguration(
                this@MainActivity,
                AWSMobileClient.getInstance().credentialsProvider,
                AWSMobileClient.getInstance().configuration
        )

        // PinpointManager初期化。セッション開始
        pinpointManager = PinpointManager(config)
        pinpointManager?.sessionClient?.startSession()
        pinpointManager?.analyticsClient?.submitEvents()

        // カスタムエンドポイント設定
        val interestsList = Arrays.asList("science", "politics", "travel")
        pinpointManager?.targetingClient?.addAttribute("Interests", interestsList)
        pinpointManager?.targetingClient?.updateEndpointProfile()

        // ユーザーIDをエンドポイントに割り当てる
        val endpointProfile = pinpointManager?.targetingClient?.currentEndpoint()
        val user = EndpointProfileUser()
        user.userId = "UserIdValue"
        endpointProfile?.user = user
        Log.d("a", "endpointId = " + endpointProfile?.endpointId)
        pinpointManager?.targetingClient?.updateEndpointProfile(endpointProfile)


        // エンドポイントIDをコピー出来るように
        findViewById<Button>(R.id.copy_endpoint_id).setOnClickListener {
            (this.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).let {
                it.primaryClip = ClipData.newPlainText("", endpointProfile?.endpointId)
                Toast.makeText(this, "コピー：${endpointProfile?.endpointId}", Toast.LENGTH_SHORT).show()
            }
        }

        // エンドポイントIDを表示
        findViewById<TextView>(R.id.end_point_id).text = "endpointId:${endpointProfile?.endpointId}"
    }

    override fun onPause() {
        super.onPause()

        // Unregister notification receiver
        LocalBroadcastManager.getInstance(this).unregisterReceiver(notificationReceiver)
    }

    override fun onResume() {
        super.onResume()

        // Register notification receiver
        LocalBroadcastManager.getInstance(this).registerReceiver(notificationReceiver,
                IntentFilter(PushListenerService.ACTION_PUSH_NOTIFICATION))
    }

    /** Amazon Pinpoint イベント送信に関する設定*/
    fun setUpForSendEvents() {
        // Buttonを押した際の処理を定義
        findViewById<Button>(R.id.button_send_ev).setOnClickListener {
            val event = pinpointManager?.analyticsClient?.createEvent("clickEvButton")
                    ?.withAttribute("hoge","clicked")
                    ?.withAttribute("fuga","clicked")
                    ?.withMetric("metric", Math.random())


            // イベントを記録
            pinpointManager?.analyticsClient?.recordEvent(event)

            // 記録したイベントを一斉に送信
            pinpointManager?.analyticsClient?.submitEvents()
        }
    }

    companion object {
        val TAG = MainActivity.javaClass.simpleName

        private var pinpointManager: PinpointManager? = null

        fun getPinpointManager(context: Context): PinpointManager? {
            if (pinpointManager == null) {
                val pinpointConfig = PinpointConfiguration(
                        context,
                        AWSMobileClient.getInstance().credentialsProvider,
                        AWSMobileClient.getInstance().configuration)

                pinpointManager = PinpointManager(pinpointConfig)
                FirebaseInstanceId.getInstance().instanceId
                        .addOnCompleteListener { task ->
                            val token = task.result.token
                            Log.d(TAG, "Registering push notifications token: $token")
                            pinpointManager!!.notificationClient.registerDeviceToken(token)
                        }
            }
            return pinpointManager
        }
    }
}

```

### 4. プッシュ通知を送信する
#### 1. 生成されたエンドポイントを取得する
[サンプルレポジトリ](https://github.com/ginyolith/aws-pinpoint-sample-android)、もしくは上記サンプルコードをビルドしたアプリを起動すると、下記のようにエンドポイントIDが表示されてコピペが可能となります。

<img src="https://qiita-image-store.s3.amazonaws.com/0/185371/155f1431-8719-2c68-3688-fe37ee5a7d92.png" width=50%>

#### 2. ダッシュボードから `Test messaging` を選択

#### 3. `Push Notifications` を選択
<img width="1360" alt="スクリーンショット 2018-11-22 16.25.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/185371/4b2231a0-8d78-9ac6-07f9-0cc5853a2db0.png">

#### 4. `Destinations` -> `Endpoint IDs` を選択。テストアプリのエンドポイントIDを所得して入力する。
<img width="1618" alt="スクリーンショット 2018-11-22 16.25.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/185371/c0a917a2-e63d-3fb9-c1b9-658b38288368.png">

#### 5. `message` の `Title` , `Body` に適当な文字列を入れて送信
<img width="1384" alt="スクリーンショット 2018-11-22 16.25.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/185371/1a576161-434f-068d-1fc3-f74b7bfb56d6.png">

#### 6. 端末にNotificationが送信される
成功すれば、入力した文字列がダイアログ表示されるはずです。
Pinpointのプロジェクトを作成した直後だと、エンドポイントIDを指定してもメッセージが送信出来ない場合がありますが、その場合はある程度時間を置いてから送信をすると、成功するようになりました。
サンプルプロジェクトだと、アプリがフォワードにいる場合はalert、そうでない場合はnotificatonが表示されるようになっています。

### まとめ
AWS SDK及びPinpoint SDKの書き方に慣れていないので最初は少し違和感を感じましたが、割と簡単に導入出来たように感じます。プッシュ通知の仕組みを導入する際は、検討してみてはいかがでしょうか。


## 参考
- [Amazon Pinpoint 開発者ガイド](https://docs.aws.amazon.com/ja_jp/pinpoint/latest/developerguide/welcome.html)
- https://aws-amplify.github.io/docs/android/push-notifications