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

Android Geofence真机无法触发,模拟器正常的排查求助

Geofence真机无法触发,仅模拟器正常的排查方案

我可以成功添加Geofence,但物理设备上从未触发,模拟器能正常工作。已严格遵循Android官方文档实现,获取了ACCESS_FINE_LOCATION与ACCESS_BACKGROUND_LOCATION权限,Manifest中注册了GeofenceBroadcastReceiver,模拟器上onReceive函数正常调用,但真机无响应,且未开启省电模式。已花费一周排查,求指出明显遗漏点?


更新:简化实现代码

package com.plutoapps.permissions

import android.Manifest
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.Geofence
import com.google.android.gms.location.GeofenceStatusCodes
import com.google.android.gms.location.GeofencingClient
import com.google.android.gms.location.GeofencingEvent
import com.google.android.gms.location.GeofencingRequest
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import com.plutoapps.permissions.ui.theme.PermissionsTheme

const val TAG = "GeofenceApp"

class MainActivity : ComponentActivity() {

    private lateinit var fusedClient: FusedLocationProviderClient
    private lateinit var geofencingClient: GeofencingClient

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        fusedClient = LocationServices.getFusedLocationProviderClient(this)
        geofencingClient = LocationServices.getGeofencingClient(this)
        val geofencePendingIntent: PendingIntent by lazy {
            val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
            PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_MUTABLE)
        }

        setContent {
            PermissionsTheme {
                // A surface container using the 'background' color from the theme
                PermissionsApp(
                    fusedClient = fusedClient,
                    geofencingClient = geofencingClient,
                    geofencePendingIntent = geofencePendingIntent
                )
            }
        }
    }
}


@SuppressLint("MissingPermission")
@Composable
fun PermissionsApp(
    modifier: Modifier = Modifier,
    fusedClient: FusedLocationProviderClient,
    geofencingClient: GeofencingClient,
    geofencePendingIntent: PendingIntent
) {

    val context = LocalContext.current

    val listOfPermissions = listOfNotNull(
        Manifest.permission.ACCESS_FINE_LOCATION,
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) Manifest.permission.POST_NOTIFICATIONS else null,
    )

    var areAllPermissionsGranted by remember {
        mutableStateOf(false)
    }
    var isBackgroundLocationPermissionsGranted by remember {
        mutableStateOf(
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ContextCompat.checkSelfPermission(
                context, Manifest.permission.ACCESS_BACKGROUND_LOCATION
            ) == PackageManager.PERMISSION_GRANTED else true
        )
    }

    val backgroundLocationPermissionsLauncher =
        rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission(),
            onResult = { isGranted ->
                isBackgroundLocationPermissionsGranted = isGranted
            })

    val allPermissionsLauncher =
        rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestMultiplePermissions(),
            onResult = { results ->
                areAllPermissionsGranted = !results.containsValue(false)
                if (results.containsValue(false)) {

                } else {
                    areAllPermissionsGranted = true
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        backgroundLocationPermissionsLauncher.launch(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
                    }

                }
            })

    LaunchedEffect(key1 = "123") {
        if (!areAllPermissionsGranted) {
            allPermissionsLauncher.launch(listOfPermissions.toTypedArray())
        }
    }

    Scaffold { paddingValues ->
        Column(
            modifier = modifier
                .padding(paddingValues)
                .fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Button(onClick = {
                if (areAllPermissionsGranted && isBackgroundLocationPermissionsGranted) {
                    fusedClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null)
                        .addOnSuccessListener {
                            geofencingClient.addGeofences(GeofencingRequest.Builder().apply {
                                setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
                                addGeofence(
                                    Geofence.Builder().setRequestId("GeoRequest1")
                                        .setCircularRegion(it.latitude, it.longitude, 5f)
                                        .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT or Geofence.GEOFENCE_TRANSITION_DWELL)
                                        .setNotificationResponsiveness(1)
                                        .setExpirationDuration(Geofence.NEVER_EXPIRE)
                                        .setLoiteringDelay(5).build()
                                )
                            }.build(), geofencePendingIntent).addOnSuccessListener {
                                Toast.makeText(context, "Geofence added", Toast.LENGTH_LONG).show()
                            }

                        }
                }
            }) {
                Text(text = "Get Location and set geofence")
            }
        }
    }
}

class GeofenceBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        Log.d(TAG, "received ")
        intent?.apply {
            val geofencingEvent = GeofencingEvent.fromIntent(this)
            if (geofencingEvent?.hasError() == true) {
                val errorMessage =
                    GeofenceStatusCodes.getStatusCodeString(geofencingEvent.errorCode)
                Log.e(TAG, errorMessage)
                return
            }

            // Get the transition type.
            val geofenceTransition = geofencingEvent!!.geofenceTransition
            context?.apply {
                NotificationHelper(this).sendNotification(
                    "GeoGeo",
                    "You ${if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_DWELL) "dwelled" else if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) "exited" else "entered"}"
                )
            }

        }
    }
}

class NotificationHelper(private val context: Context) : ContextWrapper(context) {

    companion object {
        const val channelId = "my_notification_channel"
        const val channelName = "My Notification Channel"
        const val importance = NotificationManager.IMPORTANCE_DEFAULT

    }

    fun sendNotification(title: String, message: String) {
        // Create a notification channel for Android O and above
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(channelId, channelName, importance)
            val notificationManager = context.getSystemService(NotificationManager::class.java)
            notificationManager.createNotificationChannel(channel)
        }

        // Create a notification builder
        val builder = NotificationCompat.Builder(context, channelId)
            .setSmallIcon(R.drawable.baseline_notifications_24) // Replace with your icon
            .setContentTitle(title).setContentText(message)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)

        // Show the notification
        val notificationManager = context.getSystemService(NotificationManager::class.java)
        notificationManager.notify(0, builder.build())
    }

}

Manifest配置

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <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.POST_NOTIFICATIONS" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Permissions"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.Permissions">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <receiver android:name=".GeofenceBroadcastReceiver"
            android:enabled="true"
            android:exported="false"/>
    </application>

</manifest>

排查要点与修复方案

1. Geofence半径过小

你设置的Geofence半径是5f(5米),真机的GPS精度在户外通常是3-10米,室内可能更差,导致设备无法准确触发边界穿越。建议先把半径调整到20-50米测试,确认触发逻辑正常后再根据需求缩小。

2. PendingIntent参数问题

虽然你用了FLAG_MUTABLE,但对于Android 12+,建议明确指定PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,避免系统回收或识别异常:

val geofencePendingIntent: PendingIntent by lazy {
    val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
    PendingIntent.getBroadcast(
        this, 
        0, 
        intent, 
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
    )
}

3. 系统后台定位权限管控

  • 确保设备的位置模式设置为高精度(GPS+网络),仅用网络定位的话精度不够,无法触发小半径Geofence。
  • 部分国产厂商(如小米、华为、vivo)有额外的后台权限限制,即使你申请了ACCESS_BACKGROUND_LOCATION,也需要在系统设置中给App开启后台活动权限或无限制后台运行,避免App被后台杀死导致Geofence监听失效。

4. Geofence触发参数不合理

  • setLoiteringDelay(5)设置的是5毫秒,这显然不符合实际需求,DWELL的延迟应该至少设置为10000毫秒(10秒),否则系统会忽略该触发条件:
.setLoiteringDelay(10000) // 10秒后触发DWELL
  • 建议先单独测试ENTER和EXIT触发类型,确认基础逻辑正常后再添加DWELL,避免多条件叠加导致排查困难。

5. BroadcastReceiver日志排查

在GeofenceBroadcastReceiver的onReceive中添加详细日志,确认是否有事件被传递:

override fun onReceive(context: Context?, intent: Intent?) {
    Log.d(TAG, "Geofence broadcast received")
    intent?.apply {
        val geofencingEvent = GeofencingEvent.fromIntent(this)
        if (geofencingEvent?.hasError() == true) {
            val errorMessage = GeofenceStatusCodes.getStatusCodeString(geofencingEvent.errorCode)
            Log.e(TAG, "Geofence error: $errorMessage")
            return
        }
        
        geofencingEvent?.let {
            val transition = it.geofenceTransition
            Log.d(TAG, "Transition type: $transition")
            Log.d(TAG, "Triggered geofences count: ${it.triggeringGeofences.size}")
            
            // 原有通知逻辑
            context?.apply {
                val msg = when(transition) {
                    Geofence.GEOFENCE_TRANSITION_DWELL -> "dwelled"
                    Geofence.GEOFENCE_TRANSITION_EXIT -> "exited"
                    else -> "entered"
                }
                NotificationHelper(this).sendNotification("GeoGeo", "You $msg")
            }
        }
    }
}

如果Receiver确实没收到广播,考虑改用WorkManager或前台Service来监听Geofence,部分厂商对静态BroadcastReceiver的后台限制较严格。

6. 测试场景规范

  • 测试时需要实际移动设备穿越Geofence边界,真机模拟位置可能不被系统Geofence服务认可。
  • 避免在室内测试,GPS信号差会导致位置更新不及时,无法触发Geofence。

内容的提问来源于stack exchange,提问作者Michael Appiah-kubi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 22:22:05