Android 13 までの通知関連の仕様とその実装方法を理解する

皆様こんにちは。医療ユニットだった人、現在は産休から育休中になりました山口です。
小さめの赤ちゃんだった我が子は、体型も態度も見事でかくなりました。

さて、過去の記事 にて Android 10 に対応するため着信通知の実装方法について書きました。
あれから約 1 年半、8 月中頃に Android 13 が正式リリースがなされましたが *1、その中でも個人的に気になったのが通知関連の仕様変更が含まれていたことでした。
今回は最新の Android 13 までの通知に関連する新規追加内容や仕様変更を理解しながら、その実装方法を解説していきます。
サンプルアプリ自体は以前の GitHub にて公開していたプロジェクトに追加コミットしていますので、こちらも合わせてご確認いただけると幸いです。

本題

というわけで、早速 build.gradle の targetSDK, compileSDK を最新の API33 まで上げましょう。
サンプルのプロジェクトがそこそこ古かったので、ついでに Gradle や ライブラリのバージョンも最新に更新しました。
以前に紹介した プロジェクトの更新方法 に添って対応していけば大体はできます。

Gradle 更新後に一旦プロジェクトをビルドすると、もしかしたら Manifest ファイルでビルドエラーを起こすかもしれません。
Android 12 以降からは Activity, Service, Receiver すべてに Exported 属性を明示的に宣言する必要があるため、 android:exported をもれなく定義しましょう。*2

通知の権限が変更 (Android 13 変更点)

Android 13 では通知周りの仕様変更が入り、それまで Normal Level の権限だったものが Dangerous に変更されて厳しくなりました。*3
つまりユーザーが各アプリごとに通知のラインタイムパーミッションを許可しないと通知が表示されなくなりました。
当然のことながら着信通知も通知の一種なので例外ではありません。

この変更点は Android 11 の変更点である未使用アプリの権限を自動リセットする仕様とも関連してきます。*4
これは Android 11 以降のアプリが数か月間使用されていない場合、ユーザーデータを保護するためにユーザーが許可したランタイムパーミッションを OS が自動的にリセットするというものです。
通知もユーザー許可が必要な権限になったことで自動リセット対象になり、長期間アプリを使用していないと着信含めて通知がブロックされる可能性があります。

アクティビティ図

上記の仕様に対応するため、公式が提示しているアプリが実行時に権限をリクエストするためのおすすめの方法に従って実装を行うとよいでしょう。 *5
これは通知に限った話ではなく、カメラやマイクなどのすべてのランタイムパーミッションに対して言えることです。
アクティビティ図に書き起こすと以下のようになります。

このフローにおいて、「権限が付与されているか?」及び「権限をリクエスト」以降の処理は必須です。
「権限を必要とする理由を説明する必要があるか?」及び「説明のための UI 表示」はなくても最低限の機能要件は満たせる、言わばオプション的なものですが、該当のフローを実装することでユーザーの困惑を緩和して権限付与を受け入れやすくするメリットがあることから、公式では推奨しているようです。

実装方法

まずは Manifest ファイルに通知権限 POST_NOTIFICATIONS を追加した上で、

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>


先ほどのアクティビティ図を元に通知のランタイムパーミッションをざっくりと実装したものが以下です。
アプリ側の UI 表示系は省略しているので、具体的な使い方はサンプルを見たほうがわかりやすいと思います。
なお、複数の権限を一括でリクエストしたい場合は ActivityResultContracts#RequestPermission() ではなく ActivityResultContracts#RequestMultiplePermissions() を使うことで実装可能です。

class SampleActivity : AppCompatActivity() {

    // ランタイムパーミッションの結果.
    private val requestPermissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) {
            if (it) {
                // 権限が許可されたので、該当の権限が必要な操作が可能.
                ...
            } else {
                // 権限が拒否されたので、該当の権限が必要な操作が不可.
                ...
            }
        }

    ...

    private fun checkNotificationPermission() {
        // OS バージョン確認.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
            // Android 13 未満は通知権限不要.
            return
        }
        // 通知権限が許可されているか確認.
        if (ContextCompat.checkSelfPermission(
                this, Manifest.permission.POST_NOTIFICATIONS)
            == PackageManager.PERMISSION_GRANTED) {
            // 権限許可済.
            return
        }
        // 通知権限を必要とする理由をアプリが提示する必要があるか確認.
        if (shouldShowRequestPermissionRationale(
                Manifest.permission.POST_NOTIFICATIONS)) {
            // 必要がある場合は、その理由を説明するためのUIを明示的に表示する.
            ...
            return
        }
        // 通知権限をリクエストする.
        requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
    }
}

新しい API を使った着信通知 (Android 12 追加点)

Android 12 では着信通知のための新しい通知スタイル Notification.CallStyle が追加されました。*6
以前の記事でも紹介した既存の着信通知との違いを比較してみます。

実装方法

実装差分は Notification 生成部分のみなので、該当箇所を抜粋して比較します。

既存がこちら。
以前の記事で紹介した通りなので詳細は省略します。

val notification = NotificationCompat.Builder(context, ChannelType.CALL.id).apply {
    setSmallIcon(R.drawable.app_icon_small)
    setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.app_icon))
    setContentTitle(title)
    setContentText(message)
    setPriority(NotificationCompat.PRIORITY_HIGH)
    setDefaults(NotificationCompat.DEFAULT_SOUND)
    setCategory(NotificationCompat.CATEGORY_CALL)
    setAutoCancel(true)
    setStyle(NotificationCompat.BigTextStyle().bigText(message))
    setSound(ringtoneUri, AudioManager.STREAM_RING)
    setFullScreenIntent(callPendingIntent(context, NotificationId.CALL.id), true)
    addAction(
        R.drawable.refuse_button,
        getColorString(context, R.string.button_refuse, R.color.colorRefuse),
        refusePendingIntent(context, NotificationId.CALL_REFUSE.id)
    )
    addAction(
        R.drawable.accept_button,
        getColorString(context, R.string.button_accept, R.color.colorAccept),
        callPendingIntent(context, NotificationId.CALL_ACCEPT.id, true)
    )
}.build()


そして新しいやり方がこちら。
既存の方法よりも簡潔でスッキリとした印象を受けるのではないでしょうか。

// 発信者の情報を生成
val person = Person.Builder().apply {
    setName(callerName) // 発信者名
    setImportant(true)  // 通知の優先度を上げるため重要人物であることを示すフラグを設定
}.build()

val notification = Notification.Builder(context, ChannelType.CALL.id).apply {
    setSmallIcon(R.drawable.app_icon_small)
    setFullScreenIntent(callPendingIntent(context, NotificationId.CALL.id), true)
    // 通知スタイルを着信専用に設定
    setStyle(
        Notification.CallStyle.forIncomingCall(
            person,
            refusePendingIntent(context, NotificationId.CALL_REFUSE.id),
            callPendingIntent(context, NotificationId.CALL_ACCEPT.id, true)
        )
    )
}.build()


新しい方法は、まず発信者の情報を生成するために Person クラスを使います。
今回は必要最低限の実装に絞っているため、必須情報として発信者名があれば十分です。
また着信通知の優先度を上げるため、「発信者=重要人物」とみなし setImportant(true) を設定しておきます。

次に Notification を生成しますが、Android Jetpack ライブラリの NotificationCompat.Builder ではなくデフォルトの Notification.Builder を使います。
既存の方法は設定の項目数が多かったですが、新しい方法は Notification.CallStyle#forIncomingCall() という着信通知専用のスタイルで一括設定できるようになりました。
使い方は簡単で、このメソッドに先ほど生成した Person と、応答と拒否それぞれの PendingIntent を指定するだけです。

表示比較

Android Emulator (Pixel 4a, API33) で表示を比較したのが以下です。
新しい方法のボタンは、文字やカラー、アイコンなど、いかにも着信らしい UI ですが、これらはアプリ側の指定ではなく OS が勝手に表示しているものです。
また、今回は最低限の実装により発信者アイコンを指定しなかったため、左側に表示されるアイコンが発信者名の頭文字になっていますが、Person#setIcon() で任意のアイコンに設定できます。
登録済の人だとアイコンが表示されるのは電話アプリなどでよくありがちな仕様ですよね。
こうやって比較すると、最近の電話アプリを思い浮かべたときにイメージする見た目は新しい方法のほうが近いのではないでしょうか。

その他の細かい修正点

PendingIntent の可変性

Android 12 以降のアプリにて PendingIntent を使用する場合、セキュリティ強化のために可変性を指定する必要があります。*7
使用する PengindIntent が可変か不可変かによって、明示的にフラグ (FLAG_MUTABLE or FLAG_IMMUTABLE) を指定しましょう。
大抵の使い道は不可変になると思いますし、サンプルでも不可変フラグを追加しています。

OnLifecycleEvent が非推奨

以前の記事にて、着信通知が出ている状態で OS のホーム画面やアプリ一覧画面から着信通知を出しているアプリのアイコンをタップした場合、着信通知が消える代わりに着信画面が全面に出るような実装を紹介していました。
この挙動を実現するために、OnLifecycleEvent を継承してアプリがフォアグラウンドになったことを検知していましたが、今回の対応でライブラリを最新に更新したことにより OnLifecycleEvent が非推奨になりました。*8
公式が提示しているように DefaultLifecycleObserverLifecycleEventObserver に差し替えましょう。
サンプルでは DefaultLifecycleObserver を継承して実装しましたが、必要なメソッドだけ継承すれば良くなったので以前よりも扱いやすいメリットを感じられました。

まとめ

今回は最新の Android 13 までの通知の仕様を理解するため、Android 13 の通知権限の変更内容と Android 12 の新しい着信通知の実装方法を主に解説しました。

スマホアプリにおいて通知機能が搭載されたアプリはごく一般的であり、故に通知のランタイムパーミッションは早めに対応したほうがよいでしょう。
特に着信が必要なアプリの場合、ユーザーが通知権限を拒否したときに着信が受け取れないことがわかるような設計にしましょう。
拒否直後の警告ダイアログだけでなく、着信が受け取れない状態であることを常に UI 表示するなどの工夫も考えられます。
少なくとも権限拒否が起因して着信を受けられなかった場合に、ユーザーに致命的な不具合と誤解されないようにだけはしたいものです。
企業が提供するアプリならば尚更、アプリの低評価に繋がる前に対応版をリリースしておきたいですね。

また、新しい着信通知を紹介をしましたが、既存のものを使うことに何か問題があるというわけではありません。
新しいスタイルを積極的に取り込みたいならば OS バージョンによる分岐をかけて新しい方法を追加実装しますし、一方で OS バージョン関係なくアプリの見た目をなるべく合わせるならば追加実装することなくそのままにしておくでしょう。
複数の実装方法を把握した上で、そのアプリの設計ポリシーに最も適した方法を採用しましょう。



オンライン診療ポケットドクターはお手持ちのスマホの AppStore もしくは GooglePlay から無料でダウンロードすることが可能です。

オプティムでは共に開発を行うエンジニアを募集しています。