首页 > 解决方案 > Android ForegroundService 用于后台定位

问题描述

Android 11我了解到有一些与背景位置相关的限制,但是从文档中我不太清楚这是否会影响在文件ForegroundServiceforegroundServiceType="location"声明的AndroidManifest.xml文件。

这部分文档让我感到困惑:

“如果您的应用程序在前台运行时(“使用时”)启动了前台服务,则该服务具有以下访问限制:

如果用户已授予您的应用 ACCESS_BACKGROUND_LOCATION 权限,则该服务可以随时访问位置。否则,如果用户已向您的应用授予 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限,则该服务仅在应用在前台运行时才能访问位置(也称为“在使用中访问位置”)。

那么,如果我需要后台位置访问权限,仅使用Android 11ForegroundService的with 类型"location"是否安全,或者仍然必须添加权限?ACCESS_BACKGROUND_LOCATION

注意:我创建了一个示例项目,其中 ForegroundService 声明了目标 SDK 30 的“位置”类型,并且似乎在没有后台位置权限的情况下工作(我在后台每 2 秒收到一次位置更新),这就是为什么我对此感到困惑. 我在装有 Android 11 的 Pixel 4 上运行该应用程序。

这是示例项目:

AndroidManifest.xml

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

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.LocationForegroundService">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".LocationService"
            android:enabled="true"
            android:exported="true"
            android:foregroundServiceType="location"/>
    </application>
</manifest>

定位服务

class LocationService : Service() {
private var context: Context? = null
private var settingsClient: SettingsClient? = null
private var locationSettingsRequest: LocationSettingsRequest? = null
private var locationManager: LocationManager? = null
private var locationRequest: LocationRequest? = null
private var notificationManager: NotificationManager? = null
private var fusedLocationClient: FusedLocationProviderClient? = null
private val binder: IBinder = LocalBinder()
private var locationCallback: LocationCallback? = null
private var location: Location? = null

override fun onBind(intent: Intent?): IBinder {
    // Called when a client (MainActivity in case of this sample) comes to the foreground
    // and binds with this service. The service should cease to be a foreground service
    // when that happens.
    Log.i(TAG, "in onBind()")
    return binder
}

override fun onCreate() {
    super.onCreate()

    context = this
    fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)

    createLocationRequest()

    locationCallback = object : LocationCallback() {
        @RequiresApi(Build.VERSION_CODES.O)
        override fun onLocationResult(locationResult: LocationResult) {
            super.onLocationResult(locationResult)

            for (location in locationResult.locations) {
                onNewLocation(location)
            }
        }
    }

    val handlerThread = HandlerThread(TAG)
    handlerThread.start()

    notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager?

    // Android O requires a Notification Channel.
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val name: CharSequence = "service"
        val mChannel = NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_DEFAULT)

        // Set the Notification Channel for the Notification Manager.
        notificationManager?.createNotificationChannel(mChannel)
    }
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    Log.i(TAG, "Service started")
    val startedFromNotification =
        intent?.getBooleanExtra(EXTRA_STARTED_FROM_NOTIFICATION, false)

    // We got here because the user decided to remove location updates from the notification.
    if (startedFromNotification == true) {
        removeLocationUpdates()
        stopSelf()
    }
    // Tells the system to not try to recreate the service after it has been killed.
    return START_NOT_STICKY
}

/**
 * Returns the [NotificationCompat] used as part of the foreground service.
 */
private val notification: Notification
    private get() {
        val intent = Intent(this, LocationService::class.java)

        // Extra to help us figure out if we arrived in onStartCommand via the notification or not.
        intent.putExtra(EXTRA_STARTED_FROM_NOTIFICATION, true)

        // The PendingIntent that leads to a call to onStartCommand() in this service.
        val servicePendingIntent =
            PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)

        // The PendingIntent to launch activity.
        val activityPendingIntent =
            PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java), 0)
        val builder = NotificationCompat.Builder(this)
            .addAction(R.drawable.ic_delete, "title", activityPendingIntent)
            .addAction(R.drawable.ic_delete, "remove", servicePendingIntent)
            .setContentTitle("location title").setOngoing(true)
            .setPriority(Notification.PRIORITY_HIGH).setSmallIcon(R.drawable.btn_dialog)
            .setWhen(System.currentTimeMillis())


        // Set the Channel ID for Android O.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            builder.setChannelId(CHANNEL_ID) // Channel ID
        }
        return builder.build()
    }

/**
 * Makes a request for location updates. Note that in this sample we merely log the
 * [SecurityException].
 */
fun requestLocationUpdates() {
    Log.i(TAG, "Requesting location updates")

    startForeground(NOTIFICATION_ID, notification)
    try {
        fusedLocationClient?.requestLocationUpdates(locationRequest, locationCallback, null)
    } catch (unlikely: SecurityException) {
        Log.e(TAG, "Lost location permission. Could not request updates. $unlikely")
    }
}

@RequiresApi(Build.VERSION_CODES.O)
private fun onNewLocation(location: Location) {
    Log.i(TAG, "New location ${LocalDateTime.now()}: $location")
    this.location = location

    // Notify anyone listening for broadcasts about the new location.
    val intent = Intent(ACTION_BROADCAST)
    intent.putExtra(EXTRA_LOCATION, location)
    LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)

    // Update notification content if running as a foreground service.
    if (serviceIsRunningInForeground(this)) {
        notificationManager?.notify(NOTIFICATION_ID, notification)
    }
}


/**
 * Sets the location request parameters.
 */
private fun createLocationRequest() {
    locationManager = context?.getSystemService(LOCATION_SERVICE) as LocationManager
    settingsClient = LocationServices.getSettingsClient(context)

    locationRequest = LocationRequest.create()
    locationRequest?.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
    locationRequest?.interval = 1000
    locationRequest?.fastestInterval = 1000

    val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
    locationSettingsRequest = builder.build()

    builder.setAlwaysShow(true) //this is the key ingredient
}

/**
 * Removes location updates. Note that in this sample we merely log the
 * [SecurityException].
 */
fun removeLocationUpdates() {
    Log.i(TAG, "Removing location updates")
    try {
        fusedLocationClient?.removeLocationUpdates(locationCallback)
        stopSelf()
    } catch (unlikely: SecurityException) {
        Log.e(TAG, "Lost location permission. Could not remove updates. $unlikely")
    }
}

/**
 * Class used for the client Binder.  Since this service runs in the same process as its
 * clients, we don't need to deal with IPC.
 */
inner class LocalBinder : Binder() {
    val service: LocationService
        get() = this@LocationService
}

/**
 * Returns true if this is a foreground service.
 *
 * @param context The [Context].
 */
fun serviceIsRunningInForeground(context: Context): Boolean {
    val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    for (service in manager.getRunningServices(Int.MAX_VALUE)) {
        if (javaClass.name == service.service.className) {
            if (service.foreground) {
                return true
            }
        }
    }
    return false
}

companion object {
    private const val PACKAGE_NAME = "com.example.locationforegroundservice"
    private val TAG = "TEST"

    /**
     * The name of the channel for notifications.
     */
    private const val CHANNEL_ID = "channel_01"
    const val ACTION_BROADCAST = PACKAGE_NAME + ".broadcast"
    const val EXTRA_LOCATION = PACKAGE_NAME + ".location"
    private const val EXTRA_STARTED_FROM_NOTIFICATION =
        PACKAGE_NAME + ".started_from_notification"

    /**
     * The desired interval for location updates. Inexact. Updates may be more or less frequent.
     */
    private const val UPDATE_INTERVAL_IN_MILLISECONDS: Long = 1000

    /**
     * The fastest rate for active location updates. Updates will never be more frequent
     * than this value.
     */
    private const val FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS =
        UPDATE_INTERVAL_IN_MILLISECONDS / 2

    /**
     * The identifier for the notification displayed for the foreground service.
     */
    private const val NOTIFICATION_ID = 12345678
}

主要活动

class MainActivity : AppCompatActivity() {
    private val TAG = "TEST"
    private val FOREGROUND_LOCATION_CODE = 2

    // The BroadcastReceiver used to listen from broadcasts from the service.
    private var myReceiver: MyReceiver? = null

    // A reference to the service used to get location updates.
    private var mService: LocationService? = null

    // Monitors the state of the connection to the service.
    private val mServiceConnection: ServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            val binder: LocationService.LocalBinder = service as LocationService.LocalBinder
            mService = binder.service
        }

        override fun onServiceDisconnected(name: ComponentName) {
            mService = null
        }
    }

    @RequiresApi(Build.VERSION_CODES.M)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        checkForegroundLocationPermission()

        myReceiver = MyReceiver()

        myReceiver?.let {
            LocalBroadcastManager.getInstance(this)
                .registerReceiver(it, IntentFilter(LocationService.ACTION_BROADCAST))
        }

        findViewById<Button>(R.id.start).setOnClickListener { view ->
            Snackbar.make(view, "Start listening...", Snackbar.LENGTH_LONG).show()

            Log.d("TEST", "Start listening...")

            mService?.requestLocationUpdates();
        }

        findViewById<Button>(R.id.stop).setOnClickListener { view ->
            Snackbar.make(view, "Stop listening...", Snackbar.LENGTH_LONG).show()

            Log.d("TEST", "Stop listening...")

            mService?.removeLocationUpdates()
        }
    }

    override fun onStart() {
        super.onStart()

        // Bind to the service. If the service is in foreground mode, this signals to the service
        // that since this activity is in the foreground, the service can exit foreground mode.
        // Bind to the service. If the service is in foreground mode, this signals to the service
        // that since this activity is in the foreground, the service can exit foreground mode.
        Intent(this, LocationService::class.java).also {
            bindService(it, mServiceConnection, BIND_AUTO_CREATE)
        }
    }

    override fun onResume() {
        super.onResume()

        Log.d(TAG, "onResume")
    }

    override fun onStop() {
        Log.d(TAG, "onStop")

        super.onStop()
    }

    @RequiresApi(Build.VERSION_CODES.M)
    private fun checkForegroundLocationPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            // Check if permission is not granted
            Log.d(TAG, "Permission for foreground location is not granted")

            requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                FOREGROUND_LOCATION_CODE)
        } else {
            // Permission is already granted, do your magic here!
            Toast.makeText(this, "Permission granted", Toast.LENGTH_SHORT).show()
        }
    }

    @RequiresApi(Build.VERSION_CODES.Q)
    override fun onRequestPermissionsResult(requestCode: Int,
                                            permissions: Array<out String>,
                                            grantResults: IntArray) {
        when (requestCode) {
            FOREGROUND_LOCATION_CODE -> {
                Log.d(TAG, "onRequestPermissionsResult ->  FOREGROUND_LOCATION_CODE")
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(this, "Foreground Permission granted", Toast.LENGTH_SHORT).show()
                } else {
                    Toast.makeText(this, "Foreground Permission denied", Toast.LENGTH_SHORT).show()
                }

                return
            }
        }
    }

    private class MyReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            val location: Location? = intent.getParcelableExtra(LocationService.EXTRA_LOCATION)
            if (location != null) {
                Log.d("TEST", "Location = $location")
            }
        }
    }
}

标签: androidbackgroundlocation

解决方案


推荐阅读