🚶‍♂️

Kotlinでセンサをバックグラウンドで取得し続けるアプリの実装方法

に公開

本記事ではKotlinでバックグラウンドでセンサを扱うために、AndroidでKotlinを用いて下のように「加速度センサの値をログ出力するアプリ」の実装方法を解説します。

https://www.youtube.com/watch?v=6Y2jINfZJME

以下の流れで解説します
1. フォアグラウンドで加速度センサの値を取得してログ出力する
2. バックグラウンドで加速度センサの値を取得してログ出力する

1.フォアグラウンドで加速度センサの値を取得してログ出力する

まずはアプリ画面が表示されている(フォアグラウンド)状態で加速度センサの値を取得しログ出力してみます。

実装方法

下のドキュメントからわかるようにセンサの値を取得するには権限や依存関係の追加が必要ありません。(高いサンプリング周波数で取得する場合のみ権限「HIGH_SAMPLING_RATE_SENSORS」の追加が必要)
https://developer.android.com/develop/sensors-and-location/sensors/sensors_overview?hl=ja

そのためMainActivity.ktを以下のようにして色々importするだけで簡単に実装できます。

MainActivity.kt
class MainActivity : ComponentActivity(), SensorEventListener {
    private lateinit var sensorManager: SensorManager
    private var AccSensor: Sensor? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        // センサマネージャの取得と加速度センサの登録
        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        AccSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION)
        AccSensor?.also { acc ->
            sensorManager.registerListener(this, acc, SensorManager.SENSOR_DELAY_NORMAL)
        }

        super.onCreate(savedInstanceState)
        setContent {
            Text("Hello Sensor!")
        }
    }

    // センサの値が変化したときに呼ばれる
    override fun onSensorChanged(event: SensorEvent){
        var sensorX: Float
        var sensorY: Float
        var sensorZ: Float
        // Remove the gravity contribution with the high-pass filter.
        if (event.sensor.type === Sensor.TYPE_LINEAR_ACCELERATION) {
            sensorX = event.values[0]
            sensorY = event.values[1]
            sensorZ = event.values[2]
            Log.d("MainActivity", "加速度センサー(X, Y, Z) = ($sensorX, $sensorY, $sensorZ)")
        }
    }

    //センサの精度が変更されたときに呼ばれる
    override fun onAccuracyChanged(p0: Sensor?, p1: Int) {
    }

    // アクティビティが閉じられたときにリスナーを解除する
    override fun onPause() {
        super.onPause()
        sensorManager.unregisterListener(this)
    }
}

onSensorChanged()やonAccuracyChanged()などのコードについては、こちらの記事がとてもわかりやすく解説していますので気になる方はご覧ください。
https://zenn.dev/log_suzaki/articles/eaff0d733009c4

ここまでのアプリの動作

ここまで実装ができたら以下のようにアプリがフォアグラウンド状態のときに加速度センサの値を取得しログ出力できているはずです。
https://www.youtube.com/watch?v=NqOcXrHKajk

2.バックグラウンドで加速度センサの値を取得してログ出力する

バックグラウンドで位置情報を取得するためにフォアグラウンドサービスを使います。
https://developer.android.com/develop/background-work/services?hl=ja

加速度センサの値を取得しログ出力するサービスの作成

MainActivity.ktに書いていた加速度センサの値を取得しログ出力する処理をサービスクラスに移すためserviceフォルダを作成しSensorService.ktを作成します。

 .
+├── service
+│    └── SensorService.kt
 └── MainActivity.kt

フォアグラウンドサービスを利用するにはユーザに知られずに動くゴーストアプリを防ぐために、アプリが動いているのを知らせるための通知が義務付けられています。そのため、SensorService.ktには加速度センサの値を取得しログ出力する処理に加えて通知を出す処理も書きます。

service/SensorService.kt
class SensorService: Service(), SensorEventListener {
    companion object {
        const val CHANNEL_ID = "45678"  // 任意の値
    }
    private lateinit var notificationManager: NotificationManagerCompat

    private lateinit var sensorManager: SensorManager
    private var AccSensor: Sensor? = null

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

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

        // センサーマネージャーの取得と加速度センサーの登録
        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        AccSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION)
        AccSensor?.also { acc ->
            sensorManager.registerListener(this, acc, SensorManager.SENSOR_DELAY_NORMAL)
        }

        // 通知マネージャーを取得(通知チャンネル作成などに使用)
        notificationManager = NotificationManagerCompat.from(this)

        // 通知チャンネルの作成
        val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
            .setName("センサ取得")
            .build()
        // チャンネルを通知マネージャーに登録
        notificationManager.createNotificationChannel(channel)

        // 通知をタップしたときにMainActivityを開くIntentを作成
        val openIntent = Intent(this, SensorService::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(1515, notification)

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

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

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

    // センサの値が変化したときに呼ばれる
    override fun onSensorChanged(event: SensorEvent){
        var sensorX: Float
        var sensorY: Float
        var sensorZ: Float
        // Remove the gravity contribution with the high-pass filter.
        if (event.sensor.type === Sensor.TYPE_LINEAR_ACCELERATION) {
            sensorX = event.values[0]
            sensorY = event.values[1]
            sensorZ = event.values[2]
            Log.d("Sensor", "加速度センサー(X, Y, Z) = ($sensorX, $sensorY, $sensorZ)")
        }
    }

    //センサの精度が変更されたときに呼ばれる
    override fun onAccuracyChanged(p0: Sensor?, p1: Int) {
    }
}

必要な権限の追加とサービスの宣言

AndroidManifest.xmlに以下の権限を追加します。

  • FOREGROUND_SERVICE
    • フォアグラウンドサービスに必要
  • POST_NOTIFICATIONS
    • フォアグラウンドサービス中は通知を出しておく必要がある。その通知を出すのに必要。
  • REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
    • フォアグラウンドサービス中はバッテリーの最適化をオフにしないとそのうち動作が止まってしまう。バッテリーの最適化をオフにさせるのに必要。
  • FOREGROUND_SERVICE_DATA_SYNC
    • foregroundServiceType="dataSync"のサービスに必要
    • foregroundServiceTypeはフォアグラウンドサービスでどんな処理をするかに合わせる(一覧

また先ほど作成した「service/SensorService.kt」を登録します。

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

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

権限のリクエスト画面とサービスの起動/終了ボタンを追加

MainActivity.ktに以下のように加速度センサの値を取得しログ出力する処理を消して、権限リクエストとサービス起動/終了ボタンを追加します。

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)
        setContent {
            Column {
                Button(
                    onClick = {
                        // Intentオブジェクト
                        val intent = Intent(application, SensorService::class.java)
                        // サービスの起動
                        startService(intent)
                    }
                ) {
                    Text(
                        text="サービス起動"
                    )
                }
                Button(
                    onClick = {
                        // Intentオブジェクト
                        val intent = Intent(application, SensorService::class.java)
                        // サービスの終了
                        stopService(intent)
                    }
                ) {
                    Text(
                        text="サービス終了"
                    )
                }
            }
        }
    }
}

完成したアプリの動作

https://www.youtube.com/watch?v=6Y2jINfZJME

まとめ

今回はAndroidにおいてフォアグラウンドサービスを用い、バックグラウンドで加速度センサのデータを取得しログ出力するアプリを実装しました。加速度センサだけでなく、ジャイロセンサや気圧センサなども同様の手法で取得可能です。

センサデータを分析する際には、ログに出力するだけでなく、ファイルに保存して後から参照・解析できるようにすることが重要です。そうしたデータの記録方法に焦点を当てた、わかりやすい記事が以下にありますので興味のある方はぜひご覧ください。
https://zenn.dev/log_suzaki/articles/9bed032ba619f1

Discussion