🖋️

Kotlinでバックグラウンドで動き続ける簡単なアプリの実装方法

に公開

本記事ではKotlinでのバックグラウンド処理に触れるために、AndroidでKotlinを用いて下のように「ボタンを押すと1秒ごとにログを出力する処理をバックグラウンドで始める」簡単なアプリケーションの実装方法を解説します。

https://www.youtube.com/watch?v=4-wXnd0nMV4

以下の流れで解説します。
1. AndroidStudioでプロジェクト作成
2. バックグラウンドで処理を続ける
3. 権限のリクエストを画面に出す
補足. 電源を再起動すると処理を再開する

1.AndroidStudioでプロジェクト作成

Android Studioで「New Project」


Empty Activityを選択します。
その後「Next」ボタンをクリックします。


Nameにはプロジェクト名を入力しますが今回は「ForegroundServiceSample」とします。
Package nameとSave location(保存場所)は何でも大丈夫です。
入力した後右下の「Finish」ボタンをクリックします。

その後数分程度待つとプロジェクトが作成されます。

スマートフォンでアプリケーションを起動

スマートフォンの「設定」から開発者モードをONにしUSBデバッグをONにします。

その状態でPCとスマートフォンをUSBケーブルで繋ぐと下の画像のように接続したデバイス名が出てきますので、この状態で再生ボタンを押すとアプリケーションがスマートフォンにインストールされ起動します。Android StudioのUIはバージョンによって変わりますのでご注意ください。

そして下の「Terminal」や「Problems」があるメニューから「Logcat」を選択するとアプリケーションのログを見られます。

この段階では下のようにスマホの画面には「Hello Anrdoid!」などの固定値が表示されてLogcatにはこれといったログは出ません。
https://www.youtube.com/watch?v=yTpJcIizfbI

2.バックグラウンドで処理を続ける

1秒ごとにログを出力する機能を実装

次にバックグラウンドで「ボタンを押すと1秒ごとにログを出力するアプリケーション」を実装します。
そのためにフォアグラウンドサービスというものを使います。
まずはMainActivity.ktと同じ階層に「service」というフォルダを「New」→「Package」から作成します。この「service」フォルダの中に「LogService」というファイルを「New」→「Kotlin Class/File」から作成します。その「LogService」の中身は以下のようにします。

service/LogService.kt
class LogService: Service() {
    override fun onCreate() {
        super.onCreate()
    }

    @RequiresApi(Build.VERSION_CODES.O)
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // バックグラウンドでログを出力し続ける処理(1秒ごとにログ)
        Log.d("Service", "サービスが開始")
        var i = 0
        CoroutineScope(Dispatchers.IO).launch {
            while (true) {
                Log.d("Service", i.toString())
                i++
                delay(1000)
            }
        }
        return START_STICKY
    }

    override fun onDestroy() {
        // サービス終了時の処理
        Log.d("Service", "サービスが終了")
        super.onDestroy()
    }

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }
}

プロジェクト作成のときに生成されるAndroidManifest.xmlに以下の<service.../>を追記します。

AndroidManifest.xml
...

    <application
        ...
        <activity
            ...>
                ...
        </activity>

+        <service
+            android:name=".service.LogService"
+            android:foregroundServiceType="dataSync"
+            android:enabled="true"
+            android:exported="true"
+            android:directBootAware="true"/>
    </application>

...

サービスを開始するボタンをアプリケーションの画面に表示させるため「MainActivity.kt」の一部を修正します。

MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ForegroundServiceSampleTheme {
+                Button(
+                    onClick = {
+                        // Intentオブジェクト
+                        val intent = Intent(application, LogService::class.java)
+                        // サービスの起動
+                        startService(intent)
+                    }
+                ) {
+                    Text(
+                        text="サービス起動"
+                    )
+                }
            }
        }
    }

この状態で実行すると以下のようにLogcatを見ると1秒ごとにログが出力されています。スリープやホーム画面に戻ってもログが出続けます。しかしタスクキルや一定時間が経つなどすると止まってしまいバックグラウンドで実行させるには実装が足りていません。

https://www.youtube.com/watch?v=B9dZxjfU0rU

タスクキルしても処理を続けさせる

タスクキルしても動作し続けるようにするようにバックグラウンドで処理を実行するには、アプリケーションが動いているのをユーザが認識できるように通知を出す必要があります。

そのために通知の権限とフォアグラウンドサービスの権限を追加する必要があるので、AndroidManifest.xmlの<application .../>の上に以下を追記します。

AndroidManifest.xml

 <manifest ... />

+    <!-- フォアグラウンドサービス -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+    <!-- 通知 -->
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+    <!-- foregroundServiceType="dataSync"のサービスに必要 -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

    <application
...

次にLogService.ktに通知を出す処理を追加します。

service/LogService.kt
 class LogService: Service() {
+   companion object {
+       const val CHANNEL_ID = "12345"
+   }
+   private lateinit var notificationManager: NotificationManagerCompat

    override fun onCreate() {
        super.onCreate()
    }

    @RequiresApi(Build.VERSION_CODES.O)
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // バックグラウンドでの処理
        Log.d("Service", "サービスが開始")

+       // 通知マネージャーを取得(通知チャンネル作成などに使用)
+       notificationManager = NotificationManagerCompat.from(this)
+
+       // 通知チャンネルの作成
+       val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
+           .setName("ログ出力")
+           .build()
+       // チャンネルを通知マネージャーに登録
+       notificationManager.createNotificationChannel(channel)
+
+       // 通知をタップしたときにMainActivityを開くIntentを作成
+       val openIntent = Intent(this, MainActivity::class.java).let {
+           PendingIntent.getActivity(this, 0, it, PendingIntent.FLAG_IMMUTABLE)
+       }
+
+       // 通知を作成
+       val notification = NotificationCompat.Builder(this, channel.id)
+           .setSmallIcon(R.drawable.ic_launcher_foreground)
+           .setContentTitle("ログ出力中")
+           .setContentText("フォアグラウンドサービスでログを出力し続けています")
+           .setContentIntent(openIntent)
+           .setOngoing(true)
+           .build()
+       // 通知の表示
+       startForeground(1212, notification)

        // バックグラウンドでログを出力し続ける処理(1秒ごとにログ)
        var i = 0
        CoroutineScope(Dispatchers.IO).launch {
            while (true) {
                Log.d("Service", i.toString())
                i++
                delay(1000)
            }
        }

        // サービスが強制終了されたときにも自動的に再起動されるようにする
        return START_STICKY
    }

    override fun onDestroy() {
        // サービス終了時の処理
        Log.d("Service", "サービスが終了")
        super.onDestroy()
    }

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }
}

この状態で実行するとアプリケーション詳細の権限に通知が表示されているはずです。

これを許可し、その状態で「サービス開始」ボタンを押すと通知が出てアプリケーションをタスクキルしてもログが出続けるようになります。

https://www.youtube.com/watch?v=4-wXnd0nMV4

一定時間が経過しても処理を止めさせない

一定時間が経過しシステムが勝手にアプリケーションを停止させるのを防ぐためにバッテリーの最適化をオフにする必要があります。そのための権限をAndroidManifest.xmlに追記します。

AndroidManifest.xml
    <!-- フォアグラウンドサービス -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <!-- 通知 -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <!-- foregroundServiceType="dataSync"のサービスに必要 -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
+    <!-- バッテリーの最適化のオフ -->
+    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

この状態でアプリケーションの詳細のバッテリー関係のメニューから「制限なし」に設定すれば、システムが勝手にアプリケーションを停止させるのを防げます。

https://developer.android.com/develop/background-work/services/fgs?hl=ja

3.権限のリクエストを画面に出す

現在はバッテリー最適化のオフと通知の権限の許可をいちいちアプリ詳細から設定しに行かないといけません。これはユーザにとっては全くやさしくありません。そのためアプリ起動時に権限のリクエストを画面に出すようにします。

MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
+        // 通知の権限のリクエスト
+        val permissions = arrayOf(Manifest.permission.POST_NOTIFICATIONS)
+        val requestCode = 100
+        if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+            Toast.makeText(this, "通知が許可されていません", Toast.LENGTH_SHORT).show()
+            // リクエストを送信する処理
+            requestPermissions(permissions,requestCode)
+        } else {
+            Toast.makeText(this, "通知が許可されています", Toast.LENGTH_SHORT).show()
+        }
    ...

次にバッテリー制限なしのリクエストです。

MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // 通知の権限のリクエスト
        val permissions = arrayOf(Manifest.permission.POST_NOTIFICATIONS)
        val requestCode = 100
        if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "通知が許可されていません", Toast.LENGTH_SHORT).show()
            // リクエストを送信する処理
            requestPermissions(permissions,requestCode)
        } else {
            Toast.makeText(this, "通知が許可されています", Toast.LENGTH_SHORT).show()
        }

+       // バッテリーの最適化を外させる
+       val tmpIntent = Intent()
+       val packageName = packageName
+       val pm = getSystemService(POWER_SERVICE) as PowerManager
+       if (!pm.isIgnoringBatteryOptimizations(packageName)) {
+           tmpIntent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
+           tmpIntent.data = Uri.parse("package:$packageName")
+           startActivity(tmpIntent)
+       }
        super.onCreate(savedInstanceState)
        ...

すると以下のようにアプリ起動時に権限のリクエストを画面に出すことができます。
https://www.youtube.com/watch?v=l3IgZPaNSRQ

https://appdev-room.com/android-permission

補足.電源を再起動すると処理を再開する

現時点では、端末の電源を再起動するとバックグラウンド処理は停止したままになります。しかし、アプリによっては常に処理を継続してほしい場合もあります。そうした場合、端末の再起動後に自動でバックグラウンド処理を再開させる必要があります。

そのためにAndroidが発行するブロードキャストIntentを受け取り、処理を再開する仕組みを実装します。以下のように、BroadcastReceiverを実装し端末起動時のブロードキャストを検知してサービスを再起動します。

broadcast/SampleBroadcastReciever.kt
class SampleBroadcastReciever: BroadcastReceiver() {
    override fun onReceive(application: Context, intent: Intent?) {
        Log.d("Broadcast", "何かを検出したよ")

        when (intent?.action) {
            Intent.ACTION_LOCKED_BOOT_COMPLETED -> {
                Log.d("Broadcast", "ロック解除を検知とサービス開始")
                val intent = Intent(application, LogService::class.java)
                startForegroundService(application, intent)
            }
        }
    }
}

Manifest.xmlに以下を追記します。

Manifest.xml
    <!-- 通知 -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <!-- foregroundServiceType="dataSync"のサービスに必要 -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
    <!-- バッテリーの最適化のオフ -->
    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
+   <!-- Broadcast -->
+   <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
...
    <application...>
    ...
+     <receiver
+            android:name=".broadcast.SampleBroadcastReceiver"
+            android:directBootAware="true"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+     </receiver>
    ...
    </application>

MainActivity.ktに以下を追記します。

MainActivity.kt
class MainActivity : ComponentActivity() {
+   // ブロードキャストレシーバー
+   private val br: BroadcastReceiver = BroadcastReciever()
    override fun onCreate(savedInstanceState: Bundle?) {
        val permissions = arrayOf(Manifest.permission.POST_NOTIFICATIONS)
        val requestCode = 100
        if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "通知が許可されていません", Toast.LENGTH_SHORT).show()
            // リクエストを送信する処理
            requestPermissions(permissions,requestCode)
        } else {
            Toast.makeText(this, "通知が許可されています", Toast.LENGTH_SHORT).show()
        }

        // バッテリーの最適化を外させる
        val tmpIntent = Intent()
        val packageName = packageName
        val pm = getSystemService(POWER_SERVICE) as PowerManager
        if (!pm.isIgnoringBatteryOptimizations(packageName)) {
            tmpIntent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
            tmpIntent.data = Uri.parse("package:$packageName")
            startActivity(tmpIntent)
        }

+       // BroadcastReceiverを登録
+       val intentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).apply {
+           addAction(Intent.ACTION_BOOT_COMPLETED)
+       }
+       registerReceiver(br, intentFilter)
+
        super.onCreate(savedInstanceState)
        ...

https://developer.android.com/develop/background-work/background-tasks/broadcasts?hl=ja

まとめ

今回は、Androidでフォアグラウンドサービスを使い、バックグラウンドで動作し続ける簡単なアプリを実装しました。
最後に紹介したブロードキャストIntent(今回はACTION_BOOT_COMPLETEDを使用)には、他にもさまざまな種類があります。たとえば、BluetoothがONになったときや、充電が開始されたときなどにもブロードキャストが送信されます。これらとフォアグラウンドサービスを組み合わせることで、「充電が開始されたら通知を出す」といったアプリも作成できます。
このように、ブロードキャストIntentを活用することで、アプリのアイデアや実装の幅が大きく広がります。ぜひ、どのようなブロードキャストがあるのかを調べて実際に試してみてください。

次回以降はフォアグラウンドサービスを使って「バックグラウンドで位置情報を取り続けるアプリ」や「バックグラウンドでAPIを定期的に叩くアプリ」などの実装方法を解説しようと思っています。またどこかで見かけましたらよろしくお願いします。

Discussion