Android平台Geofence无触发问题排查(Kotlin实现)
问题描述
已创建后台运行的Service启动Geofencing,搭配BroadcastReceiver处理位置过渡事件。计时器功能在前台、后台及App被杀死状态下均正常运行,但Geofencing始终无法接收到任何过渡事件。已知Android 12有位置更新相关改动,但未收到相关警告。
代码实现
主Activity(按钮控制Service启停)
class MainActivity : AppCompatActivity() { lateinit var btn: Button lateinit var text: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) btn = findViewById(R.id.button) text = findViewById(R.id.text) checkText() btn.setOnClickListener(View.OnClickListener { if (GeofencingService.isServiceStarted){ GeofencingService.isParticipatingOn = false val intent = Intent(applicationContext, GeofencingService::class.java) stopService(intent) Toast.makeText(applicationContext ,"Remove GeoFences", Toast.LENGTH_LONG).show() Log.e("Main", "Geofences service killed") text.text = "Geofencing Stopped" } else{ Toast.makeText(applicationContext ,"Start Foreground", Toast.LENGTH_LONG).show() Log.e("Main", "Geofences service started") val intent = Intent(applicationContext, GeofencingService::class.java) startService(intent) text.text = "Geofencing Started" } }) } private fun checkText(){ if (GeofencingService.isServiceStarted){ text.text = "Geofencing Started" } else{ text.text = "Geofencing Stopped" } } }
Service实现
class GeofencingService: Service() { private var geofencingClient: GeofencingClient? = null companion object{ var geofencelist = mutableListOf<Geofence>() var isServiceStarted = false var isParticipatingOn = false } override fun onCreate() { super.onCreate() Log.e("Main", "Geofences onCreate") isServiceStarted = true isParticipatingOn = true geofencelist.clear() geofencingClient = LocationServices.getGeofencingClient(this) startCustomForeground() } override fun onDestroy() { super.onDestroy() Log.e("Main", "Geofences onDestroy") isServiceStarted = false removeGeoFences() if (isParticipatingOn){ val intent = Intent(this, GeofencingService::class.java) intent.action = "RESTART_GEOFENCE_EVENT" startService(intent) //this.sendBroadcast(intent) } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { //startForegroundService(intent) if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS) { geofencelist.add( Geofence.Builder() .setRequestId("KEY") .setCircularRegion( lat, // lat long, // long 3220f // meters ) .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT) .setExpirationDuration(Geofence.NEVER_EXPIRE) .setNotificationResponsiveness(120000) .build()) timer() addGeofences() } else { Log.e("Service", "Your Device doesn't support Google Play Services.") } return START_NOT_STICKY } override fun onBind(intent: Intent?): IBinder? { TODO("Not yet implemented") return null } @SuppressLint("MissingPermission") fun addGeofences(){ geofencingClient?.addGeofences(getGeofencingRequest(), geofencePendingIntent)?.run { addOnSuccessListener { // Geofences added Log.e("Main", "Geofences added") // ... } addOnFailureListener { // Failed to add geofences // ... } } } @SuppressLint("MissingPermission") fun removeGeoFences(){ geofencingClient?.removeGeofences(geofencePendingIntent)?.run { addOnSuccessListener { // Geofences removed Log.e("Main", "Geofences removed") // ... } addOnFailureListener { // Failed to remove geofences // ... } } } private fun getGeofencingRequest(): GeofencingRequest { return GeofencingRequest.Builder().apply { setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER or GeofencingRequest.INITIAL_TRIGGER_DWELL or GeofencingRequest.INITIAL_TRIGGER_EXIT) addGeofences(geofencelist) }.build() } private val geofencePendingIntent: PendingIntent by lazy { val intent = Intent(this, GeofencingBroadcastReceiver::class.java) intent.action = "START_GEOFENCE_EVENT" // We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when calling // addGeofences() and removeGeofences(). if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S){ PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) }else{ PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } } private fun startCustomForeground() { val CHANNEL_ID = "GEOFENCING" val CHANNEL_NAME = "Geofencing Service" val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_NONE) channel.lightColor = Color.TRANSPARENT channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE val manager = (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager) manager.createNotificationChannel(channel) val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(this, CHANNEL_ID) val notification = notificationBuilder.setOngoing(true) .setContentTitle(CHANNEL_NAME + "is running in background") .setPriority(NotificationManager.IMPORTANCE_NONE) .setCategory(Notification.CATEGORY_SERVICE) .build() startForeground(1, notification) } private fun timer(){ val timer = object: CountDownTimer(200000, 1000) { override fun onTick(millisUntilFinished: Long) { Log.e("Counter: ", millisUntilFinished.toString()) val intent = Intent(applicationContext, GeofencingBroadcastReceiver::class.java) intent.action = "TIMER" applicationContext.sendBroadcast(intent) } override fun onFinish() {} } timer.start() } }
BroadcastReceiver实现
class GeofencingBroadcastReceiver: BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent?.action == "START_GEOFENCE_EVENT"){ val geofencingEvent = GeofencingEvent.fromIntent(intent) if (geofencingEvent.hasError()) { val errorMessage = GeofenceStatusCodes .getStatusCodeString(geofencingEvent.errorCode) Log.e("Broadcaster", errorMessage) return } // Get the transition type. Log.e("Potato", " Geofencing service broadcast started") val geofenceTransition = geofencingEvent.geofenceTransition val triggeringGeofences = geofencingEvent.triggeringLocation // Test that the reported transition was of interest. when (geofenceTransition) { (Geofence.GEOFENCE_TRANSITION_ENTER )-> { Toast.makeText(context,"ENTER - "+ triggeringGeofences.longitude.toString() +" - "+ triggeringGeofences.latitude.toString() ,Toast.LENGTH_LONG).show() Log.i("Potato", "ENTER - Lat: ${triggeringGeofences.latitude} - Long: ${triggeringGeofences.longitude}") } (Geofence.GEOFENCE_TRANSITION_EXIT) -> { Toast.makeText(context,"EXIT - "+ triggeringGeofences.longitude.toString() +" - "+ triggeringGeofences.latitude.toString() ,Toast.LENGTH_LONG).show() Log.i("Potato", "EXIT - Lat: ${triggeringGeofences.latitude} - Long: ${triggeringGeofences.longitude}") } else -> { Log.e("Potato", " ERROR") } } } else if(intent?.action == "RESTART_GEOFENCE_EVENT" ) { if (!GeofencingService.isServiceStarted){ Log.e("Potato", " Geofencing service restarted") val intentService = Intent(context, GeofencingService::class.java) context?.let { ContextCompat.startForegroundService(it,intentService) } } } else if(intent?.action == "TIMER" ) { Log.e("Potato", " TIMER TICKING ++") } } }
Manifest配置
<receiver android:name=".GeofencingBroadcastReceiver" android:enabled="true" android:exported="false"> <intent-filter> <action android:name="GeofencingBroadcastReceiver" /> </intent-filter> </receiver> <service android:name=".GeofencingService" android:enabled="true" android:exported="false" android:foregroundServiceType="location"> </service>
可能的原因及修复方案
1. BroadcastReceiver的Intent Filter不匹配
代码中注册Geofence时,PendingIntent指定的action是START_GEOFENCE_EVENT,但Manifest里给Receiver配置的intent-filter action是GeofencingBroadcastReceiver,两者完全不匹配,导致系统发送的Geofence过渡广播无法被Receiver接收。
修复:
修改Manifest中Receiver的intent-filter,添加正确的action:
<receiver android:name=".GeofencingBroadcastReceiver" android:enabled="true" android:exported="false"> <intent-filter> <action android:name="START_GEOFENCE_EVENT" /> <action android:name="RESTART_GEOFENCE_EVENT" /> <action android:name="TIMER" /> </intent-filter> </receiver>
2. 位置权限缺失
Android 12及以上版本,Geofencing需要以下权限:
ACCESS_FINE_LOCATION:获取精确位置,用于判断Geofence过渡ACCESS_BACKGROUND_LOCATION:允许App在后台获取位置,否则App退到后台后Geofence不会触发
问题点:
代码中用@SuppressLint("MissingPermission")忽略了权限检查,可能实际并未申请到必要权限,尤其是后台位置权限。
修复:
- 在Manifest中添加权限声明:
<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_LOCATION" />
- 在MainActivity中动态申请权限,确保用户授权后再启动Geofencing Service:
private val REQUEST_LOCATION_PERMISSIONS = 1001 private fun requestLocationPermissions() { val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION) } else { arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) } ActivityCompat.requestPermissions(this, permissions, REQUEST_LOCATION_PERMISSIONS) } // 处理权限申请结果 override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == REQUEST_LOCATION_PERMISSIONS) { val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } if (allGranted) { // 权限已授权,启动Geofencing Service val intent = Intent(this, GeofencingService::class.java) startService(intent) text.text = "Geofencing Started" } else { Toast.makeText(this, "需要位置权限才能使用Geofencing功能", Toast.LENGTH_LONG).show() } } }
然后在启动Service前先检查权限,未授权则申请:
btn.setOnClickListener(View.OnClickListener { if (GeofencingService.isServiceStarted){ // 停止Service逻辑不变 } else{ val hasFineLocation = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED val hasBackgroundLocation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED } else true if (hasFineLocation && hasBackgroundLocation) { // 启动Service val intent = Intent(applicationContext, GeofencingService::class.java) startService(intent) text.text = "Geofencing Started" Toast.makeText(applicationContext ,"Start Foreground", Toast.LENGTH_LONG).show() Log.e("Main", "Geofences service started") } else { requestLocationPermissions() } } })
3. Geofence初始触发设置错误
代码中设置了INITIAL_TRIGGER_DWELL,但创建Geofence时并未调用setLoiteringDelay()设置逗留时间,这会导致DWELL事件无法触发,虽然不影响ENTER/EXIT,但可能干扰系统的触发逻辑。
修复:
移除不必要的初始触发类型,或者添加逗留时间设置:
// 方案1:移除DWELL初始触发 private fun getGeofencingRequest(): GeofencingRequest { return GeofencingRequest.Builder().apply { setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER or GeofencingRequest.INITIAL_TRIGGER_EXIT) addGeofences(geofencelist) }.build() } // 方案2:添加逗留时间(如果需要DWELL事件) Geofence.Builder() .setRequestId("KEY") .setCircularRegion(lat, long, 3220f) .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT or Geofence.GEOFENCE_TRANSITION_DWELL) .setExpirationDuration(Geofence.NEVER_EXPIRE) .setNotificationResponsiveness(120000) .setLoiteringDelay(30000) // 设置逗留30秒后触发DWELL事件 .build()
4. Service重启逻辑问题
在Service的onDestroy()中,尝试通过startService(intent)重启Service,但此时App可能处于后台,Android 12+对后台启动Service有严格限制,应该使用ContextCompat.startForegroundService(),并且确保有足够的权限。
修复:
修改onDestroy()中的重启逻辑:
override fun onDestroy() { super.onDestroy() Log.e("Main", "Geofences onDestroy") isServiceStarted = false removeGeoFences() if (isParticipatingOn){ val intentService = Intent(this, GeofencingService::class.java) ContextCompat.startForegroundService(this, intentService) Log.e("Main", "Geofences service restart initiated") } }
5. 测试场景问题
确保测试时真实移动到Geofence区域内外,或者使用Android Studio的Location模拟功能模拟位置变化:
- 打开Android Studio的
App Inspection->Location - 设置一个在Geofence区域外的位置,等待后再设置区域内的位置,观察是否触发ENTER事件
验证步骤
- 修复以上问题后,重新安装App
- 授权位置权限(包括后台权限)
- 启动Geofencing Service
- 模拟位置变化或实际移动,查看Logcat中是否有
ENTER/EXIT的日志输出
内容的提问来源于stack exchange,提问作者SirHammouD

