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

Android Kotlin应用中无UI后台路线追踪的Google Navigation SDK实现问题

问题: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
}

问题分析与修复方案

核心问题点

  1. 依赖带UI的NavigationView:当前代码绑定了NavigationView组件,这是用于展示导航UI的控件,完全不符合无界面后台的需求,且会在应用退到后台时因UI生命周期销毁导致导航中断。
  2. 后台存活机制缺失:Android 8.0+对后台进程有严格限制,无界面进程会被系统快速回收,必须用前台服务保持进程存活。
  3. 权限不足:后台定位需要ACCESS_BACKGROUND_LOCATION权限(Android 10+),当前仅检查了前台定位权限。
  4. 缺少进度监听:未实现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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 00:05:55