Fire OS 用のメッセージングソリューション「Amazon Device Messaging (ADM)」の実装

# はじめに
Fire TV Stick や Amazon のタブレットは、FireOS という AndroidベースのOSが入っています（例：FireOS8 は Android11ベース）。
FireOS 用のアプリも Android 同様に AndroidStudio で開発することができます。Androidで動くアプリであればまず FireOS上でも動作しますが、**一部のGoogleのサービスがサポートされていません**（[サポート対象のAPI・サービス・機能](https://developer.amazon.com/ja/docs/app-submission/migrate-existing-app.html#APIsandServices)）。
なかでも、ユーザへのPush通知などに使われている「Firebase Cloud Messaging (FCM)」が使えないことが大きく、Amazonのサービスから代替を見つける必要があります。

ここでは、FireOS で使える Amazonのメッセージングソリューション「Amazon Device Messaging (ADM)」の実装について、公式以外の情報が世の中に少なく、実装を体験した身として概要をメモ書きがてら書き残したいと思います。

# 目次
- [メッセージフロー](#メッセージフロー)
- [ADM設定手順](#adm設定手順)
  - [前提条件](#前提条件)
  - [認証情報の取得](#認証情報の取得)
  - [アプリ側の設定](#アプリ側の設定)
  - [開発者サーバー側の設定](#開発者サーバー側の設定)
- [テスト](#テスト)
- [おわりに](#おわりに)

# メッセージフロー
ADM は、一般的な FCM と同じように、**サーバからクライアントアプリにメッセージを送信する**サービスです。
具体的には、以下の4つのコンポーネントからなります。

・開発者サーバー
・ADMサーバー
・ADMクライアント
・クライアントアプリ

メッセージフローとしては、開発者サーバーから渡したJSONが、ADMサーバ、ADMクライアントを経由し、アプリにAndroidのIntentオブジェクトにアタッチされたエクストラとして渡されます。
開発者が制御可能な部分は、開発者サーバ、クライアントアプリの二箇所です。
![Qiita1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652177/ba1d67bc-d192-b530-67ba-4df93d89cd10.png)

# ADM設定手順

### 前提条件
[Amazon Developer](https://developer.amazon.com/ja/) にアカウントを準備し、Amazon appstore にアプリが用意してあること。

### 認証情報の取得
Amazon Device Messaging（ADM）を使用するには、Amazonに対してアプリを一意に識別できることを示す必要があるため、必要な認証情報を用意する必要があります。
具体的な手順は[公式の認証情報の取得方法](https://developer.amazon.com/ja/docs/adm/obtain-credentials.html)に記載がありますが、簡単に説明すると以下のようになります。

1. Amazon Developer よりADMを使用するアプリを選択し、アプリに「**セキュリティプロファイル**」を作成する(ここに OAuth認証情報が含まれます)。
2. セキュリティプロファイル管理より、アプリに「**APIキー**」を追加します。
＊APIキーの追加には、MD5署名とSHA-256署名を取得する必要があるため注意
3. セキュリティプロファイル管理より、「**クライアント認証情報**」を取得します（クライアントID、クライアントシークレット）。

### アプリ側の設定
#### Android StudioプロジェクトにADMをセットアップ(ADM SDKの適用)
1. 以下より ADM SDKをダウンロードし、zipを展開する。
[Amazon Device Messaging（ADM）SDKのダウンロード](https://developer.amazon.com/ja/docs/adm/overview.html#download)
2. Android Studio のフォルダ構造をAndroidからProjectに変更し、app > libs にamazon-device-messaging-1.1.0.jarファイルを貼り付ける。
3. build.gradleファイル に以下を追記する。
   ```gradle
   dependencies {
       compileOnly files('libs/amazon-device-messaging-1.1.0.jar')
   }
   ```
[公式 Amazon Device Messaging（ADM）のセットアップ方法（クライアントアプリ側）](https://developer.amazon.com/ja/docs/adm/set-up.html)

#### アプリのマニフェストの更新
1. AndroidManifest.xmlファイルを、以下のように追加変更する。
   ```AndroidManifest.xml
   <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:amazon="http://schemas.amazon.com/apk/res/android"
      package="[パッケージ名]">
      
      <!-- このパーミッションにより、ADMメッセージがほかのアプリにインターセプトされる危険性が回避されます。 -->
      <permission
         android:name="[パッケージ名].permission.RECEIVE_ADM_MESSAGE"
         android:protectionLevel="signature" />
      <uses-permission android:name="[パッケージ名].permission.RECEIVE_ADM_MESSAGE" />
      
      <!-- このパーミッションにより、ADMからのプッシュ通知をアプリが受信できるようになります。 -->
      <uses-permission android:name="com.amazon.device.messaging.permission.RECEIVE" />
      
      <!-- メッセージ受信時にプロセッサがスリープ状態になるのを防ぐため、ADMはWAKE_LOCKを使用します。 -->
      <uses-permission android:name="android.permission.WAKE_LOCK" />
      ...
      ...
      <!-- <application ...> の中身は任意 -->
      <application>
         ...
         <!-- 明示的にADMを有効化。ADMを必須とするにはtrueとする -->
         <amazon:enable-feature
            android:name="com.amazon.device.messaging"
            android:required="true" />

         <!-- Android 8.0以降(Fire OS 7)に対応した記法 -->
         <service
            android:name="[JobService名]"
            android:permission="android.permission.BIND_JOB_SERVICE"
            android:exported="false" />

         <!-- 旧バージョンの記法 -->
         <service
            android:name="[サービス名]"
            android:exported="false" />

         <!-- ADMから送信されるREGISTRATIONインテントとRECEIVEインテントを処理するための、ブロードキャストレシーバーを宣言 -->
         <receiver
            android:name="[レシーバー名]"
            android:exported="true"
            android:permission="com.amazon.device.messaging.permission.SEND" >

            <intent-filter>
               <action android:name="com.amazon.device.messaging.intent.REGISTRATION" />
               <action android:name="com.amazon.device.messaging.intent.RECEIVE" />

               <!-- categoryタグ内のname値をアプリのパッケージ名に置き換えます。 -->
               <category android:name="[パッケージ名]" />
            </intent-filter>
         </receiver>
         ...
      </application>
      ...
      ...
   </manifest>
   ```
   
2. AndroidManifest.xmlファイルの更新後、ADMManifest.checkManifestAuthoredProperly()を呼び出して、変更が適切かどうかを確認する。

#### 登録IDの取得
特定デバイスへのアプリのインストールを一意に識別するもの（≒ Firebaseのデバイス登録トークン）。
登録IDは、アプリのインスタンスがADMサーバーから取得します。開発者サーバー、ADMサーバー、ADMクライアントで、アプリの適切なインスタンスにメッセージをルーティングするために使用されます。

jarファイル(ADM SDK)のADMクラスに`getRegistrationId()`というメソッドがあり、ADMの登録IDが取得できます(`com.amazon.device.messaging.ADM`)。
現在のアプリインスタンスの登録IDがない場合、`startRegister()`メソッドでアプリをADMサーバーに登録することができます。

例えば、アプリの起動時のプロセスで必ず以下のコードを実行することで、登録IDを持たない状態を防げます。
```kotlin
final ADM adm = new ADM(this);
if (adm.getRegistrationId() == null)
{
   // startRegister()は非同期です。アプリへの通知は、登録IDが利用できるようになると、
   // onRegistered()コールバックで行われます。
   adm.startRegister();
}
```
[公式 登録IDの取得](https://developer.amazon.com/ja/docs/app-porting/device-messaging-fit-implement.html#registration)

#### 登録とメッセージの処理の実装
Android 8.0以降の場合、以下の二つのクラスを拡張して、受信したADMのメッセージを処理する必要がある。
- `com.amazon.device.messaging.ADMMessageHandlerJobBase`
メッセージを処理するためのクラス
以下にコールバックメソッドを列挙する
  - fun onRegistered
  - fun onUnregistered
  - fun onRegistrationError
  - fun **onMessage**
- `com.amazon.device.messaging.ADMMessageReceiver`
　適切なメッセージ処理クラスにメッセージを転送するためのクラス

```SampleADMHandler.kt
class SampleADMHandler : ADMMessageHandlerJobBase() {

    override fun onRegistered(context: Context, newRegistrationId: String) {
         // ヘッダーのキーと値のペアを使用してHTTPで登録IDを
         // 開発者サーバーに送信する例は次のとおりです。
         URL url = new URL(YOUR_WEBSERVER_URL);
         HttpURLConnection con = (HttpURLConnection) url.openConnection();
         con.setDoInput(true);
         con.setUseCaches(false);
         con.setRequestMethod("POST");
         con.setRequestProperty("RegistrationId", newRegistrationId);
         con.getResponse();
    }

    override fun onUnregistered(context: Context, registrationId: String) {
    }

    override fun onRegistrationError(context: Context, errorId: String) {
    }

    override fun onMessage(context: Context, intent: Intent) {
        // インテントに含まれていたエクストラを取得します。
        val extras = intent.extras
        // メッセージの要素のキーを取得します。
        val key = extras?.getString("key") ?: ""

        // keyの内容によって処理を分岐したり、通知を作成したりする。
        // メッセージの受信でトリガーしたいアクションを記載する。
    }
}
```

```SampleADMReceiver.kt
class MyADMReceiver : ADMMessageReceiver() {
   public Receiver() {
      // これは下位互換性を維持するために必要です
      super(MyADMLegacyMessageHandler.class);
      // 使用できる場合は、新しいジョブベースを使用します
      if (ADMLatestAvailable) {
         registerJobServiceClass(MyADMMessageHandler.class, <JOB_ID>)
      }
   }
}
```
 
### 開発者サーバー側の設定
#### アクセストークンのリクエスト形式
アクセストークンは、ADMに対して開発者サーバーの識別情報を認証して、メッセージを送信できるようにするための一時的なメタデータです。
ADMを使用してメッセージを送信する場合、メッセージリクエストにアクセストークンが必要であることから、まずはアクセストークンをADMサーバーにリクエストします。

アクセストークンは、クライアント認証情報を提供することで取得可能です。
以下のPOSTリクエストを参考に実装します。
```
POST /auth/O2/token HTTP/1.1
Host: api.amazon.com
Content-Type: application/x-www-form-urlencoded;charset=UTF-8

grant_type=client_credentials&scope=messaging:push&client_id=(クライアントID)&client_secret=(クライアントシークレット)
```

#### アクセストークンのレスポンス形式
ステータスコード200のHTTPレスポンスメッセージは、以下のようになります。
```
X-Amzn-RequestId: <クライアントID>
Content-Type: application/json

{
  "access_token":"Atc|<クライアントアクセストークン>",
  "expires_in":3600,
  "scope":"messaging:push",
  "token_type":"Bearer"
}
```
リクエストに失敗した場合は、ADMから200以外のエラーステータスコードが返されます。メッセージには`reason`が含まれる場合があります。
[公式 アクセストークンのリクエスト方法](https://developer.amazon.com/ja/docs/adm/request-access-token.html)

#### メッセージの送信（リクエスト）
以下が揃ったら、実際にメッセージを作成できます。
・登録ID
・アクセストークン

開発サーバーより、例えば以下のようにPOSTリクエストを送ります。メッセージコンテンツは JSON で記述します。
（"key":"value" の値は、先述の`onMessage`で処理する際に参照、使用します）

```
POST /messaging/registrations/(送信先アプリインスタンスの登録ID)/messages HTTP/1.1
Host: api.amazon.com
Authorization: Bearer (アクセストークン)
Content-Type: application/json
X-Amzn-Type-Version: com.amazon.device.messaging.ADMMessage@1.0
Accept: application/json
X-Amzn-Accept-Type: com.amazon.device.messaging.ADMSendResult@1.0

{
    "data":{"key1":"value1","key2":"value2"},
    "consolidationKey":"Some Key"
}
```

#### メッセージの送信（レスポンス）
以下に、レスポンスの例を記述します。
（registrationID：アプリインスタンスの現在の登録ID）
```
HTTP/1.1 200
X-Amzn-Data-md5: <MD5チェックサム>
X-Amzn-RequestId: <16進数のリクエストID>
Content-Type: application/json
X-Amzn-Type-Version: com.amazon.device.messaging.ADMSendResult@1.0
Content-Length: 308

{"registrationID":"amzn1.adm-registration.v1.<16進数のクライアント認証情報>"}
```
リクエストに失敗した場合は、ADMから200以外のエラーステータスコードが返されます。メッセージには`reason`が含まれる場合があります。
[公式 メッセージの送信方法](https://developer.amazon.com/ja/docs/adm/send-message.html)

## テスト
開発者コンソールから、ADMのテストメッセージをアプリに送信することができます。
具体的な手順は[開発者コンソールでテストメッセージを送信する方法](https://developer.amazon.com/ja/docs/adm/send-message.html#test-adm-console)にありますが、簡単に説明すると以下のようになります。

1. 開発者コンソールにログインし、アプリ&サービス > デバイスメッセージング 画面に移動します。
2. 「メッセージのテスト送信」の各項目を埋めます。
・クライアントIDとクライアントシークレット
・登録ID
・有効期限
3. メッセージのタイプと内容を記入し、[テストメッセージを送信] をクリックして送信します。

# おわりに
この記事を読んでいる方は、突然 Amazon デバイスのアプリ開発、もしくはAndroidからの移植を行うことになったけれど、公式以外に情報が少なく困っているのではないでしょうか。
本記事で概要を掴んで、詳細情報は公式を確認しつつ、うまく実装できることを応援しています。

# 参考
Amazon Appstore 公式
- [Amazon Device Messaging（ADM）について](https://developer.amazon.com/ja/docs/adm/overview.html)
- [認証情報の取得方法](https://developer.amazon.com/ja/docs/adm/obtain-credentials.html)
- [Amazon Device Messaging（ADM）のセットアップ方法（クライアントアプリ側）](https://developer.amazon.com/ja/docs/adm/set-up.html)
- [デバイスメッセージングの移行について](https://developer.amazon.com/ja/docs/app-porting/device-messaging.html)
- [既存のアプリをAmazonアプリストアへ移植する方法](https://developer.amazon.com/ja/docs/app-submission/migrate-existing-app.html)
- [Amazon Developer](https://developer.amazon.com/ja/)