[Android] Oreoのバックグラウンド実行制限下でGeofencingを使う

# はじめに

この記事は、[Android Advent Calendar 2018](https://qiita.com/advent-calendar/2018/android)の3日目の記事です。

最近Geofencingを実装する機会があったのですが、思いのほか詰まってしまい大変だったので、その時の解決方法を共有したいと思います。

# Geofencingとは 

Geofencingを利用すると、ある位置座標から半径N[m]に入ったり出たりしたことを簡単に検知することが出来ます。例えばお店の近くを通った時に、そのお店の情報をプッシュ通知でお知らせするなどの使い方が出来ます。![Geofencing](https://developer.android.com/images/training/geofence.png)

([Create and monitor geofences  |  Android Developers](https://developer.android.com/training/location/geofencing)より引用)

# これだとOreoで動かない

とりあえず[公式ドキュメント](https://developer.android.com/training/location/geofencing)を参考にしながらGeofencingを実装していきます。

Gradleに`implementation 'com.google.android.gms:play-services-location:16.0.0'`を追加して準備は完了です。

まずはイベントを処理する側から作ります。

```kotlin
class GeofenceTransitionsIntentService : IntentService("Geofence") {
    override fun onHandleIntent(intent: Intent?) {
        val geofencingEvent = GeofencingEvent.fromIntent(intent)

        when (geofencingEvent.geofenceTransition) {
            Geofence.GEOFENCE_TRANSITION_ENTER -> sendNotification("Enter")
            Geofence.GEOFENCE_TRANSITION_EXIT -> sendNotification("Exit")
            else -> sendNotification("error")
        }
    }

    private fun sendNotification(text: String) {
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        createNotificationChannel(manager)

        val notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setStyle(NotificationCompat.BigTextStyle())
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setContentTitle("Geofence")
            .setContentText(text)
            .build()
        manager.notify(0, notification)
    }

    private fun createNotificationChannel(manager: NotificationManager) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            if (manager.getNotificationChannel(CHANNEL_ID) == null) {
                val channel = NotificationChannel(CHANNEL_ID, "name", NotificationManager.IMPORTANCE_HIGH)
                channel.description = "description"
                manager.createNotificationChannel(channel)
            }
        }
    }

    companion object {
        private const val CHANNEL_ID = "channelId"
    }
}
```

`IntentService`の`onHandleIntent()`でGeofenceのイベントの発火を受け取り、通知を作成するだけです。

次にGeofenceを設置する部分は、こんな感じになります。

```kotlin
val geofencingClient = LocationServices.getGeofencingClient(this)

val geofence = Geofence.Builder()
    .setRequestId("Geofence")
    .setCircularRegion(35.681098, 139.767062, 100f)     // 東京駅から半径100m
    .setExpirationDuration(Geofence.NEVER_EXPIRE)
    .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
    .build()

val request = GeofencingRequest.Builder()
    .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
    .addGeofence(geofence)
    .build()

val pendingIntent = PendingIntent.getService(
    this,
    0,
    Intent(this, GeofenceTransitionsIntentService::class.java),
    PendingIntent.FLAG_UPDATE_CURRENT)

geofencingClient.addGeofences(request, pendingIntent)?.also {
    it.addOnSuccessListener { Toast.makeText(this, "addOnSuccess", Toast.LENGTH_SHORT).show() }
    it.addOnFailureListener { Toast.makeText(this, "addOnFailure", Toast.LENGTH_SHORT).show() }
}
```

公式ドキュメント通りに作ったので特に問題はないはずです。

さっそくビルドして実行。Fake GPSというアプリを利用して、位置情報をGeofenceが設置されている`35.681098, 139.767062`に偽装すると

![notification](https://qiita-image-store.s3.amazonaws.com/0/152199/af5e99d0-9a06-76b6-42a4-d1ab23296c7c.png)

通知が来ました！めでたしめでたし…とはなりません。ここで実行しているアプリをkillした状態で、もう一度通知が来るかどうか試してみます。

うーん、今度は通知が来ません。原因を探るためにログを確認してみます。Logcatの右上から`No Filters`を指定すると、全てのログを確認することが出来ます。

![logcat](https://qiita-image-store.s3.amazonaws.com/0/152199/4e1358cd-b24a-66d6-48de-8825634777e8.png)

たくさんあるログの中から`Geofence`で検索してみると、それっぽいログが見つかりました。

```
W/ActivityManager: Background start not allowed: service Intent { cmp=jp.shiita.geofences/.GeofenceTransitionsIntentService (has extras) } to jp.shiita.geofences/.GeofenceTransitionsIntentService from pid=-1 uid=10020 pkg=jp.shiita.geofences
```

どうも`Service`はバックグラウンドから実行することは出来ないみたいです。[Background Execution Limits | Android Developers](https://developer.android.com/about/versions/oreo/background.html)

> Android 8.0 では、追加機能があります。システムは、バックグラウンド アプリによるバッグラウンド サービスの作成を許可しません。

# 解決方法

いろいろと調べたところ、どうやら`BroadcastReceiver`を使えば大丈夫なようです。`IntentService`で書かれたコードを`BroadcastReceiver`に書き換えていきます。

```kotlin
class GeofenceTransitionsBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        context ?: return
        val geofencingEvent = GeofencingEvent.fromIntent(intent)

        when (geofencingEvent.geofenceTransition) {
            Geofence.GEOFENCE_TRANSITION_ENTER -> sendNotification(context, "Enter")
            Geofence.GEOFENCE_TRANSITION_EXIT -> sendNotification(context, "Exit")
            else -> sendNotification(context, "error")
        }
    }
}
```

変わったところは`BroadcastReceiver`は`Context`を継承していない点と、`onHandleIntent()`が`onReceive()`になった点ぐらいですね。

Geofenceを設置する部分も修正すれば完了です。

```kotlin
// IntentServiceは使えない
// val pendingIntent = PendingIntent.getService(
//     this,
//     0,
//     Intent(this, GeofenceTransitionsIntentService::class.java),
//     PendingIntent.FLAG_UPDATE_CURRENT)

// BroadcastReceiverに変更
val pendingIntent = PendingIntent.getBroadcast(
    this,
    0,
    Intent(this, GeofenceTransitionsBroadcastReceiver::class.java),
    PendingIntent.FLAG_UPDATE_CURRENT)
```

# おわりに

`BroadcastReceiver`を使うことで、OreoでもGeofencingを実装することが出来ました。当時私が詰まっていたときは「公式ドキュメント通りなのになんで動かないんだよ！！！」って感じでした。過信は禁物ですね。

これからGeofencingを実装する方は、ぜひ参考にしてください。ソースコードは[GitHub](https://github.com/shiita0903/Geofences)で公開しています。