しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

Kotlinでアプリを閉じた時のバックグラウンド処理を動かす

アプリを閉じた時に通知をセットする方法について下URLを参考に調べてみました

developer.android.com

調べた所、通知セットはMainActivityのonStopが良いようです
onPauseでも動くのですが、重い処理は途中で停止される事もあるのでonStopの方が確実そうです

ちなみにonStopはMainActivityが閉じてからすぐに開いた場合は呼ばれません
具体的には他アプリを開いてそのアプリが落ちてMainActivityに戻る時などです
ただこのケースでは再度MainActivityを閉じる時にonStopが呼ばれるので問題はなさそうです
他にはonPauseの途中でクラッシュした場合も呼ばれなくなるので注意が必要です

実際に通知のセットもしてみようと思います
今回はアプリを閉じて10秒後に通知が鳴るようにしました

まずは下のようなReceiverクラスを作成します

class AlarmReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val builder = NotificationCompat.Builder(context, "channelId")
            .setSmallIcon(android.R.drawable.btn_plus)
            .setContentTitle("タイトル")
            .setContentText("説明")
            .setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
            .setAutoCancel(true)
        NotificationManagerCompat.from(context).apply {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
                return
            }
            notify(0, builder.build())
        }
    }
}

次はAndroidManifest.xmlにPOST_NOTIFICATIONSの権限追加とAlarmReceiverの登録を行います

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

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

    <application>
        <receiver
            android:name=".AlarmReceiver"
            android:exported="false" />

           <!-- 略 -->
    </application>
</manifest>

最後にMainActivityで下のメソッドを作成してonPauseで呼び出しました

fun setAlarm() {
    val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
    val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager

    if (notificationManager?.getNotificationChannel("channelId") == null) {
        val channel = NotificationChannel("channelId", "チャンネル名", NotificationManager.IMPORTANCE_HIGH).apply {
            description = "説明文"
        }
        notificationManager?.createNotificationChannel(channel)
    }

    val date = Calendar.getInstance().apply { add(Calendar.SECOND, 10) }
    val notificationIntent = Intent(this, AlarmReceiver::class.java)
    val pendingIntent = PendingIntent.getBroadcast(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
    alarmManager.set(AlarmManager.RTC_WAKEUP, date.timeInMillis, pendingIntent)
}

上記で無事に通知を鳴らす事ができるようになりました

また、確実に通知をセットする場合はWorkManagerを使う事もできます
WorkManager自体多少遅延する可能性はありますが

まずはbuild.gradleで下のようにライブラリを追加します

dependencies {
    implementation("android.arch.work:work-runtime-ktx:1.0.1")
}

次は下のようにWorkerクラスを作成します

class MyWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
    override fun doWork(): Result {
        setAlarm()

        return Result.success()
    }

    fun setAlarm() {
        val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
        val alarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager

        if (notificationManager?.getNotificationChannel("channelId") == null) {
            val channel = NotificationChannel("channelId", "チャンネル名", NotificationManager.IMPORTANCE_HIGH).apply {
                description = "説明文"
            }
            notificationManager?.createNotificationChannel(channel)
        }

        val date = Calendar.getInstance().apply { add(Calendar.SECOND, 10) }
        val notificationIntent = Intent(applicationContext, AlarmReceiver::class.java)
        val pendingIntent = PendingIntent.getBroadcast(applicationContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
        alarmManager.set(AlarmManager.RTC_WAKEUP, date.timeInMillis, pendingIntent)
    }
}

最後にMainActivityのonStopでMyWorkerを呼び出します

override fun onStop() {
    super.onStop()

    val workRequest = OneTimeWorkRequestBuilder<MyWorker>().build()
    WorkManager.getInstance().enqueue(workRequest)
}

WorkManagerを使う時、Android Studioのバージョンによっては下のようなエラーが発生します

Duplicate class android.support.v4.app.INotificationSideChannel found in modules core-1.13.0.aar -> core-1.13.0-runtime (androidx.core:core:1.13.0) and support-compat-26.1.0.aar -> support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)

手元の環境ではgradle.propertiesに下の行を追加する事で改善しました
下で出てくるJetifierとはSupport Libraryへの参照をAndroidXへの参照に置き換えてくれるツールのようです

android.enableJetifier=true

上記をAndroid12で動かす時、下のエラーでクラッシュしました

 FATAL EXCEPTION: WorkManager-WorkManagerTaskExecutor-thread-0 (Ask Gemini)
Process: com.example.myapplication, PID: 26318
java.lang.IllegalArgumentException: com.example.myapplication: Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent.
Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE if some functionality depends on the PendingIntent being mutable, e.g. if it needs to be used with inline replies or bubbles.

こちらはbuild.gradleに下の行を追加する事で改善しました

implementation("androidx.work:work-runtime:2.7.1")

上記でも通知を鳴らす事ができました