Android端末で、FCM経由でAWSSNSを受け取るまで

# はじめに
GoogleがFirebaseを吸収した結果、GCM(Google Cloud Messaging)がFCM(Firebase Cloud Massaging)に切り替わった。
そのため、諸々の設定の方法がGCM時代の手順から変わっている。
ここでは、ゼロからFCMの環境とAWS、FCM、Androidの設定や実装を書いていく。
なお、**Server側の実装に関しては記載してない**。

# 前提条件
任意のAndroidプロジェクトが作られている
**AWSのAccess KeyとSecret Keyが生成されていること**

# Firebaseの設定
プロジェクトを新規追加し、Androidアプリとの紐づけを行う
1. [Firebase Console](https://console.firebase.google.com/?hl=ja)にGoogleアカウントでログインする。ログインするアカウントにサービスが紐づけられるので、注意すること
2. 「新規プロジェクトを作成」ボタンを押す
3.  プロジェクト名を任意で入力。国/地域を「日本」に。
4.  プロジェクト作成ボタンを押す。しばらくすると画面が切り替わる
5.  「AndroidアプリにFirebaseを追加」を選択すると、ダイアログが表示されるので情報を入力していく
6.  Android package nameにSNSをアプリのパッケージ名を入力する
7.  アプリのニックネームはコンソールの表示用なので任意。デバッグ署名は空欄でOK
8.  REGISTER APPをクリック
9.  Download google-services.jsonをダウンロードして、ここに配置してね、と言われるので言われた通りに配置する
10. 終わったら続行ボタンを押す
11. Gradleの設定してね、と言われるので、言われた通りに設定する。その後、終了をクリック
12. 登録されたアプリの設定を開く(右上の赤丸のやつをクリックするとメニューが出てくる)
<img width="300" alt="スクリーンショット 2017-04-10 17.13.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/76726/6abe6a35-8ed5-8b00-58ab-d77f49725d9d.png">
13. クラウドメッセージングタブを選択する
14. **Legacy server key** をコピーする。AWS側の設定で使う。

# AWS側の設定
AWSSNSサービスとFCMを紐づける
1. AWSコンソールにサインインする
2. 検索窓に「SNS」と入力し、「Simple Notification Service」を選択する
<img width="766" alt="スクリーンショット 2017-04-10 16.52.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/76726/7d14b39f-e8eb-7afd-aa30-98cb65437271.png">
3. メニューから「Application」を選択
4. 画面が切り替わるので、「Create platform application」ボタンをクリック
5. Application Nameを入力する。任意
6. push Platform Notificationで**Google Cloud Messaging(GCM)を選択する**
7. API Keyにコピーした「**Legacy server key**」を貼り付け、Create platform applicationをクリックする
8. 登録されると一覧に情報が追加されるので「ARN」をクリックし、詳細を開く
9. 画面上部にある、「Application ARN」をコピーする

# Android Studio側の実装
## Gradleの設定
[公式の説明](https://firebase.google.com/docs/notifications/android/console-audience)が結構わかりやすいのでそちらを見てもOK。特にバージョン情報は最新のものを参照したほうが良い

設定はGradleに以下を追加(Fireabase設定手順でclasspath等の設定は終わっている前提)

```groovy:build.gradle
dependencies {
  // ...
  compile 'com.google.firebase:firebase-core:10.0.1'
  compile 'com.google.firebase:firebase-messaging:10.0.1'
  // Getting a "Could not find" error? Make sure you have
  // the latest Google Repository in the Android SDK manager
}
```

## メッセージを受け取る用のサービスを追加
先のGradleの設定の公式説明の続きに書いてあるのでそちらを参考したほうがわかりやすいかも。また、メッセージの細かな仕様も同一ページに書いてあるので一読推奨。

やることはAndroidManifestの追加とServiceの実装を追加

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

```java:MyFirebaseMessagingService.java
public class MyFirebaseMessagingService extends FirebaseMessagingService {

    private static final String TAG = "MyFirebaseMsgService";

    /**
     * Called when message is received.
     *
     * @param remoteMessage Object representing the message received from Firebase Cloud Messaging.
     */
    // [START receive_message]
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // TODO(developer): Handle FCM messages here.
        // Not getting messages here? See why this may be: https://goo.gl/39bRNJ
        Log.d(TAG, "From: " + remoteMessage.getFrom());

        // Check if message contains a data payload.
        if (remoteMessage.getData().size() > 0) {
            Log.d(TAG, "Message data payload: " + remoteMessage.getData());
        }
    }
}
```

## AWSにEndpointを登録するための実装を追加
これだけがAWSとFCMの合わせ技になる。
[AWS側の公式の説明](https://firebase.google.com/docs/notifications/android/console-device)と[FCM側の公式の説明](https://firebase.google.com/docs/notifications/android/console-device)を参照。

### AWS SDKを追加する
build.gradleに以下を追加

```groovy:build.gradle
dependencies {
  // ...
    compile 'com.amazonaws:aws-android-sdk-core:2.4.+'
    compile 'com.amazonaws:aws-android-sdk-sns:2.4.+'
}
```

### AndroidManifestにサービスを追加する
AndroidManifest.xmlに以下を追加

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

MyFirebaseInstanceIDServiceクラスを実装する

```java:MyFirebaseInstanceIDService.java
public class MyFirebaseInstanceIDService extends FirebaseInstanceIdService {
    /**
     * Called if InstanceID token is updated. This may occur if the security of
     * the previous token had been compromised. Note that this is called when the InstanceID token
     * is initially generated so this is where you would retrieve the token.
     */
    // [START refresh_token]
    @Override
    public void onTokenRefresh() {
        // Get updated InstanceID token.
        String refreshedToken = FirebaseInstanceId.getInstance().getToken();
        Log.d(TAG, "Refreshed token: " + refreshedToken);

        // If you want to send messages to this application instance or
        // manage this apps subscriptions on the server side, send the
        // Instance ID token to your app server.
        sendRegistrationToServer(refreshedToken);
    }
    // [END refresh_token]

    /**
     * Persist token to third-party servers.
     *
     * Modify this method to associate the user's FCM InstanceID token with any server-side account
     * maintained by your application.
     *
     * @param token The new token.
     */
    private void sendRegistrationToServer(String token) {
        // TODO: Implement this method to send token to your app server.
    }
}
```

上記のsendRegistrationToServer内で、AWS側にtoken情報を送る。ここはAWS側の実装を参考にすればOK。最終的には以下のようになる

```java:MyFirebaseInstanceIDService.java
public class NotificationInstanceIdServiceTemp extends FirebaseInstanceIdService {

    private static final String APPLICATION_ARN = "AWSSNSでコピーしたApplication ARN";
    private static final String ENDPOINT = "https://sns.ap-northeast-1.amazonaws.com";
    private static final String ACCESS_KEY = "AWSのアクセスキー";
    private static final String SECRET_KEY = "AWSのSecretKey";

    @Override
    public void onTokenRefresh() {
        super.onTokenRefresh();
        String token = FirebaseInstanceId.getInstance().getToken();
        sendRegistrationToServer(token);
    }

    private void sendRegistrationToServer(String token) {
        AmazonSNSClient client = new AmazonSNSClient(generateAWSCredentials());
        client.setEndpoint(ENDPOINT);
        //SharedPreferenceに保存したendpointArnが存在したらそちらから取得するようにしてもOK
        String endpointArn = createEndpointArn(token, client);
        HashMap<String, String> attr = new HashMap<>();
        attr.put("Token", token);
        attr.put("Enabled", "true");
        SetEndpointAttributesRequest req = new SetEndpointAttributesRequest().withEndpointArn(endpointArn).withAttributes(attr);
        client.setEndpointAttributes(req);
    }

    private String createEndpointArn(String token, AmazonSNSClient client) {
        String endpointArn;
        try {
            System.out.println("Creating platform endpoint with token " + token);
            CreatePlatformEndpointRequest cpeReq =
                    new CreatePlatformEndpointRequest()
                            .withPlatformApplicationArn(APPLICATION_ARN)
                            .withToken(token);
            CreatePlatformEndpointResult cpeRes = client
                    .createPlatformEndpoint(cpeReq);
            endpointArn = cpeRes.getEndpointArn();
        } catch (InvalidParameterException ipe) {
            String message = ipe.getErrorMessage();
            System.out.println("Exception message: " + message);
            Pattern p = Pattern
                    .compile(".*Endpoint (arn:aws:sns[^ ]+) already exists " +
                            "with the same token.*");
            Matcher m = p.matcher(message);
            if (m.matches()) {
                // The platform endpoint already exists for this token, but with
                // additional custom data that
                // createEndpoint doesn't want to overwrite. Just use the
                // existing platform endpoint.
                endpointArn = m.group(1);
            } else {
                // Rethrow the exception, the input is actually bad.
                throw ipe;
            }
        }
        storeEndpointArn(endpointArn);
        return endpointArn;
    }

    private AWSCredentials generateAWSCredentials() {
        return new AWSCredentials() {
            @Override
            public String getAWSAccessKeyId() {
                return ACCESS_KEY;
            }

            @Override
            public String getAWSSecretKey() {
                return SECRET_KEY;
            }
        };
    }

    private void storeEndpointArn(String endpointArn) {
        //SharedPreferenceにでもendpointArnを保存して、次回以降はcreateEndpointArnの処理を省略しても良い(公式はその方式になってる)
    }

    private String getEndPointArn() {
        //SharedPreferenceからendpointArnを取得して、次回以降はcreateEndpointArnの処理を省略しても良い(公式はその方式になってる)
    }
}
```

*APPLICATION_ARN*はAWSSNSの設定の過程でコピーしたやつを貼り付ける
*ACCESS_KEY*と*SECRET_KEY*はそれぞれAWSに登録されているものを設定
*END_POINT*はAPPLICATION_ARNのパラメータと合致するものを記載する。
application ARNのリージョンが「arn:aws:**sns:ap-northeast-1**〜」となっているなら[このページ](http://docs.aws.amazon.com/ja_jp/general/latest/gr/rande.html#sns_region)を参考に、該当するURLを記載する。日本のAWSSNSなら`https://sns.ap-northeast-1.amazonaws.com`になる。

# メッセージを送信/受信する
アプリを起動すると最初にEndpointArnの登録処理が行われる。正常に行われれば、AWSSNS側の「End points」にトークン情報が追加される。
End pointsの画面に行き方は、AWSにログイン→SNSサービスを開く→Applicationsメニューを選ぶ→Arnのリンクをクリック で開ける

メッセージを送信する場合は、Tokenの横のチェックボックスにチェックを入れると「Publish to Endpoint」ボタンが有効になるのでそれをクリックする。
送信メッセージ作成用の画面に切り替わるので任意のメッセージを入力して「Publish message」を押せばOK。正常に実装できていれば`MyFirebaseMessagingService.java`にメッセージが飛んでくる