Kotlin FirebaseMessaging 通知受信時にアプリの状態を参照

# Kotlin FirebaseMessaging 通知受信時にアプリの状態を知る
通知を受信した際に、アプリがフォアグランド／バックグラウンド／ターミネートの状態で処理を分けたい場合がります。とくにフルスクリーン通知やFlutterでの処理を行う場合など。FirebaseMessagingService内でその状態を知ることができます。

# フォアグランド／バックグラウンドか判定

```PushNotificationListenerService.kt
class PushNotificationListenerService : FirebaseMessagingService() {

    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)

        val isForeground = isAppInForeground(this)

        if (isForeground) {
            Log.d("FCM", "アプリはフォアグラウンド")
            handleForegroundNotification(message)
        } else {
            Log.d("FCM", "アプリはバックグラウンド")
            handleBackgroundNotification(message)
        }
    }

    private fun isAppInForeground(context: Context): Boolean {
        val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val appProcesses = activityManager.runningAppProcesses ?: return false

        for (appProcess in appProcesses) {
            if (appProcess.processName == context.packageName) {
                return appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
            }
        }
        return false
    }

    private fun handleForegroundNotification(message: RemoteMessage) {
        // フォアグラウンド時の通知処理
        Log.d("FCM", "フォアグラウンド通知処理")
    }

    private fun handleBackgroundNotification(message: RemoteMessage) {
        // バックグラウンド時の通知処理
        Log.d("FCM", "バックグラウンド通知処理")
    }
}
```

こんなふうにして利用

```AndroidManifest.xml
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    
    <!-- MESSAGING_EVENTコールバックハンドラ登録 -->
    <service
            android:name=".PushNotificationListenerService"
            android:exported="false">
        <intent-filter>
            <action android:name="com.google.firebase.MESSAGING_EVENT" />
        </intent-filter>
    </service>
```

# フォアグランド／バックグラウンド／ターミネートの判定

```PushNotificationListenerService.kt
class PushNotificationListenerService : FirebaseMessagingService() {

    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)

        val appState = getAppState()
        Log.d("FCM", "アプリの状態: $appState")

        when (appState) {
            AppState.FOREGROUND -> {
                Log.d("FCM", "フォアグラウンド: アプリが表示中です")
                handleForegroundNotification(message)
            }
            AppState.BACKGROUND -> {
                Log.d("FCM", "バックグラウンド: アプリは非表示ですが動作中です")
                handleBackgroundNotification(message)
            }
            AppState.TERMINATED -> {
                Log.d("FCM", "ターミネート: アプリが完全に終了していました")
                handleTerminatedNotification(message)
            }
        }
    }

    private fun handleForegroundNotification(message: RemoteMessage) {
        Log.d("FCM", "フォアグラウンド通知処理を実行")
    }

    private fun handleBackgroundNotification(message: RemoteMessage) {
        Log.d("FCM", "バックグラウンド通知処理を実行")
    }

    private fun handleTerminatedNotification(message: RemoteMessage) {
        Log.d("FCM", "ターミネート通知処理を実行")
        // フルスクリーン通知などを呼び出し
    }

    private fun getAppState(): AppState {
        val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val appProcesses = activityManager.runningAppProcesses

        // アプリプロセスが存在しない場合はターミネート
        if (appProcesses.isNullOrEmpty()) {
            return AppState.TERMINATED
        }

        // アプリプロセスを検索
        for (appProcess in appProcesses) {
            if (appProcess.processName == packageName) {
                return when (appProcess.importance) {
                    ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND -> AppState.FOREGROUND
                    ActivityManager.RunningAppProcessInfo.IMPORTANCE_BACKGROUND,
                    ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED -> AppState.BACKGROUND
                    else -> AppState.TERMINATED
                }
            }
        }

        // プロセス情報が取得できない場合もターミネートと見なす
        return AppState.TERMINATED
    }

    enum class AppState {
        FOREGROUND,
        BACKGROUND,
        TERMINATED
    }
}
```

フルスクリーン通知の例 Android 14以降

```kotlin

    // 新しいトークンが生成された時の処理
    // 中でサーバにトークンを送信する処理などを定義
    override fun onNewToken(p0: String) {
        super.onNewToken(p0)

        // チャンネルidを設定
        addChannelId()
    }
    
    // チャンネルの設定
    private fun addChannelId() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channelId = resources.getString(R.string.channel_id)
            val channelName = resources.getString(R.string.channel_name)
            val importance = NotificationManager.IMPORTANCE_HIGH
            val notificationChannel = NotificationChannel(channelId, channelName, importance).apply {
                enableLights(true) // ライトの設定を有効にする
                lightColor = Color.BLUE // ライトの色を設定
                enableVibration(true) // バイブレーションを有効にする
                description = "This channel is used for important notifications"
            }

            val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(notificationChannel)
        }
    }

    private fun sendNotificationFullScreen(title: String, text: String) {
        // 通知タップ時に遷移するアクティビティを指定
        val intent = Intent(this, MainActivity::class.java)
        //val intent = Intent(this, CustomActivity::class.java)
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
        // 何度も遷移しないようにする（1度だけ！）
        val pendingIntent: PendingIntent = PendingIntent.getActivity(this,0, intent, PendingIntent.FLAG_IMMUTABLE)

        // 通知メッセージのスタイルを設定（改行表示に対応）
        val inboxStyle = NotificationCompat.InboxStyle()
        val messageArray: List<String> = text.split("\n")
        for (msg: String in messageArray) {
            inboxStyle.addLine(msg)
        }

        // 通知音の設定
        val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)

        // 通知の見た目を設定
        val notificationBuilder
                = NotificationCompat.Builder(this, resources.getString(R.string.channel_id))
                .setContentTitle(title)
                .setContentText(text)
                // ステータスバーに表示されるアイコン
                .setSmallIcon(R.mipmap.ic_launcher)
                // 上で設定したpendingIntentを設定
                .setContentIntent(pendingIntent)
                // 優先度を最大化
                .setPriority(PRIORITY_MAX)
                .setDefaults(NotificationCompat.DEFAULT_ALL)
                // 通知音を出すように設定
                .setSound(defaultSoundUri)
                .setFullScreenIntent(pendingIntent, true)

        // Android 8.0 以上はチャンネル設定 必須
        // strings.xml に channel_id を指定してください
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            notificationBuilder.setChannelId(resources.getString(R.string.channel_id))
        }
        
        // 通知を実施
        // UUIDを付与することで各通知をユニーク化
        val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        val uuid = UUID.randomUUID().hashCode()
        notificationManager.notify(uuid, notificationBuilder.build())

    }
```

### 補足
実行環境によってはうまくいかないこともあるようなので、、、

```kotlin
private fun getAppState(): AppState {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    val appProcesses = activityManager.runningAppProcesses ?: return AppState.TERMINATED

    // プロセスがリスト内に存在するかを確認
    val appProcess = appProcesses.find { it.processName == packageName }

    if (appProcess == null) {
        // プロセスが見つからない場合はターミネートとみなす
        return AppState.TERMINATED
    }

    // プロセスが見つかった場合は状態を判定
    return when (appProcess.importance) {
        ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND -> AppState.FOREGROUND
        ActivityManager.RunningAppProcessInfo.IMPORTANCE_BACKGROUND,
        ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED -> AppState.BACKGROUND
        else -> AppState.TERMINATED
    }
}
```