ぺんぎんメモ

プログラミングのメモです。たまに私生活のことや鬱っぽいことを書きます。

2021年09月02日 位置情報の取得

今日は 1,040 円だった。
昼に 380 円のモスバーガー単品、夜にうどんだった。

アプリを起動していない状態で常に現在位置をサーバーに送信するためには、けっこう長い手順を踏まなければならない。事前知識のない状態で実装することは僕にはできなかった。ある程度知識を身に付けた状態でようやく実装し終えることができた。

まずサービスについて。
サービスはアプリが非アクティブ状態になっても常に動作し続ける。しかし非アクティブ状態のサービスは制限を受ける。たとえば位置情報サービスの場合、1時間に数回しか現在位置を取得できなくなる。アクティブ状態であればこのような制限は受けない。
サービスであってもこのような制限を受けたくない場合がある。
そういったときのために、フォアグラウンドサービスが用意されている。

フォアグラウンドサービスであれば制限を受けないが、その代わりに通知領域にサービスの内容を表示し続けなければならない。この通知領域の作り方も一工夫要る。
これはドキュメントに載っていることをそのまま実装すればいいが、何もわからない状態だと、たとえ写経するだけであったとしても辛かったりする。
具体的には、次のようなルールを満たすコードを書く必要がある。

  • onStartCommand コールバックメソッド内で、startForeground() を使って通知領域を作成する
  • サービスの開始は startForegroundService() で行う
  • ACCESS_BACKGROUND_LOCATION を有効にしなければならない
  • ACCESS_COARSE_LOCATION 権限の要求と ACCESS_BACKGROUND_LOCATION 権限の要求は、完全に別々に行わなければならない

これらはコンパイル時にエラーが確認できず、すべて実行時に確認できる。
これがけっこう辛い。
最後については、requestPermissions(arrayOf(ACCESS_COARSE_LOCATION, ACCESS_BACKGROUND_LOCATION), 123) を呼び出してもエラーが出ない。
色々と頑張る必要がある。

通知の作成にも一癖あって、ただ Notification オブジェクトを作って startForeground() に渡せばいいというものではなく、その前に createNotificationChannel で通知チャンネルを作成しなければならない。

以上のことをまとめると onStartCommand コールバックメソッドの実装は次のようになる。

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    val id = "id"

    if (manager.getNotificationChannel(id) == null) {
        val mChannel = NotificationChannel(id, "name", NotificationManager.IMPORTANCE_HIGH)
        mChannel.apply {
            description = "description"
        }
        manager.createNotificationChannel(mChannel)
    }

    val notification = NotificationCompat.Builder(this, id).apply {
        setContentTitle("content title")
        setContentText("content text")
        setSmallIcon(R.drawable.ic_launcher_background)
    }.build()
    startForeground(1, notification)
    return START_STICKY
}

AndroidManifest.xml と MainActivity.kt(のonCreate)には次のように書く。

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val btnA = findViewById<Button>(R.id.buttonA)
    btnA.setOnClickListener {
        requestPermissions(arrayOf(
            ACCESS_COARSE_LOCATION,
            ACCESS_FINE_LOCATION,
        ), 123)
    }
    val btnB = findViewById<Button>(R.id.buttonB)
    btnB.setOnClickListener {
        requestPermissions(arrayOf(
            ACCESS_BACKGROUND_LOCATION,
        ), 123)
    }
    val btnC = findViewById<Button>(R.id.buttonC)
    btnC.setOnClickListener {
        val intent = Intent(this, MyService::class.java)
        startForegroundService(intent)
    }
}

実際に位置情報を取得するコードはこちらこちらなどに書かれている。