Androidの通知チャネルにオリジナル通知音を設定する

# 概要
- Android Oreoから、プッシュ通知を行うためには**通知チャネル(Notification Channnel)**の利用が必須になっています。
- 通知チャネルにアプリオリジナルの通知音(アプリ内音源)を設定するための実装方法を書きました。

## 通知チャネルとは
通知チャネルは、Android 8.0(Oreo)から追加された新機能です。

[通知チャネルを作成して管理する - Android Developers](https://developer.android.com/training/notify-user/channels?hl=ja)

プッシュ通知を種別ごとに複数のチャネルに割り振り、チャネルごとに、プッシュ通知におけるバイブレーションのオン・オフ、通知音の変更といった細かい設定を**OSレベル**で行える仕組みになっています。

例えば、LINEには以下のような通知チャネルが設けられています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189802/1144da91-85db-9d15-1f43-f2abd230dcb3.png" width="300px">
これにより、「通話やトークを受信したときの通知は受けたいけれども、グループに招待された際の通知は必要ない」といった場合にも、チャネルごとの設定を変更することで対応できます。

なお、一度作成したチャネルの設定については、アプリ側から変更できるのはチャネルの名前と説明のみで、通知音やバイブレーションなどの設定をアプリ側から任意に変更するには、対象の通知チャネルを削除したのちに再作成する必要があります。
(配信中のアプリにおいてこれを行うことは推奨されません。ただし、[設定値を参照](https://developer.android.com/training/notify-user/channels?hl=ja#ReadChannel)してユーザに対して設定の変更を促すといったことは可能です。)

# 通知チャネルにオリジナルの通知音を設定する

Android版LINEをお使いの方はご存知かと思いますが、LINEにはアプリオリジナルの通知音が複数存在し、デフォルトの設定ではその中の「シンプルベル」という名前の通知音が鳴動すると思います。

私が開発していたアプリにこれと同様の機能を実装する機会があったのですが、通知チャネルが必須となるAndroid O以降では少々複雑な実装になったため、それについて本記事にまとめました。

## 手順

### 1. 通知音の格納

`res/raw`の配下に、通知音として使用する`.wav`なり`.mp3`なりの音源を入れておきます。

### 2. 通知チャネルを再作成
アップデート後の起動で、[既存の通知チャネルを削除](https://developer.android.com/training/notify-user/channels?hl=ja#DeleteChannel)し、アプリオリジナルの音を設定した[通知チャネルを再作成](https://developer.android.com/training/notify-user/channels?hl=ja#CreateChannel)します。

### 3. 通知チャネルにアプリオリジナルのサウンドを設定する

通知チャネル作成と同時に`NotificationChannel#setSound`を用いて、通知チャネルにアプリオリジナルのサウンドを設定します。
通知チャネルに音をセットするには、

```kotlin

protected open fun create(): NotificationChannel = create(NotificationManager.IMPORTANCE_HIGH).apply {

        ~

        setSound(getOriginalSoundUri(R.original_sound), audioAttributes)
}

```

のようにして、`NotificationChannel.setSound`の引数に、rawディレクトリに入れた音のURIと、`audioAttributes`の2つを指定します。URIは以下のようにして取得しています。

```kotlin

fun getOriginalSoundUri(id: Int): String {
        val sb = StringBuilder()
        sb.append(ContentResolver.SCHEME_ANDROID_RESOURCE)
        sb.append(File.pathSeparator + File.separator + File.separator)
        sb.append(context.packageName)
        sb.append(File.separator)
        sb.append(context.resources.getResourceTypeName(id))
        sb.append(File.separator)
        sb.append(context.resources.getResourceEntryName(id))
        return sb.toString()
}

```

ここでセットされた音をアプリの通知設定の画面から見ると、[アプリの通知音]という名前で設定されています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189802/77283f6f-8704-adf7-875e-07fd6de4ac8c.jpeg" width="300px">


### 4. オリジナル通知音を端末にコピー

ユーザが[アプリの通知音]を一旦変えてしまうと、アプリ側から再び[アプリの通知音]に戻すことは(通知チャネルを再作成しない限りは)できません。
ユーザ自身がオリジナルの通知音に設定し直せるようにするためには、
`app/res/raw`
ディレクトリの音を、
`Android/Notifications`または`Android/media/[パッケージ名]/Notifications`
に保存して、[アプリの通知設定]から参照できるようにする必要があります。
`Android/Notifications`
では、`READ_EXTERNAL_STORAGE`のパーミッションが必要なうえ、アプリをアンインストールした後もファイルが残ってしまうことから、今回は
`Android/media/[パッケージ名]/Notifications`
に保存することにしました。

`app/res/raw`ディレクトリに格納してある音は
`context.resources.openRawResource(R.sound_name)`
で参照できます。

まず、音源を`Android/media/[パッケージ名]/Notifications`に保存するためのメソッド
`exportOriginalSounds()`
を定義します。

```kotlin

fun exportOriginalSounds() {
      try {
          val dir = File(context.externalMediaDirs[0], Environment.DIRECTORY_NOTIFICATIONS)
          if (!dir.exists()) {
              dir.mkdirs()
          }
          context.resources.openRawResource(R.original_sound).use { inputStream ->
             val name = "OriginalSound.wav"
             val path = File(dir.toString() + File.separator + name)
             FileOutputStream(path, false).use { outputStream ->
                 inputStream.copyTo(outputStream)
             }
             // MediaScannerConnection
             MediaScannerConnection.scanFile(
                     MangoContext.app,
                     arrayOf(path.toString()),
                     arrayOf(Constants.ORIGINAL_SOUND_MIME_TYPE)
             ) { scannedPath, scannedUri ->
                 val c: Class<*> = MediaScannerConnection::class.java
                 LogOut.d(c, "Scanned $scannedPath")
                 LogOut.d(c, "-> uri: $scannedUri")
             }
          }
      } catch (e: IOException) {
          LogOut.logException(e)
      }
}

```

これを通知チャネル作成時に実行します。以下に例を記載します。

```kotlin

protected open fun create(): NotificationChannel = create(NotificationManager.IMPORTANCE_HIGH).apply {

        ~

        setSound(DEFAULT_SOUND_URI, audioAttributes)
        exportOriginalSounds()
}

```

これでユーザが音の設定からオリジナルの音も選択できるようになります。

## ハマった点

`exportOriginalSounds()`の中盤で`MediaScannerConnection.scanFile()`を呼び出していますが、これはオリジナル通知音の音源をContentProviderに登録する必要があるためです。
詳しい説明は[Contextのリファレンス](https://developer.android.com/reference/android/content/Context.html?hl=ja#getExternalFilesDir(java.lang.String))に記載されています。

> If you supply a non-null type to this function, the returned file will be a path to a sub-directory of the given type. Though these files are not automatically scanned by the media scanner, you can explicitly add them to the media database with MediaScannerConnection.scanFile.

始めは`MediaScannerConnection.scanFile()`を呼び出していなかったばかりに、端末を再起動するまでは通知音が設定画面から選択できない、という現象が発生していました。


# まとめ

- `setSound`メソッドを用いることで、通知チャンネルにオリジナルの通知音を設定できる。
- オリジナルのリソースは、端末ストレージにコピーしないと再利用ができない。
- オリジナル通知音の音源は、恣意的にContentProviderに登録する必要がある。
- 公式ドキュメントは隅から隅まで漏らさず読むべし。