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

[前回](https://qiita.com/kilalabu/items/ca420f2346635efe19a8)の続き
[タスク2：通知を更新またはキャンセルする](https://developer.android.com/codelabs/android-training-notifications#3)を参考に通知機能を実装してみた。

|update|cancel|
|----|----|
|![update.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670405/ddf7246f-4f77-603f-5f7c-55e59e389192.gif)|![cancel.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670405/8e69d8c0-2261-e0aa-f122-5104baf70390.gif)|

## 実装
### layout
`android:enabled`でボタン操作をコントロール
`true`のときボタンが押せる
`false`のときボタンが押せない

```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_constraintBottom_toTopOf="@+id/update"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:enabled="@{viewModel.sendButtonEnabled}"
            android:onClick="@{(v) -> viewModel.sendNotification()}"/>

        <Button
            android:id="@+id/update"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Update Me!"
            app:layout_constraintBottom_toTopOf="@+id/cancel"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/notify"
            android:enabled="@{viewModel.updateButtonEnabled}"
            android:onClick="@{(v) -> viewModel.updateNotification()}"/>

        <Button
            android:id="@+id/cancel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Cancel Me!"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/update"
            android:enabled="@{viewModel.cancelButtonEnabled}"
            android:onClick="@{(v) -> viewModel.cancelNotification()}"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```
### ViewModel
#### ボタンコントロールの設定
```java:ViewModel抜粋
public class NotificationViewModel extends BaseObservable {
    
    public boolean sendButtonEnabled = true; // 起動時はsendボタンのみ押せるようにする
    public boolean updateButtonEnabled;
    public boolean cancelButtonEnabled;

    @Bindable public boolean isSendButtonEnabled() {
        return sendButtonEnabled;
    }

    public void setSendButtonEnabled(boolean sendButtonEnabled) {
        this.sendButtonEnabled = sendButtonEnabled;
        notifyPropertyChanged(BR.sendButtonEnabled);
    }

    @Bindable public boolean isUpdateButtonEnabled() {
        return updateButtonEnabled;
    }

    public void setUpdateButtonEnabled(boolean updateButtonEnabled) {
        this.updateButtonEnabled = updateButtonEnabled;
        notifyPropertyChanged(BR.updateButtonEnabled);
    }

    @Bindable public boolean isCancelButtonEnabled() {
        return cancelButtonEnabled;
    }

    public void setCancelButtonEnabled(boolean cancelButtonEnabled) {
        this.cancelButtonEnabled = cancelButtonEnabled;
        notifyPropertyChanged(BR.cancelButtonEnabled);
    }

}
```
#### 更新通知の定義
```java:ViewModel抜粋
    public void updateNotification() {
        // 画像ファイルをビットマップに変換。 
        // 外部に既に存在している画像ファイルを読み込むには「BitmapFactory」クラスを利用する。
        Bitmap androidImage = BitmapFactory.decodeResource(view.getContext().getResources(), R.drawable.mascot_1);

        NotificationCompat.Builder notifyBuilder = getNotificationBuilder();
        // 通知ビルダーの設定内容を上書きする
        // BigPictureStyleは、NotificationCompat.Styleのサブクラス。通知の代替レイアウトを提供する。
        notifyBuilder.setStyle(new NotificationCompat.BigPictureStyle()
                .bigPicture(androidImage)
                .setBigContentTitle("通知更新！"))
                .setContentText("本文が更新されました");

        // 通知を送信する
        mNotifyManager.notify(NOTIFICATION_ID, notifyBuilder.build());
        // updateボタンを押せなくさせる
        setUpdateButtonEnabled(false);
    }
```
#### キャンセルの定義
```java:ViewModel抜粋
    public void cancelNotification() {
        // IDに紐づく通知をキャンセルする
        mNotifyManager.cancel(NOTIFICATION_ID);

        // 各ボタンのコントロール
        setSendButtonEnabled(true);
        setUpdateButtonEnabled(false);
        setCancelButtonEnabled(false);
    }
```
#### ViewModel全文
```java:NotificationViewModel.java
public class NotificationViewModel extends BaseObservable {

    // チャネル 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 boolean sendButtonEnabled = true;
    public boolean updateButtonEnabled;
    public boolean cancelButtonEnabled;

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

    @Bindable public boolean isSendButtonEnabled() {
        return sendButtonEnabled;
    }

    public void setSendButtonEnabled(boolean sendButtonEnabled) {
        this.sendButtonEnabled = sendButtonEnabled;
        notifyPropertyChanged(BR.sendButtonEnabled);
    }

    @Bindable public boolean isUpdateButtonEnabled() {
        return updateButtonEnabled;
    }

    public void setUpdateButtonEnabled(boolean updateButtonEnabled) {
        this.updateButtonEnabled = updateButtonEnabled;
        notifyPropertyChanged(BR.updateButtonEnabled);
    }

    @Bindable public boolean isCancelButtonEnabled() {
        return cancelButtonEnabled;
    }

    public void setCancelButtonEnabled(boolean cancelButtonEnabled) {
        this.cancelButtonEnabled = cancelButtonEnabled;
        notifyPropertyChanged(BR.cancelButtonEnabled);
    }

    // 通知チャネル作成（ユーザーが通知設定をカスタマイズできるようにする）
    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() {

        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());
        setSendButtonEnabled(false);
        setUpdateButtonEnabled(true);
        setCancelButtonEnabled(true);
    }

    public void updateNotification() {
        // ビットマップに変換。 外部に既に存在している画像ファイルを読み込むには「BitmapFactory」クラスを利用する。
        Bitmap androidImage = BitmapFactory.decodeResource(view.getContext().getResources(), R.drawable.mascot_1);

        NotificationCompat.Builder notifyBuilder = getNotificationBuilder();
        // 通知ビルダーの設定内容を上書きする
        // BigPictureStyleは、NotificationCompat.Styleのサブクラス。通知の代替レイアウトを提供する。
        notifyBuilder.setStyle(new NotificationCompat.BigPictureStyle()
                .bigPicture(androidImage)
                .setBigContentTitle("通知更新！"))
                .setContentText("本文が更新されました");

        mNotifyManager.notify(NOTIFICATION_ID, notifyBuilder.build());
        setUpdateButtonEnabled(false);
    }

    public void cancelNotification() {
        mNotifyManager.cancel(NOTIFICATION_ID);
        setSendButtonEnabled(true);
        setUpdateButtonEnabled(false);
        setCancelButtonEnabled(false);
    }
}
```
### 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;
    }
}
```

## 参考サイト
https://developer.android.com/codelabs/android-training-notifications?index=..%2F..%2Fandroid-training#3