Android Kotlin应用中无UI后台路线追踪的Google Navigation SDK实现问题
我需要在Android Kotlin应用中基于Google Navigation SDK实现无用户界面的后台路线追踪,核心需求包括:
- 获取路线与多途经点
- 后台持续监控行程状态
- 获取下一个途经点的剩余时间、距离等导航数据
代码无需放在MainActivity中,但可在MainActivity里调用相关类完成SDK初始化。目前已参照官方教程编写代码,但后台运行功能无法正常工作,拆分SDK初始化逻辑后还出现错误。
以下是我的实现代码:
import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.widget.Toast import androidx.core.content.ContextCompat import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.model.LatLng import com.google.android.libraries.navigation.NavigationApi import com.google.android.libraries.navigation.NavigationView import com.google.android.libraries.navigation.Navigator import com.google.android.libraries.navigation.SimulationOptions import com.google.android.libraries.navigation.Waypoint class NavigationManager( private val context: Context, private val navView: NavigationView, private val onNavigatorReady: (Navigator) -> Unit ) { private var mNavigator: Navigator? = null private var arrivalListener: Navigator.ArrivalListener? = null private var routeChangedListener: Navigator.RouteChangedListener? = null private var navigatorScope: InitializedNavScope? = null private var pendingNavActions = mutableListOf<InitializedNavRunnable>() /** * Initializes the Navigation API and sets up necessary listeners. */ fun initializeNavigation() { // Ensure necessary permissions are granted if (!checkPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) { Toast.makeText(context, "Location permission is required", Toast.LENGTH_LONG).show() return } // Initialize the Navigation SDK NavigationApi.getNavigator(context, object : NavigationApi.NavigatorListener { override fun onNavigatorReady(navigator: Navigator) { val scope = InitializedNavScope(navigator) navigatorScope = scope pendingNavActions.forEach { block -> scope.block() } pendingNavActions.clear() mNavigator = navigator // Set up navigation listeners registerNavigationListeners() // Call the callback when the navigator is ready onNavigatorReady(navigator) // Set up the map to follow the user's location navView.getMapAsync { googleMap -> googleMap.followMyLocation(GoogleMap.CameraPerspective.TILTED) } // Simulate vehicle location in debug mode if (BuildConfig.DEBUG) { mNavigator?.simulator?.setUserLocation(MainActivity.startLocation) } // Initialize the first navigation (e.g., to Trafalgar Square) navigateToPlace(MainActivity.TRAFALGAR_SQUARE) } override fun onError(errorCode: Int) { handleNavigatorError(errorCode) } }) } private fun checkPermissionGranted(permissionToCheck: String): Boolean = ContextCompat.checkSelfPermission(context, permissionToCheck) == PackageManager.PERMISSION_GRANTED private fun navigateToPlace(placeId: String) { val waypoint = try { Waypoint.builder().setPlaceIdString(placeId).build() } catch (e: Waypoint.UnsupportedPlaceIdException) { Toast.makeText(context, "Unsupported place ID", Toast.LENGTH_SHORT).show() return } withNavigatorAsync { val pendingRoute = mNavigator?.setDestination(waypoint) pendingRoute?.setOnResultListener { code -> when (code) { Navigator.RouteStatus.OK -> startGuidance() else -> Toast.makeText(context, "Error starting guidance: $code", Toast.LENGTH_SHORT).show() } } } } private fun startGuidance() { mNavigator?.setAudioGuidance(Navigator.AudioGuidance.VOICE_ALERTS_AND_GUIDANCE) if (BuildConfig.DEBUG) { mNavigator?.simulator?.simulateLocationsAlongExistingRoute( SimulationOptions().speedMultiplier(5f) ) } mNavigator?.startGuidance() } private fun registerNavigationListeners() { withNavigatorAsync { arrivalListener = Navigator.ArrivalListener { Toast.makeText(context, "User has arrived at the destination!", Toast.LENGTH_SHORT).show() mNavigator?.clearDestinations() if (BuildConfig.DEBUG) { mNavigator?.simulator?.unsetUserLocation() } } mNavigator?.addArrivalListener(arrivalListener) routeChangedListener = Navigator.RouteChangedListener { Toast.makeText(context, "Route changed", Toast.LENGTH_SHORT).show() } mNavigator?.addRouteChangedListener(routeChangedListener) } } private fun withNavigatorAsync(block: InitializedNavRunnable) { val navigatorScope = navigatorScope if (navigatorScope != null) { navigatorScope.block() } else { pendingNavActions.add(block) } } private fun handleNavigatorError(errorCode: Int) { when (errorCode) { NavigationApi.ErrorCode.NOT_AUTHORIZED -> { Toast.makeText(context, "Error: API key is not authorized", Toast.LENGTH_LONG).show() } NavigationApi.ErrorCode.TERMS_NOT_ACCEPTED -> { Toast.makeText(context, "Error: Terms not accepted", Toast.LENGTH_LONG).show() } else -> { Toast.makeText(context, "Error: $errorCode", Toast.LENGTH_LONG).show() } } } fun onStop() { mNavigator?.clearDestinations() mNavigator?.cleanup() arrivalListener?.let { mNavigator?.removeArrivalListener(it) } routeChangedListener?.let { mNavigator?.removeRouteChangedListener(it) } } fun onSaveInstanceState(savedInstanceState: Bundle) { navView.onSaveInstanceState(savedInstanceState) } fun onStart() { navView.onStart() } fun onResume() { navView.onResume() } fun onPause() { navView.onPause() } fun onStop() { navView.onStop() } fun onDestroy() { navView.onDestroy() onStop() // Clean up navigator listeners and resources } } // 补充代码中缺失的定义 private typealias InitializedNavRunnable = () -> Unit private class InitializedNavScope(val navigator: Navigator) { fun block() = Unit }
问题分析与修复方案
核心问题点
- 依赖带UI的NavigationView:当前代码绑定了
NavigationView组件,这是用于展示导航UI的控件,完全不符合无界面后台的需求,且会在应用退到后台时因UI生命周期销毁导致导航中断。 - 后台存活机制缺失:Android 8.0+对后台进程有严格限制,无界面进程会被系统快速回收,必须用前台服务保持进程存活。
- 权限不足:后台定位需要
ACCESS_BACKGROUND_LOCATION权限(Android 10+),当前仅检查了前台定位权限。 - 缺少进度监听:未实现
ProgressListener,无法获取下一个途经点的剩余时间、距离等核心数据。
修复后的实现
1. 重构NavigationManager,移除UI依赖
import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.os.Build import android.widget.Toast import androidx.core.content.ContextCompat import com.google.android.libraries.navigation.NavigationApi import com.google.android.libraries.navigation.Navigator import com.google.android.libraries.navigation.SimulationOptions import com.google.android.libraries.navigation.Waypoint private typealias InitializedNavRunnable = () -> Unit private class InitializedNavScope(val navigator: Navigator) class BackgroundNavigationManager( private val context: Context, private val onProgressUpdate: (remainingTimeMs: Long, remainingDistanceM: Int) -> Unit ) { private var mNavigator: Navigator? = null private var arrivalListener: Navigator.ArrivalListener? = null private var progressListener: Navigator.ProgressListener? = null private var navigatorScope: InitializedNavScope? = null private var pendingNavActions = mutableListOf<InitializedNavRunnable>() fun initializeNavigation() { if (!checkRequiredPermissions()) { Toast.makeText(context, "需要定位权限(含后台定位)", Toast.LENGTH_LONG).show() return } NavigationApi.getNavigator(context, object : NavigationApi.NavigatorListener { override fun onNavigatorReady(navigator: Navigator) { navigatorScope = InitializedNavScope(navigator) pendingNavActions.forEach { it.invoke() } pendingNavActions.clear() mNavigator = navigator registerCoreListeners() if (BuildConfig.DEBUG) { mNavigator?.simulator?.setUserLocation(LatLng(51.5074, -0.1278)) } } override fun onError(errorCode: Int) { val errorMsg = when (errorCode) { NavigationApi.ErrorCode.NOT_AUTHORIZED -> "API密钥未授权" NavigationApi.ErrorCode.TERMS_NOT_ACCEPTED -> "未接受服务条款" else -> "导航初始化错误:$errorCode" } Toast.makeText(context, errorMsg, Toast.LENGTH_LONG).show() } }) } private fun checkRequiredPermissions(): Boolean { val fineLocation = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED val backgroundLocation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED } else { true } return fineLocation && backgroundLocation } // 设置多途经点路线 fun setMultiWaypointRoute(waypoints: List<Waypoint>) { withNavigatorAsync { mNavigator?.clearDestinations() waypoints.forEach { mNavigator?.addWaypoint(it) } waypoints.lastOrNull()?.let { destination -> mNavigator?.setDestination(destination)?.setOnResultListener { code -> if (code == Navigator.RouteStatus.OK) startBackgroundGuidance() else Toast.makeText(context, "路线设置失败:$code", Toast.LENGTH_SHORT).show() } } } } private fun startBackgroundGuidance() { mNavigator?.setAudioGuidance(Navigator.AudioGuidance.MUTED) if (BuildConfig.DEBUG) { mNavigator?.simulator?.simulateLocationsAlongExistingRoute( SimulationOptions().speedMultiplier(5f) ) } mNavigator?.startGuidance() } private fun registerCoreListeners() { withNavigatorAsync { // 到达途经点监听 arrivalListener = Navigator.ArrivalListener { waypoint -> Toast.makeText(context, "到达途经点:${waypoint.placeId}", Toast.LENGTH_SHORT).show() } mNavigator?.addArrivalListener(arrivalListener) // 导航进度监听:获取剩余时间、距离 progressListener = Navigator.ProgressListener { progress -> onProgressUpdate(progress.remainingTimeMillis, progress.remainingDistanceMeters) } mNavigator?.addProgressListener(progressListener) } } private fun withNavigatorAsync(block: InitializedNavRunnable) { navigatorScope?.let { block.invoke() } ?: pendingNavActions.add(block) } // 清理资源 fun cleanup() { mNavigator?.let { navigator -> arrivalListener?.let { navigator.removeArrivalListener(it) } progressListener?.let { navigator.removeProgressListener(it) } navigator.clearDestinations() navigator.cleanup() } mNavigator = null } }
2. 实现前台服务保持后台存活
import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service import android.content.Intent import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat class NavigationForegroundService : Service() { private lateinit var navigationManager: BackgroundNavigationManager override fun onCreate() { super.onCreate() createNotificationChannel() startForeground(NOTIFICATION_ID, buildNavigationNotification()) navigationManager = BackgroundNavigationManager(this) { remainingTimeMs, remainingDistanceM -> // 处理导航进度更新,比如发送通知或保存到本地 updateNotification(remainingTimeMs, remainingDistanceM) } navigationManager.initializeNavigation() // 示例:设置多途经点路线 val waypoints = listOf( Waypoint.builder().setPlaceIdString("途经点ID1").build(), Waypoint.builder().setPlaceIdString("终点ID").build() ) navigationManager.setMultiWaypointRoute(waypoints) } private fun buildNavigationNotification(): Notification { return NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("后台导航中") .setContentText("监控路线进度") .setSmallIcon(R.drawable.ic_navigation) .setPriority(NotificationCompat.PRIORITY_LOW) .build() } private fun updateNotification(remainingTimeMs: Long, remainingDistanceM: Int) { val timeStr = "${remainingTimeMs / 60000}分钟" val distanceStr = "${remainingDistanceM / 1000}公里" val notification = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("后台导航中") .setContentText("剩余:$timeStr / $distanceStr") .setSmallIcon(R.drawable.ic_navigation) .setPriority(NotificationCompat.PRIORITY_LOW) .build() val manager = getSystemService(NotificationManager::class.java) manager.notify(NOTIFICATION_ID, notification) } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( CHANNEL_ID, "后台导航", NotificationManager.IMPORTANCE_LOW ) getSystemService(NotificationManager::class.java).createNotificationChannel(channel) } } override fun onBind(intent: Intent?): IBinder? = null override fun onDestroy() { super.onDestroy() navigationManager.cleanup() } companion object { private const val CHANNEL_ID = "BackgroundNavigationChannel" private const val NOTIFICATION_ID = 1001 } }
3. 在MainActivity中启动服务
import android.content.Intent import androidx.appcompat.app.AppCompatActivity import android.os.Bundle class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 启动后台导航服务 startService(Intent(this, NavigationForegroundService::class.java)) } }
4. 配置权限(AndroidManifest.xml)
<!-- 定位权限 --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- Android 10+ 后台定位权限 --> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <!-- 前台服务权限 --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> <!-- 注册服务 --> <service android:name=".NavigationForegroundService" />
关键注意事项
- 权限申请:必须主动向用户申请
ACCESS_FINE_LOCATION和ACCESS_BACKGROUND_LOCATION权限,仅在Manifest声明无效。 - 进程存活:前台服务是后台导航的必要条件,否则系统会在应用退到后台后快速回收进程。
- 资源清理:服务销毁时必须调用
navigator.cleanup(),避免内存泄漏。 - 进度监听:通过
ProgressListener可以实时获取下一个途经点的剩余时间、距离,满足核心需求。
内容的提问来源于stack exchange,提问作者Geno Yves Cadiot
相关产品推荐
相关产品推荐

