Codelabsの通知機能の実装をViewModelを使って書き換えてみた

## はじめに
[Codelabs タスク1：基本的な通知を作成する](https://developer.android.com/codelabs/android-training-notifications#2)を参考に通知機能を実装してみた。
![notification.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670405/cde8bdd6-3beb-e4a8-3cb4-fff7d5b40f28.gif)

## 通知機能の実装ポイント
### ①通知チャネルを作成する
* チャネルIDを用意
* 通知チャネルのカスタマイズ初期設定(APIレベル26以上に対して)
    * チャネルの設定名称
    * 通知の優先度（他にも通知が複数あったときの表示位置の優先度）
    * バイブ、LEDの色設定
    * チャネルの説明文
* 注意事項
    * Android8.0（APIレベル26）より古いデバイスはカスタマイズできない。
    * Android8.0（APIレベル26）以上は通知チャネルを作成しないとクラッシュする。

#### 通知チャネル画面を比較する
|Android7.0（APIレベル24）|Android10.0（APIレベル29）|
| ---- | ---- |
|![スクリーンショット 2021-03-17 17.58.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670405/eba51c3b-7813-0b04-57ac-012ca1e040cd.png)|![スクリーンショット 2021-03-17 17.59.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670405/298582ed-ac6d-fe3c-ab22-1ab678f9b3c7.png)|
#### 通知チャネルのカスタマイズ画面（APIレベル26以上）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670405/3495e4e2-b018-e909-ea83-dd1fc791f528.png" width=50%>

### ②通知を作成する
* 通知IDを用意
* 通知する内容を設定
    * タイトル
    * 本文
    * アイコン
    * タップしたときの処理
    * 通知の優先度（API25以下に対して）
    * バイブ、LEDの色設定（API25以下に対して）

### ③通知を送信する
ボタンがクリックされた時に②で設定した通知を送信させる

## 実装
### layout
```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".MainActivity">

    <data>
        <variable
            name="viewModel"
            type="com.example.samplenotification.NotificationViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <Button
            android:id="@+id/notify"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Notify Me!"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            android:onClick="@{(v) -> viewModel.sendNotification()}"/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```
### Interface
```java:NotificationView
public interface NotificationView {

    PendingIntent getNotificationContentIntent(int notificationId);

    Context getContext();
}
```

### Activity
```java:MainActivity.java
public class MainActivity extends AppCompatActivity implements NotificationView{

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

        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        NotificationViewModel notificationViewModel = new NotificationViewModel();

        // ViewModelにActivityのViewを紐付ける
        notificationViewModel.setView(this);
        // xmlのviewModelにNotificationViewModelを紐付ける
        binding.setViewModel(notificationViewModel);

        // 通知チャネル作成
        notificationViewModel.createNotificationChannel();

    }

    // PendingIntentを使用すると、Android通知システムがコードに代わって割り当てられたアクションを実行してくれる。
    @Override
    public PendingIntent getNotificationContentIntent(int notificationId) {
        Intent notificationIntent = new Intent(this, MainActivity.class);
        // PendingIntentの作成にはコンテンツインテント(アクティビティを起動するインテント)と通知idが必要
        PendingIntent notificationPendingIntent = PendingIntent.getActivity(this,
                notificationId, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        return notificationPendingIntent;
    }

    @Override
    public Context getContext() {
        return this;
    }
}
```


### ViewModel
```java:NotificationViewModel.java
public class NotificationViewModel {

    // チャネル ID　全てのチャネルはパッケージ内で一意のIDに関連付ける
    private static final String PRIMARY_CHANNEL_ID = "primary_notification_channel";
    // PendingIntentに渡す通知id(requestCode)。 通知作成,更新または削除する場合に必要になるため、通知IDは必ず保存する。
    private static final int NOTIFICATION_ID = 0;
    // 通知処理を管理してくれるオブジェクト
    private NotificationManager mNotifyManager;
    private NotificationView view;

    public void setView(NotificationView view) {
        this.view = view;
    }

    // 通知チャネル作成（ユーザーが通知設定をカスタマイズできるようにする）
    public void createNotificationChannel() {
        mNotifyManager = (NotificationManager) view.getContext().getSystemService(NOTIFICATION_SERVICE);

        // Android 8.0（APIレベル26）より古いデバイスはカスタマイズできない。逆にAndroid8.0（APIレベル26）以上は作成しないとクラッシュする。
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            NotificationChannel notificationChannel = new NotificationChannel(PRIMARY_CHANNEL_ID, "お知らせ", NotificationManager.IMPORTANCE_HIGH);

            // 通知機能の初期設定
            notificationChannel.enableLights(true);
            notificationChannel.setLightColor(Color.RED);
            notificationChannel.enableVibration(true);
            notificationChannel.setDescription("お知らせの説明");

            mNotifyManager.createNotificationChannel(notificationChannel);
        }
    }

    // 送信する通知内容を定義
    private NotificationCompat.Builder getNotificationBuilder() {

        // MainActivityを起動してくれる通知のコンテンツインテントを取得
        PendingIntent notificationPendingIntent = view.getNotificationContentIntent(NOTIFICATION_ID);

        // 通知ビルダー(チャネル ID の指定を必要とするコンストラクタ)の初期設定をする
        NotificationCompat.Builder notifyBuilder = new NotificationCompat.Builder(view.getContext(), PRIMARY_CHANNEL_ID)
                .setContentTitle("通知されました")
                .setContentText("ここは本文です")
                .setSmallIcon(R.drawable.ic_android)  // 通知アイコンの設定
                .setContentIntent(notificationPendingIntent) // ユーザーが通知をタップしたときに起動するインテント
                .setAutoCancel(true) // ユーザーが通知をタップしたときに通知を閉じる
                // 以下、Android 8.0（APIレベル26）より古いデバイスに対しての設定
                .setPriority(NotificationCompat.PRIORITY_HIGH) // 通知の優先度を設定。通知が複数あったとき優先度の高い通知から上に並べられる
                .setDefaults(NotificationCompat.DEFAULT_ALL); // 通知の音、バイブレーション、LEDの色パターンをデフォルト値に設定
        return notifyBuilder;
    }

    // 通知ビルダーの結果を送信する（通知発信）
    public void sendNotification() {
        NotificationCompat.Builder notifyBuilder = getNotificationBuilder();
        mNotifyManager.notify(NOTIFICATION_ID, notifyBuilder.build());
    }
}
```

## おわりに
次回は通知の更新、キャンセルを紹介

## 参考サイト
https://developer.android.com/codelabs/android-training-notifications#2