【Android O】通知チャンネルを国際化する

# この記事は？
Android O では通知機能に[通知チャンネル](https://developer.android.com/about/versions/oreo/android-8.0.html?hl=en#notifications)が導入されました。

この記事では通知チャンネルを国際化する方法について説明します。

# サンプルコード
サンプルコードは次のリポジトリに置いてあります。

https://github.com/hshiozawa/NotificationChannelTester

主に参照するコードは次のディレクトリ配下に置いてあります。

https://github.com/hshiozawa/NotificationChannelTester/tree/master/app/src/main/java/com/hjm/notificationchanneltester

# 準備
まず、準備として通知チャンネルを作成するコードについて解説します。

（すでに通知チャンネルを実装している場合、この項目は飛ばしてもかまいません。）

通知チャンネルは NotificationChannel クラスと NotificationManager の createNotificationChannel() を使って作成・登録します。

今回は作成する処理をまとめたメソッドを次のように定義しました。

```java
public class NotificationChannelManager {

    @RequiresApi(Build.VERSION_CODES.O) // -- (1)
    public static void create(Context context, String channelId, @StringRes int titleResId, @StringRes int descriptionResId) {
        // -- (2)
        String title = context.getString(titleResId);
        String description = context.getString(descriptionResId);

        NotificationChannel channel = new NotificationChannel(channelId, title, NotificationManager.IMPORTANCE_DEFAULT);
        channel.setDescription(description);

        NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.createNotificationChannel(channel);
    }

}
```

詳細を解説します。

## (1): `@RequiresApi(Build.VERSION_CODES.O)`
NotificationChannel クラス自体や createNotificationChannel() は API Level 26 で導入されました。
そのため、定義したメソッドを Android O 未満の端末で実行すると、NotificationChannel のコンストラクタを呼び出す時にクラスが見つからないため java.lang.NoClassDefFoundError でクラッシュしてしまいます。

これを回避するために、このメソッドが Android O 以上で呼び出されることを期待していることを `@RequiresApi` を使って宣言します。同時に、Android O 未満でこのメソッドが呼び出されないようにするため、呼び出し側では Build.VERSION.SDK_INT をチェックします（後述）。

`@RequiresApi` はあくまでコンパイル時の警告のためだけに利用されるため、`@RequiresApi` だけではクラッシュを防げないので注意が必要です。

## (2): 通知チャンネルの作成
(2) の部分では、通知チャンネルのタイトルとディスクリプションの実際の文字列を取得します。
その後、NotificaitonChanne インスタンスを作成して登録します。

いくつか設定項目がありますが、必ず設定しなければならないのは、

- ID
- タイトル
- デフォルト通知レベル

です。

ディスクリプション自体は任意設定項目ですが、今回は国際化の結果を分かりやすくするために設定しています。

## (3): 通知チャンネルの登録
次のようにしてこのメソッドを Activity の onCreate() で呼び出します。

```java:MainActivity
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannelManager.create(
                getApplicationContext(), 
                "ChannelId_1",
                R.string.channelTitle,
                R.string.channelDescription);
        }

        ....
    }
}
```

（もしかしたら Application.onCreate() で呼んだ方がいいのかもしれません）

これで通知チャンネルの登録はおわりです。

## 通知チャンネルの確認
ここまでの実装でアプリを起動すると通知チャンネルが作成されます。

設定アプリの［通知］から該当のアプリを開くと、通知チャネルが設定されていることが確認できます。
通知チャンネルはユーザーには「カテゴリ」として表示されます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/41604/5d530684-4d02-7d4d-022e-01db7e22eb32.png" width=300>

ここで「チャネル１」をタップすると、「チャンネル１」に対して詳細な通知設定ができます。
この画面の下部には設定したディスクリプションが表示されます。


<img src="https://qiita-image-store.s3.amazonaws.com/0/41604/60d83599-fc0a-a246-d578-d852338bcc0b.png" width=300>


# ロケール変更時に発生する問題
英語の strings.xml を用意して、アプリを英語に対応させます。

```xml:strings.xml(en)
<resources>
  <string name="channelTitle">Channel 1</string>
  <string name="channelDescription">This is channel 1.</string>
</resources>
```

この後、言語を「英語」に切り替えます。
しかし、［通知］を開いても通知チャンネル名とディスクリプションが日本語のままです。

<img src="https://qiita-image-store.s3.amazonaws.com/0/41604/36bb3c12-d4ce-a970-0499-2105a8d1be5c.png" width=300>

<img src="https://qiita-image-store.s3.amazonaws.com/0/41604/3e70abde-331d-4b54-a34d-f624c4dcb93b.png" width=300>

これはアプリからチャンネル名などを登録する際に、ローカライズされた文字列を直接送っているからです。

そのため、Android システムとしてはローカライズされた後の通知チャンネル名とディスクリプションしか分かりません。

# 通知チャンネルの国際化
本題の通知チャンネルの国際化方法について述べます。

手順としては、

1. ACTION_LOCALE_CHANGED を受信する
2. 受信後、再度 API を呼び出してチャンネル情報を更新する

これはドキュメントに書いてあるので公式な方法だと思います。

> You can rename this channel when the system locale changes by listening for the ACTION_LOCALE_CHANGED broadcast.

ref: https://developer.android.com/reference/android/app/NotificationChannel.html#NotificationChannel(java.lang.String,%20java.lang.CharSequence,%20int)

## ACTION_LOCALE_CHANGED の受信
システムの言語が変更されるとシステムから ACTION_LOCALE_CHANGED が発行されるのでそれを受信します。

まずは受信用の BroadcastReceiver を定義します。
実際の更新処理は TODO にしておきます。

```java
public class LocaleChangedReceiver extends BroadcastReceiver {
    private static final String TAG = LocaleChangedReceiver.class.getName();

    @Override
    public void onReceive(Context context, Intent intent) {
        if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) {
            Log.i(TAG, "ACTION_LOCALE_CHANGED is received");
            // TODO: 通知チャンネル名を正しいロケールで更新する
        }
    }
}
```

次に ACTION_LOCALE_CHANGED の受信に対して、LocaleChangedReceiver が呼び出されるように AndroidManifest.xml に宣言します。

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hjm.notificationchanneltester">

        ...

        <receiver
            android:name=".LocaleChangedReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.LOCALE_CHANGED" />
            </intent-filter>
        </receiver>

        ...

    </application>
```

compileSdkVersion>=26 からは基本的に AndroidManifest.xml によるインテント受信はできなくなりましたが、
この ACTION_LOCALE_CHANGED はこの制限から除外されています。そのため、AndroidManifest.xml で宣言しても問題ありません。
ref: [Implicit Broadcast Exceptions | Android Developers](https://developer.android.com/guide/components/broadcast-exceptions.html)

## Notification Channel の更新
ACTION_LOCALE_CHANGED を受信したら、チャンネル名とディスクリプションを最新のロケール文字列で更新します。

notificationManager.createNotificationChannel() は、同じ ID に対して呼び出すとチャンネル名やディスクリプションを更新することができます。

> This can also be used to restore a deleted channel and to update an existing channel's name, description, and/or importance. 

ref: https://developer.android.com/reference/android/app/NotificationManager.html#createNotificationChannel(android.app.NotificationChannel)

今回はすでに NotificationChannelManager.create() として処理をまとめているのでそのメソッドをそのまま呼び出せば大丈夫です。ロケールに応じて Context.getString() で値を取得し createNotificationChannel() を呼び出します。

コードは次の通りです。

```java
@Override
public void onReceive(Context context, Intent intent) {
    if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) {
        Log.i(TAG, "ACTION_LOCALE_CHANGED is received");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // 言語が切り替わったのでチャンネル名などを更新する
            NotificationChannelManager.create(
                getApplicationContext(), 
                "ChannelId_1",
                R.string.channelTitle,
                R.string.channelDescription);
        }
    }
}
```

## 動作確認
この実装を行ったあと、言語を変更すると正しく通知チャンネルが変更されていることが確認できます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/41604/3d9687d9-8ce7-4ffb-5f33-04636e81eb56.png" width=300>

<img src="https://qiita-image-store.s3.amazonaws.com/0/41604/c52e6d24-fd31-1f54-978b-d062182d5ff5.png" width=300>

言語の変更から ACTION_LOCALE_CHANGE が受信できるまでに 10 秒程度かかるため、実際に切り替わるためには少し時間がかかります。