Notificationのなんとなく使っているけど正確な意味がよく分からない部分を調べてみた

Notificationってなんとなく使ってはいるけど、正確な意味って実はよく分かっていないって部分があったりしませんか？
私自身、質問されたときに正しく答えられる自信が無かったので調べてみました。

# PendingIntent

NotificationにPendingIntentを付与して、タップされたときのアクションを指定することができます。

```kotlin
val intent = Intent(this, MainActivity::class.java).also {
    it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
    it.putExtra(EXTRA_NOTIFICATION, "Notification $num $count")
}
val pendingIntent = PendingIntent
    .getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
```

## requestCode

PendingIntent.getActivityの第二引数でInt値のrequestCodeを指定します。
startActivityForResultを使うなら必要なパラメータですが、startActivityでは不要なはずのパラメータです。
コードを追って行ってみましたが、プロセス間通信をまたいでいたり追い切れないところもあるため、確信を持って断言できると訳ではないですが、このrequestCodeで投げられるIntentが変化したりはしないようでした。
ただし、PendingIntentRecord.Keyに使用されており、requestCodeの異なるPendingIntentは別のPendingIntentであると判定されます。
これは後ほど説明するPendingIntentの同一性に関係しています。

## FLAG

PendingIntent.getActivityの第四引数であるflag、これもなかなか意味が分かりにくいパラメータですね。同一のPendingIntentがあった場合の挙動を指定します。PendingIntentが同一ってどういうこと？というところは別項目で説明します。

### FLAG_UPDATE_CURRENT

すでに同じものがある場合、その情報を更新します。
先に作ったPendingIntentのパラメータ等が最後に作成したPendingIntentと同じパラメータに変わった形で動作します。
通常はこのフラグを使用することになるかと思います。

### FLAG_CANCEL_CURRENT

すでに同じものがある場合、それをキャンセルしてから新しく作り直します。
先に作ったPendingIntentは機能しなくなり、最後に作ったPendingIntentのみが機能するようになります。

### FLAG_ONE_SHOT

同一のPendingIntentがすでにある場合は、先に作成されたPendingIntentが使われ、内容の更新は行われない。
また、同一のPendingIntentは1度しかIntentを送信しない。
複数のNotificationに同じPendingIntentが設定された場合、最初にタップされたもの以外は何も起こらないことになります。先にあるNotificationは削除するという実装だとしても、これを指定するメリットも無いと思うのでNotificationでこのフラグを使うことはなさそうです。

### FLAG_NO_CREATE

PendingIntentを作成しません、すでに同一のPendingIntentが作成されている場合は、同じものが返りますが、同じものがなければnullが返ります。すでに同じものがある場合にのみ動作するという使い方をするのでしょうが、ちょっと用途が思いつかないです。

### FLAG_IMMUTABLE
Android 6(SDK 23)で追加。他のフラグとの論理和で使用。
Android S以降ではFLAG_IMMUTABLEかFLAG_MUTABLEのどちらかを明示的に指定する必要があります。
PendingIntentを渡された側が、PendingIntentで指定されたIntentを投げるsendメソッドは、別のIntentを指定することで、PeindingIntentで指定されていないパラメータを追加して投げることができます。
FLAG_IMMUTABLEを指定することで、パラメータの上書きができないことを示します。

### FLAG_MUTABLE 
Android S(SDK 31)で追加。他のフラグとの論理和で使用。
Android S以降ではFLAG_IMMUTABLEかFLAG_MUTABLEのどちらかを明示的に指定する必要があります。
名前の通り、FLAG_IMMUTABLEとは逆にPendingIntentのsend時にパラメータの上書きが可能であることを示します。

## PendingIntentの同一性

FLAGの説明で同じPendingIntentがあれば～というのがありましたが、同じPendingIntentであるという判定はどのように行われているのでしょうか？実装を見てみましょう。
PendingIntentRecord.KeyというクラスがPendingIntentのKeyであり、このKeyが同一である場合に同じPendingIntentであると判定されているようです。

```java:PendingIntentRecord.java
@Override
public boolean equals(Object otherObj) {
    if (otherObj == null) {
        return false;
    }
    try {
        Key other = (Key)otherObj;
        if (type != other.type) {
            return false;
        }
        if (userId != other.userId){
            return false;
        }
        if (!Objects.equals(packageName, other.packageName)) {
            return false;
        }
        if (!Objects.equals(featureId, other.featureId)) {
            return false;
        }
        if (activity != other.activity) {
            return false;
        }
        if (!Objects.equals(who, other.who)) {
            return false;
        }
        if (requestCode != other.requestCode) {
            return false;
        }
        if (requestIntent != other.requestIntent) {
            if (requestIntent != null) {
                if (!requestIntent.filterEquals(other.requestIntent)) {
                    return false;
                }
            } else if (other.requestIntent != null) {
                return false;
            }
        }
        if (!Objects.equals(requestResolvedType, other.requestResolvedType)) {
            return false;
        }
        if (flags != other.flags) {
            return false;
        }
        return true;
    } catch (ClassCastException e) {
    }
    return false;
}
```

たくさんのパラメータで判定されていますが、通常のPendingIntentの作成過程で制御可能なのは、type、requestCode、intent、flagぐらいでしょうか。flagはPendingIntentのflagで、intentはそのままですね。
typeはgetActivityかgetServiceかgetBroadcastかみたいなIntentの投げ方の種別
requestCodeとintentとflagはgetActivityの引数ですね。

また、intentの同一性はfilterEqualsで判定されています、この中身も見てみましょう。

```java:Intent.java
public boolean filterEquals(Intent other) {
    if (other == null) {
        return false;
    }
    if (!Objects.equals(this.mAction, other.mAction)) return false;
    if (!Objects.equals(this.mData, other.mData)) return false;
    if (!Objects.equals(this.mType, other.mType)) return false;
    if (!Objects.equals(this.mIdentifier, other.mIdentifier)) return false;
    if (!(this.hasPackageEquivalentComponent() && other.hasPackageEquivalentComponent())
            && !Objects.equals(this.mPackage, other.mPackage)) {
        return false;
    }
    if (!Objects.equals(this.mComponent, other.mComponent)) return false;
    if (!Objects.equals(this.mCategories, other.mCategories)) return false;

    return true;
}
```

だいたい、Intentが持つパラメータのほとんどが比較に使われてますが、ExtrasやFlagが含まれてないですね。
先の、同じPendingIntentを上書きするしない、というのはExtrasやFlagを書き換えるかどうかと読み替えても良いでしょう。

逆にPendingIntentを作るさい、同じと判定されて上書きなどを防止したい場合は、requestCodeを異なるものにするか、IntentのExtraやFlag以外で差を作る。ことが必要になります。
上書きされると困るパラメータはdata(uri)のパスやクエリーなどとして表現するのが良いかもしれません。

# Notification

## priority

Notification作成時にpriorityを指定することができます。
Android 8以降では、通知の重要性はNotificationChannelのimportanceで指定するため、priorityにはあまり意味はありません。ただ、通知領域で複数の通知が表示される場合、priorityが高いものの方が上に表示されます。
Android 8未満では、以下のような違いがあるようです、これも環境依存かもしれません。

|優先度|結果|
|--|--|
|PRIORITY_MIN|soundもvibrateも指定が無い場合、通知アイコンも表示されない、soundかvibrateがあると通知アイコンは表示される|
|PRIORITY_LOW|通知アイコンが表示される|
|PRIORITY_DEFAULT|通知アイコンが表示される|
|PRIORITY_HIGH|soundかvibrateが指定されていると、通知時にHeads Up表示される|
|PRIORITY_MAX|soundかvibrateが指定されていると、通知時にHeads Up表示される|


## tag / id

Notificationを発行する際、TAGとIDを指定します、TAGは省略可能で、省略した場合はnullを指定した場合と同じになります。

```kotlin
notificationManager.notify(id, notification)
notificationManager.notify("tag", id, notification)
```

この二つの値のペアで、Notificationの同一性を判断します。
二つの値とも同じにして複数回投げると先に出したNotificationが上書きされます。
どちらかの値が異なっていれば別のNotificationとして両方が通知領域に表示されます。
NotificationChannelが異なっていても、これらの値が同一であれば同じ通知として扱われます。

# NotificationChannel

Android8以上ではNotificationを作る前にNotificationChannelを作成し、そのChannelを指定する必要があります。
単純に作るだけなら以下ですね

```kotlin
val notificationManager: NotificationManager = getSystemService()!!
if (VERSION.SDK_INT >= VERSION_CODES.O) {
    val channel = NotificationChannel(CHANNEL_ID, "channel", NotificationManager.IMPORTANCE_DEFAULT)
    notificationManager.createNotificationChannel(channel)
}
```

NotificationChannelの表示の仕方はOSバージョンや、メーカーのカスタマイズによって変わってきますが、以下のようになります。

||||
|:-:|:-:|:-:|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/6e4922ed-3998-c6c3-b6c8-41ad113f14fc.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/3fa93b02-b128-dcb7-9a97-030834436cc1.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/50e6eb8f-e3a7-37d9-bd93-8770737312d1.png)|

## Channel ID

NotificationChannelを作成するにはChannel IDを指定する必要があります。これはStringで任意の文字列を指定することができます。
このIDはNotificationChannelの識別に使用されるので、IDが違えば別のチャンネル扱いになります。任意の文字列なので、日本語とか空文字でも識別子として機能します。（それが管理上適切かどうかは置いておいて）

作成済みのChannelIDを指定してcreateNotificationChannelをコールするとChannel情報の更新となり、チャンネル名を変更することができます。

チャンネルを削除するときもこのIDを指定します。存在しないChannelIDを指定しても問題は起こらないので、削除する前にチャンネルが存在するかのチェックは特に不要なようです。

```kotlin
notificationManager.deleteNotificationChannel(CHANNEL_ID)
```

また、環境依存だとは思いますが、チャンネル一覧に出てくるチャンネル名はこのChannel IDの辞書順に表示されるようです。

```kotlin
notificationManager.createNotificationChannel(NotificationChannel("a", "channel1", NotificationManager.IMPORTANCE_DEFAULT))
notificationManager.createNotificationChannel(NotificationChannel("b", "channel2", NotificationManager.IMPORTANCE_DEFAULT))
```
Channel IDのa,bを入れ替えると表示される順序が変わります。

|"a" = "channel1"|"a" = "channel2"|
|:-:|:-:|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/ab702ac7-c80c-5c50-ba97-3b48dc8b979e.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/b6ac7e1d-7ead-5a1b-ce01-f55e77d207bb.png)|

## description

Notification Channelに説明を追加することができます。

```kotlin
val channel = NotificationChannel(CHANNEL_ID, "channel", NotificationManager.IMPORTANCE_DEFAULT)
channel.description = "description"
notificationManager.createNotificationChannel(channel)
```

どこに表示されるかは環境依存ですが、デフォルトでは一覧では表示されず、NotificationChannelの詳細の一番下にあまり目立たない形で表示されます。一覧に表示される環境もあるようです。

|descriptionなし|descriptionあり|
|:-:|:-:|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/cb2a6a5f-127c-50dd-25ae-06c073e28348.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/c00a91ac-178e-9eb4-772b-a09b6f4db34c.png)|

一度description付きで作ってしまった後に削除したい場合は、空文字を指定することで削除することができます。

```kotlin
val channel = NotificationChannel(CHANNEL_ID, "channel", NotificationManager.IMPORTANCE_DEFAULT)
channel.description = ""
notificationManager.createNotificationChannel(channel)
```

### importance

通知チャンネルの重要度を指定します。IMPORTANCE_DEFAULTが音が鳴る実質最大優先度になります。
ForegroundServiceの場合はIMPORTANCE_LOWのチャンネルを指定しましょう。

|importance|重要度|作成直後の設定|
|:-|:-|:-|
|IMPORTANCE_UNSPECIFIED|ユーザーが重要度を設定していないことを示す値、これを指定して作成するとIllegalArgumentExceptionが発生する||
|IMPORTANCE_NONE|重要性なし、無効状態で作成される|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/df469968-dae1-b256-cee7-94ea355c84c5.png)|
|IMPORTANCE_MIN|最小の重要度、サイレントかつ最小化状態で通知される|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/8f7420bd-0ab9-bbaa-b17b-a00cb620f88e.png)|
|IMPORTANCE_LOW|低優先度、サイレント通知|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/c48f11eb-11de-c865-1a3f-b7ec7f4f01ed.png)|
|IMPORTANCE_DEFAULT|デフォルト、通知音付きで通知される|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/3a9476b5-a142-56e6-bfb3-9ec027dad7bb.png)|
|IMPORTANCE_HIGH|未使用、指定してもデフォルトと同様の設定||
|IMPORTANCE_MAX|未使用、指定してもデフォルトと同様の設定（アノテーションの範囲外のため警告が出る）||

重要度は、createNotificationChannelを再実行することで、下げることはできますが、上げることはできません。

また、重要度とは別に、バイブレーションやLEDについて個別で指定することができます

```kotlin
channel.enableVibration(true)
channel.enableLights(true)
```

バイブレーションやLEDは初回作成時の初期値として使われ、それ以降は異なる値でcreateNotificationChannelしても、設定は更新されません。

## NotificationChannelGroup

NotificationChannelGroupを作成し、NotificationChannelにGroupIDを指定することで、NotificationChannelをグループ化することができます。

```kotlin
notificationManager.createNotificationChannelGroup(
    NotificationChannelGroup("group1", "group1")
)
notificationManager.createNotificationChannelGroup(
    NotificationChannelGroup("group2", "group2")
)
notificationManager.createNotificationChannel(
    NotificationChannel("channel1", "channel1", NotificationManager.IMPORTANCE_DEFAULT)
        .also { it.group = "group1" }
)
notificationManager.createNotificationChannel(
    NotificationChannel("channel2", "channel2", NotificationManager.IMPORTANCE_DEFAULT)
        .also { it.group = "group1" }
)
notificationManager.createNotificationChannel(
    NotificationChannel("channel3", "channel3", NotificationManager.IMPORTANCE_DEFAULT)
        .also { it.group = "group2" }
)
notificationManager.createNotificationChannel(
    NotificationChannel("channel4", "channel4", NotificationManager.IMPORTANCE_DEFAULT)
        .also { it.group = "group2" }
)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/8bc69576-257b-f54f-367f-d29f55206a83.png" width="400"/>

# まとめ

## PendingIntentのFLAG

基本、FLAG_UPDATE_CURRENTとFLAG_IMMUTABLEの論理和を指定しよう。他のフラグは用途が特殊なので、よく考えて。

## PendingIntentの同一性

同じPendingIntentは複数作られるのではなく、FLAGによって上書きなどが発生する。
同じPendingIntentであると判定されたくない場合は、requestCodeを異なるものにするか、IntentのExtraとFlag以外で違いを作る必要がある。EXTRAではなく、data（uri）のクエリーやパスでパラメータを表現するのが良いかも。

## Notificationの重要性

Android 8未満ではNotificationのpriorityで設定する。（通知時の音の有無などもNotificationで設定）
Android 8以上ではNotificationChannelのimportanceで設定する。（通知時の音の有無などもNotificationChannelで設定）
Android 8以上でもNotificationのpriorityは完全な無意味ではなく、表示順序などに現れる。

## NotificationChannel

Android 8以上ではNotificationChannelの作成が必要、名前や説明は作成後にも更新できるが、重要度などユーザー操作以外では更新されないものがある。
NotificationChannelGroupを作ることでNotificationChannelをまとめることができる。