見出し画像

Now in REALITY Tech #42 AndroidでFullScreenIntentを用いて全画面の着信通知を作成する

こんにちは、REALITYのAndroidエンジニアの営業終了大魔王です。

今回は、REALITY Androidで通話アプリなどでよく使われている、全画面の通知の実装をしたので、そのことについてまとめて行こうと思います。

どうして全画面通知なのか

REALITYにはビデオチャットという機能があります。

ビデオチャット機能では、ZoomやGoogle Meetのようなビデオ通話をアバターで実現することができます。

現在のREALITYにおけるビデオチャットには、誰でも参加可能な、開放されたビデオチャットと、チャット上の特定の人やグループで開催される閉じたビデオチャットがあります。

前者の解放されたビデオチャットでは、多くのユーザーに参加する導線が出現しますが、後者はそうではありません。そこで、閉じたビデオチャットを開始した際に、すぐに周りからの反応が得られるようにLINEなどのアプリなので使われているような全画面の着信通知を実装しました。

FullScreenIntentとは

全画面の通知はFullScreenIntentを用いて実装することができます。

FullScreenIntentとはAndroidのPendingIntentの一種でOSで通知を表示するときのNotificationに設定すると、全画面の通知を表示することができます(OSの状態によっては通常の通知になることがあります)。

全画面通知を使うにはAndroidManifest.xmlで以下の権限をリクエストする必要があります。

// 余計なスペースが入っていた
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT "/>

// 正しい書き方
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>

余談ですが、権限の文字列に余計なスペースが混ざってしまっていたことに気がつかず、何度試しても全画面が表示されない沼に三日ほどハマってしまいました。

実際の通知の作成は以下のようになっています。

val tag = getRandomString(8)
val contentIntent = Intent("android.intent.action.VIEW").apply {
    setPackage(context.packageName)
    data = Uri.parse(config.customScheme + "://{PATH_TO_THE_CONTENT}")
    putExtra(TAG, tag)
}
val contentPendingIntent = PendingIntent.getActivity(
    context, 0, contentIntent,
    FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
)

val fullScreenIntent = Intent(context, OffAppIncomingVideoChatActivity::class.java).apply {
    putExtra(...)
    putExtra(TAG, tag)
}

val fullScreenPendingIntent = PendingIntent.getActivity(
    context, 0, fullScreenIntent,
    FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
)

val cancelIntent = Intent(context, CallCancelReceiver::class.java).apply {
    putExtra(TAG, tag)
}
val cancelPendingIntent = PendingIntent.getBroadcast(context, 0, cancelIntent, FLAG_UPDATE_CURRENT or FLAG_MUTABLE)

val bitmap = getBitmapFromUrl(context, osNotification.largeIcon)
val notification = NotificationCompat.Builder(context, channelId)
    // (省略 setSmallIconなど)
    .setFullScreenIntent(fullScreenPendingIntent, true)
    .build()

val manager = NotificationManagerCompat.from(context)
notification.flags = notification.flags or Notification.FLAG_AUTO_CANCEL
manager.notify(tag, NOTIFICATION_ID, notification)

通知内に表示される、通知を削除するキャンセルボタンの実装は、通知のActionに通知をキャンセルするためのBroadcastReceiverをPendingIntentとして設定し、通知を削除するようになっています。

class CallCancelReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        context?.let {
            val tag = intent?.getStringExtra(TAG) ?: ""
            val manager = NotificationManagerCompat.from(it)
            manager.cancel(tag, NOTIFICATION_ID)
        }
    }
}

OffAppIncomingVideoChatActivityがロック画面の上に表示される全画面の通知の実態で、中身の実装は以下のようになってます。

class OffAppIncomingVideoChatActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // ロック中にに全画面で表示するのに必要
        showWhenLockedAndTurnScreenOn()
        window.setFlags(
            WindowManager.LayoutParams.FLAG_FULLSCREEN,
            WindowManager.LayoutParams.FLAG_FULLSCREEN
        )
        setContent {
            OffAppIncomingVideoChatScreen(OffAppIncomingVideoChatScreenData(
                ...
            ))
        }
        super.onCreate(savedInstanceState)
    }

    private fun showWhenLockedAndTurnScreenOn() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
            setShowWhenLocked(true)
            setTurnScreenOn(true)
        } else {
            window.addFlags(
                WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                        or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
            )
        }
    }
}

UIはJetpack Composeで実装されています。REALITY AndroidではJetpack Composeを導入していて、新規画面だけでなく、既存画面のCompose化も推進しています。

こんな感じの見た目です


おわりに

REALITYでは現在一緒に働く仲間を積極的に募集しています!
少しでも興味が湧いたら下記フォームから気軽にカジュアル面談をお待ちしています。

REALITYエンジニアカジュアル面談お申し込みフォーム