【Kotlin研修11日目】通知機能の実装、通知によるアクティビティの起動

# 通知(Notification)
[参考1: 通知](https://developer.android.com/guide/topics/ui/notifiers/notifications?hl=ja)
[参考2: 通知チャネル](https://developer.android.com/training/notify-user/channels?hl=ja)
[参考3: 研修10日目](https://qiita.com/b150005/items/bc7054a520d4b858dc0f)
`ホーム画面`の`通知領域`に`通知アイコン`を表示したり、`通知ドロワー`に`通知メッセージ`を表示したりできる機能。

`通知`は`バックグラウンド処理`で実行されるため、`バックグラウンド処理`を行う`サービスクラス`(=`Service`クラスを継承するクラス)で管理する。

`通知情報`に関するデータは`NotificationCompat.Builder`を通じて`Notification`オブジェクトによって管理され、
`通知方法`に関するデータは`通知チャネル`(=`NotificationChannel`オブジェクト)に格納され、`通知チャネル`は`NotificationManager`によって管理される。

また、`通知`(=`Notification`オブジェクト)は`NotificationManagerCompat`オブジェクトによって実行される。

## 通知領域と通知アイコン

![通知領域と通知アイコン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/bd689fee-6ad6-81fc-ce5c-060d9272ee5f.png)

## 通知ドロワーと通知メッセージ

![通知ドロワーと通知メッセージ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/a88a28dd-0eea-0ccf-15b6-ddab78f6270e.png)

## NotificationCompat.Builder
`Notification`オブジェクトのファクトリクラスであり、
`NotificationCompat`クラス内で定義されたクラス(=`ネストクラス`)。

## Notification
`通知`の`アイコン`・`タイトル`・`メッセージ`など、`通知情報`に関するデータを格納するクラス。

## 通知チャネル(NotificationChannel)
`通知`の`重要度`・`通知音`・`バイブレーションパターン`など、`通知方法`に関するデータを、チャネル毎にまとめて管理できる機能。

## NotificationManager
`通知方法`に関する各種データを格納した`通知チャネル`(=`NotificationChannel`オブジェクト)と、
`通知情報`に関するデータを格納した`Notification`オブジェクトを管理するクラス。

## NotificationManagerCompat
`通知`を実行するメソッドを定義するクラス。

`NotificationManagerCompat`オブジェクトは、独自に再定義した`NotificationManager`オブジェクトを保持する`サービスクラス`で管理される。

***

# 通知の実装
[参考1: 通知](https://developer.android.com/guide/topics/ui/notifiers/notifications?hl=ja)
[参考2: 通知の作成](https://developer.android.com/training/notify-user/build-notification?hl=ja)
`通知`機能を実装する手順は、以下の通り。

> 1. `通知チャネル`(=`NotificationChannel`オブジェクト)の生成
> 2. `NotificationManager`オブジェクトの取得、`通知チャネル`のセット
> 3. `NotificationCompat.Builder`オブジェクトの生成、`通知情報`の定義
> 4. `Notification`オブジェクトの生成
> 5. `NotificationManagerCompat`オブジェクトを用いて通知を表示

## 通知チャネルの生成
`通知方法`に関するデータをまとめる`通知チャネル`は、`サービス`の初期化時に実行される`onCreate()`ブロック内で生成する。

なお、`サービスクラス`では、親である`Service`クラスで定義されたメソッドの`オーバーライド`時に、`親メソッド`を**呼び出す必要がない**。

また、`チャネルID`は`サービスクラス`で不変であるため、`クラス定数`として`companion object`ブロックで定義する。

### 定義

```kotlin
// 通知チャネルの生成
NotificationChannel(
    id: String!, 
    name: CharSequence!, 
    importance: Int
)
// パラメータ
// id: パッケージ内で一意の、通知チャネルの識別ID(String型)
// name: ユーザに表示される通知チャネル名
// importance: 通知チャネルの重要度
// -> NotificationManagerクラス定数を利用
```

#### 通知チャネルの重要度を表すNotificationManagerクラス定数

|重要度|定数名|内容|
|:--|:--|:--|
|高|`IMPORTANCE_HIGH`|通知音とポップアップ<br>値: `4`|
|↑|`IMPORTANCE_DEFAULT`|通知音のみ<br>値: `3`|
||`IMPORTANCE_LOW`|スリープ画面**最上部**への通知メッセージ<br>※通知アイコンあり<br>値: `2`|
|↓|`IMPORTANCE_MIN`|スリープ画面**下部**への通知メッセージ<br>※通知アイコンなし<br>値: `1`|
|低|`IMPORTANCE_NONE`|非通知<br>値: `0`|

### サンプルコード

```kotlin:ServiceManageService.kt
class SoundManageService : Service() {

    // クラス定数
    companion object {
        private const val CHANNEL_ID = "soundmanagerservice_notification_channel"
    }
    ...
    // サービスの初期化時に実行する処理
    // <- 親クラスのonCreate()メソッドの呼び出しは不要
    override fun onCreate() {
        ...
        // 通知チャネル名
        val name = getString(R.string.notification_channel_name)

        // 通知チャネルの重要度
        // -> NotificationManagerのクラス定数を利用
        val importance = NotificationManager.IMPORTANCE_DEFAULT

        // 通知チャネル
        val channel = NotificationChannel(CHANNEL_ID, name, importance)
        ...
    }
    ...
}
```

## NotificationManagerオブジェクトの取得・通知チャネルのセット
`NotificationManager`は`OSレベル(システムレベル)`で既に提供されている`サービスオブジェクト`であるため、`Context.getSystemService()`メソッドを用いて取得する。

また、独自に作成した`通知チャネル`を`NotificationManager`に管理させるには、`NotificationManager`内で`通知チャネル`を**再生成**する必要がある。

### 定義

```kotlin
// サービスオブジェクトの取得
fun <T: Any!> Context.getSystemService(serviceClass: Class<T>): T
// パラメータ
// serviceClass: OSレベル(システムレベル)で提供されるサービスオブジェクトのJavaクラス名
//               または
//               Contextクラス定数(後述)

// NotificationManagerに通知チャネルをセット
NotificationManager.createNotificationChannel(
    channel: NotificationChannel
): Unit
// channel: 通知チャネルオブジェクト
```

#### サービスオブジェクトを表すContextクラス定数
[参考: Context](https://developer.android.com/reference/kotlin/android/content/Context#getsystemservice)

`Javaクラス名`が使用できる`サービスオブジェクト`は限られているため、その場合は`Context`クラスの`クラス定数`を利用する。

### サンプルコード

```kotlin:SoundManageService.kt
class SoundManageService : Service() {
    ...
    // サービスの初期化時に実行する処理
    // <- 親クラスのonCreate()メソッドの呼び出しは不要
    override fun onCreate() {
        ...
        // 通知チャネル
        val channel = NotificationChannel(CHANNEL_ID, name, importance)

        // NotificationManagerオブジェクトの取得
        val manager = getSystemService(NotificationManager::class.java)

        // 通知チャネルをセット
        manager.createNotificationChannel(channel)
    }
    ...
}
```

## 通知情報の定義
[参考: NotificationCompat.Builder](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.Builder)
`通知情報`に関するデータは、`Notification`オブジェクトの`ファクトリクラス`である`NotificationCompat.Builder`に格納(=`セット`)する。

`Notification`オブジェクトに格納されるデータは「`Notification`オブジェクトが生成された時点で`NotificationCompat.Builder`オブジェクトが持っているデータ」であるため、
`Notification`オブジェクトを生成する前に`通知情報`に関するデータを`NotificationCompat.Builder`オブジェクトに格納しておく必要がある。

### 定義

```kotlin
// NotificationCompat.Builderオブジェクトの生成
NotificationCompat.Builder(
    @NonNull context: Context, 
    @NonNull channelId: String
)
// context: NotificationCompat.Builderオブジェクトを生成する
//          サービスクラスオブジェクト(=コンテキスト)
// channelId: Notificationオブジェクトを連携させる通知チャネルID

// 通知エリアに表示するアイコンの格納
@NonNull
NotificationCompat.Builder.setSmallIcon(
    icon: Int
): NotificationCompat.Builder
// パラメータ
// icon: アイコンのID(R値)

// 通知ドロワーに表示するタイトル
@NonNull
NotificationCompat.Builder.setContentTitle(
   @Nullable title: CharSequence?
): NotificationCompat.Builder
// パラメータ
// title: タイトルを表すCharSequence型文字列
// -> CharSequence: String型やStringBuilder型で実装されるインタフェース
// => String型やStringBuilder型の文字列が利用可能

// 通知ドロワーに表示するメッセージ
@NonNull
NotificationCompat.Builder.setContentText(
   @Nullable text: CharSequence?
): NotificationCompat.Builder
// パラメータ
// text: メッセージを表すCharSequence型文字列
```

### サンプルコード

```kotlin:SoundManagerService.kt
// クラス定数
companion object {
    private const val CHANNEL_ID = "soundmanagerservice_notification_channel"
}

// Notificationオブジェクトを生成するBuilderオブジェクト
val builder = NotificationCompat.Builder(this@SoundManageService, CHANNEL_ID)

// 通知エリアに表示するアイコン
builder.setSmallIcon(android.R.drawable.ic_dialog_info)

// 通知ドロワーに表示するタイトル
// -> Context.getString(resId:)メソッドを用いてCharSequence型文字列を取得
builder.setContentTitle(getString(R.string.msg_notification_title_finish))

// 通知ドロワーに表示するメッセージ
builder.setContentText(getString(R.string.msg_notification_text_finish))
```

## Notificationオブジェクトの生成
`Notification`オブジェクトは、その`ファクトリクラス`である`NotificationCompat.Builder`クラスの`build()`メソッドを用いて生成する。

### 定義

```kotlin
@NonNull
NotificationCompat.Builder.build(): Notification
```

### サンプルコード

```kotlin:SoundManageService.kt
// Notificationオブジェクトを生成するBuilderオブジェクト
val builder = NotificationCompat.Builder(this@SoundManageService, SoundManageService.CHANNEL_ID)

...   // 通知情報に関するデータの格納

// Notificationオブジェクト
val notification = builder.build()
```

## NotificationManagerCompatによる通知の実行
`通知`は`NotificationManagerCompat`の`notify()`メソッドを用いて実行する。

なお、`NotificationManagerCompat`オブジェクトは、独自に再定義した`NotificationManager`オブジェクトを管理する`サービスクラス`で管理されるため、
`NotificationManagerCompat`クラスの`from()`メソッドを用いて`サービスクラス`から取得する必要がある。

### 定義

```kotlin
// サービスクラスがもつNotificationManagerCompatオブジェクトの取得
@NonNull
NotificationManagerCompat.from(
    @NonNull context: Context
): NotificationManagerCompat
// パラメータ
// context: NotificationManagerを管理するサービスクラスオブジェクト(=コンテキスト)

// 通知の実行
NotificationManagerCompat.notify(
    id: Int, 
    @NonNull notification: Notification
): Unit
// パラメータ
// id: アプリ内で一意となる、Notificationオブジェクトの識別ID
// notification: Notificationオブジェクト
```

### サンプルコード

```kotlin:SoundManagerService.kt
// Notificationオブジェクトを生成するBuilderオブジェクト
val builder = NotificationCompat.Builder(this@SoundManageService, CHANNEL_ID)

...   // NotificationCompat.Builderに通知情報に関するデータをセット

// Notificationオブジェクトの生成
val notification = builder.build()

// サービスクラスからNotificationManagerCompatオブジェクトを取得
val manager = NotificationManagerCompat.from(this@SoundManageService)

// 通知を実行
manager.notify(100, notification)
```

***

# アクティビティを起動する通知の実装
[参考1: 通知からアクティビティを起動する](https://developer.android.com/training/notify-user/navigation?hl=ja)
[参考2: 研修3日目](https://qiita.com/b150005/items/f2c1d8236ac5e9770f4f)
`アクティビティ`間の画面遷移を実現する`インテント`を利用した`PendingIntent`を用いて、`通知ドロワー`から`アクティビティ`を起動することができる。

`通知ドロワー`から`アクティビティ`を起動する手順は、以下の通り。

> 1. 起動先`アクティビティ`の情報を保持する`Intent`オブジェクトの生成
> 2. 起動先`アクティビティ`に値を渡す必要があれば、`Intent`オブジェクトにデータを格納
> 3. `Intent`オブジェクトを保持する`PendingIntent`オブジェクトの生成
> 4. `NotificationCompat.Builder`オブジェクトに`PendingIntent`オブジェクトを格納(=`セット`)
> 5. `マニフェストファイル`(=`AndroidManifest.xml`)に`フォアグラウンドサービス`の`パーミッション`を記述
> 6. `Notification`オブジェクトを生成し、`サービス`を`フォアグラウンド`で実行

## インテント(Intent)
`アクティビティ`・`フラグメント`・`サービス`間の橋渡し役を担うクラス。
`遷移元`と`遷移先`の情報をもつ。

## PendingIntent
`サービスオブジェクト`などの`外部アプリケーション`に対して、`インテント`の実行権限(=`トークン`)を与えるメソッドを定義するクラス。

`PendingIntent`はあくまで`通知`(=`Notification`オブジェクト)などから`インテント`を実行できる**権限**に過ぎず、`ユーザ操作`によって**実行しない**(=`保留`する)ことも選択できるため、`PendingIntent`と命名された。

### IntentとPendingIntent

`Intent`は`アクティビティ`・`フラグメント`・`サービス`の起動を行う`オブジェクト`であり、
`PendingIntent`は`外部アプリケーション`に対する`インテント`の**実行権限**(=`トークン`)を指す。

## Intentオブジェクトの生成
`PendingIntent`は`外部アプリケーション`に対する`アクティビティ`・`サービス`の**起動権限**を指すが、
`起動先アクティビティ・サービス`の情報は`Intent`オブジェクトが保持するため、`Intent`オブジェクトを作成する。

### 定義

```kotlin
Intent(packageContext: Context!, cls: Class<*>!)
// パラメータ
// packageContext: 起動元コンテキスト
// cls: Javaクラス化した起動先コンテキスト
// -> KotlinのクラスをJavaに変換する場合は、"<Kotlinクラス名>::class.java"と記述
```

### サンプルコード

```kotlin:SoundManageService.kt
// 起動先アクティビティを指定するIntentオブジェクトの生成
val intent = Intent(this@SoundManageService, MainActivity::class.java)
```

## Intentオブジェクトとのデータのやり取り
[参考: 研修6日目(Bundle)](https://qiita.com/b150005/items/d72f35af46bc19a40f97#bundle)
`起動先`に対して`起動元`から値を渡す場合は、`Intent`クラスの`putExtra()`メソッドを用いてデータを格納する。

また、渡す値が`キー`と`値`をもつ`Map`型のデータ構造(=`Bundle`オブジェクト)である場合は、`putExtras()`メソッドを用いて`Intent`オブジェクトにデータを格納する。

`起動先`で値を受け取る際は、引数`name`に`キー`を指定した`get<T>Extra(name:defaultValue:)`メソッドを用いて値を取得する。

### 定義

```kotlin
// -- データの格納 --

// Intentオブジェクトへのデータ格納(起動元, Bundle型以外)
Intent.putExtra(name: String!, value: <T>): Intent
// パラメータ
// name: データ取得時に必要となるキー
// value: 起動先アクティビティ・サービスに渡す値

// Intentオブジェクトへのデータ格納(起動元, Bundle型)
Intent.putExtras(extras: Bundle): Intent
// パラメータ
// extras: 起動先アクティビティ・サービスに渡すBundleオブジェクト

// -- データの取得 --

// Intentオブジェクトからのデータ取得(起動先, Bundle型・配列以外)
Intent.get<T>Extra(name: String!, defaultValue: <T>): <T>
// パラメータ
// name: 取得する値のキー
// defaultValue: 対象のデータが存在しない場合に返される値

// Intentオブジェクトからのデータ取得(起動先, 配列)
Intent.get<T>ArrayExtra(name: String!): <T>Array?
// パラメータ
// name: 取得する配列データのキー

// Intentオブジェクトからのデータ取得(起動先, Bundle型)
Intent.getExtras(): Bundle?
```

### サンプルコード

```kotlin:SoundManageService.kt(起動元)
// 起動先アクティビティを指定するIntentオブジェクトの生成
val intent = Intent(this@SoundManageService, MainActivity::class.java)

// Intentオブジェクトにデータを格納
intent.putExtra("fromNotification", true)
```

```kotlin:MainActivity.kt(起動先)
// Intentオブジェクトからデータを取得
// <- アクティビティクラスはIntentオブジェクトをintentプロパティとして保持
val fromNotification = intent.getBooleanExtra("fromNotification", false)
```

## PendingIntentオブジェクトの生成
`PendingIntent`オブジェクトを生成するメソッドは、`起動先オブジェクト`によって異なる。

`起動先オブジェクト`が`アクティビティ`である場合は`getActivity()`メソッドを、
`サービス`である場合は`getService()`メソッドを用いる。

### 定義

```kotlin
// 起動先オブジェクトがアクティビティである場合
PendingIntent.getActivity(
    context: Context!, 
    requestCode: Int, 
    intent: Intent!, 
    flags: Int
): PendingIntent!
// パラメータ
// context: 起動元コンテキスト
// requestCode: コンポーネントの識別番号
// <- 複数のコンポーネントからPendingIntentを利用する場合に利用
// intent: Intentオブジェクト
// flags: OS内にPendingIntentオブジェクトが残っている場合の処理フラグ
// -> PendingIntentクラス定数を利用
```

#### PendingIntentオブジェクトが重複した場合の処理を表すPendingIntentクラス定数

|定数名|内容|
|:--|:--|
|`FLAG_ONE_SHOT`|既存オブジェクトの破棄<br>新しいオブジェクトの生成|
|`FLAG_NO_CREATE`|既存オブジェクトの再利用<br>※既存オブジェクトが存在しない場合は`null`|
|`FLAG_CANCEL_CURRENT`|最初に生成されたオブジェクトの利用|
|`FLAG_UPDATE_CURRENT`|既存オブジェクトの再利用<br>`extra`データのみ置換|

### サンプルコード

```kotlin:SoundManageService.kt
// アクティビティを起動するPendingIntentオブジェクト
val stopServiceIntent = PendingIntent.getActivity(
    this@SoundManageService,
    0,
    intent,
    PendingIntent.FLAG_CANCEL_CURRENT
)
```

## PendingIntentオブジェクトのセット
`PendingIntent`オブジェクトを起動するのは`Notification`オブジェクトであるが、
`Notification`オブジェクトを生成するのは`NotificationCompat.Builder`オブジェクトであるため、
`NotificationCompat.Builder`オブジェクトに`PendingIntent`オブジェクトをセットする。

なお、`タップ`された`通知`を自動的に消去する場合は`NotificationCompat.Builder`の`setAutoCancel()`メソッドの引数に`true`を指定する。

### 定義

```kotlin
// PendingIntentオブジェクトのセット
@NonNull
NotificationCompat.Builder.setContentIntent(
    @Nullable intent: PendingIntent?
): NotificationCompat.Builder
//パラメータ
// intent: 最終的にNotificationオブジェクトにセットする
//         PendingIntentオブジェクト

// 通知タップ時の通知ドロワーへの非表示フラグの変更
@NonNull
NotificationCompat.Builder.setAutoCancel(
    autoCancel: Boolean
): NotificationCompat.Builder
// パラメータ
// autoCancel: 通知がタップされた場合の通知ドロワーへの非表示フラグ
// -> true: タップ時に通知ドロワーから削除
//    false: タップ時に通知ドロワーに残置
```

### サンプルコード

```kotlin:SoundManageService.kt
// Notificationオブジェクトを作成するBuilderオブジェクト
val builder = NotificationCompat.Builder(this@SoundManageService, CHANNEL_ID)

// 起動先アクティビティを指定するIntentオブジェクトの生成
val intent = Intent(this@SoundManageService, MainActivity::class.java)

// アクティビティを起動するPendingIntentオブジェクト
val stopServiceIntent = PendingIntent.getActivity(
    this@SoundManageService,
    0,
    intent,
    PendingIntent.FLAG_CANCEL_CURRENT
)

// BuilderにPendingIntentオブジェクトをセット
builder.setContentIntent(stopServiceIntent)

// タップされた通知メッセージを自動的に消去
builder.setAutoCancel(true)
```

## マニフェストファイルへのパーミッションの追記
[参考: 研修10日目(バックグラウンド処理)](https://qiita.com/b150005/items/bc7054a520d4b858dc0f#%E3%83%90%E3%83%83%E3%82%AF%E3%82%B0%E3%83%A9%E3%82%A6%E3%83%B3%E3%83%89%E5%87%A6%E7%90%86)
`通知`を含む`サービス`は本来`バックグラウンド処理`で動作するが、`PendingIntent`を用いて`ユーザ操作`を受け付ける場合は`フォアグラウンド処理`で動作させる必要がある。

ここで、`サービス`を`フォアグラウンド処理`で動作させる場合は、`マニフェストファイル`(=`AndroidManifest.xml`)に`パーミッション`を追記する必要がある。

### フォアグラウンド処理

`アクティビティ`から独立した`サービス`によって`メインスレッド`上で実行される、ユーザから**直接認識され**、ユーザによって**操作を受ける**処理。

### サンプルコード

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    ...
</manifest>
```

## サービスをフォアグラウンドで起動
`アクティビティ`の起動を`ユーザ操作`によって行うため、`アクティビティ`を起動する`通知`を実行する`サービスクラス`は、`Service`クラスの`startForeground()`メソッドを用いて起動する。

### 定義

```kotlin
Service.startForeground(
    id: Int, 
    notification: Notification!
): Unit
// パラメータ
// id: アプリ内で一意となる、Notificationオブジェクトの識別番号
// notification: 通知するNotificationオブジェクト
```

### サンプルコード

```kotlin:SoundManageService.kt
// Notificationオブジェクトの生成
val notification = builder.build()

// サービスのフォアグラウンド実行
startForeground(200, notification)
```