You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android平台Geofence无触发问题排查(Kotlin实现)

Geofencing无法接收过渡事件的排查方案

问题描述

已创建后台运行的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")忽略了权限检查,可能实际并未申请到必要权限,尤其是后台位置权限。

修复:

  1. 在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" />
  1. 在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模拟功能模拟位置变化:

  1. 打开Android Studio的App Inspection -> Location
  2. 设置一个在Geofence区域外的位置,等待后再设置区域内的位置,观察是否触发ENTER事件

验证步骤

  1. 修复以上问题后,重新安装App
  2. 授权位置权限(包括后台权限)
  3. 启动Geofencing Service
  4. 模拟位置变化或实际移动,查看Logcat中是否有ENTER/EXIT的日志输出

内容的提问来源于stack exchange,提问作者SirHammouD

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 03:54:30