Edited at
AndroidDay 3

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


はじめに

この記事は、Android Advent Calendar 2018の3日目の記事です。

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


Geofencingとは

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

(Create and monitor geofences | Android Developersより引用)


これだとOreoで動かない

とりあえず公式ドキュメントを参考にしながらGeofencingを実装していきます。

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

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

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"
}
}

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

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

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に偽装すると

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

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

たくさんあるログの中から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


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



解決方法

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

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")
}
}
}

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

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

// 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で公開しています。