2

From Android 11 I learned there are some restrictions related to background location, but from the documentation is not very clear to me if this affects a ForegroundService which has the foregroundServiceType="location" declared in the AndroidManifest.xml file.

This part of the documentation is confusing for me:

"If your app starts a foreground service while the app is running in the foreground ("while-in-use"), the service has the following access restrictions:

If the user has granted the ACCESS_BACKGROUND_LOCATION permission to your app, the service can access location all the time. Otherwise, if the user has granted the ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION permission to your app, the service has access to location only while the app is running in the foreground (also known as "while-in-use access to location")."

So, if I need background location access is it safe to use only the ForegroundService with type "location" for Android 11 or it is still mandatory to add the ACCESS_BACKGROUND_LOCATION permission?

NOTE: I created a sample project with ForegroundService declared with type "location" for target SDK 30 and seems to work without the background location permission (I receive the location updates every 2 seconds while in background) and this is why I am confused about this. I run the app on Pixel 4 with Android 11.

This is the sample project:

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>

LocationService

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
}

MainActivity

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")
            }
        }
    }
}
Laura
  • 2,653
  • 7
  • 37
  • 59
  • You should have both – tyczj Mar 24 '21 at 15:13
  • I created a sample project with target SDK 30 and seems to work without the background location and this is why I am confused. – Laura Mar 24 '21 at 15:14
  • Not mandatory but in order to have the desired outcome you should have both. – Eyosiyas Mar 24 '21 at 15:15
  • Does your app request location updates in the background? – Eyosiyas Mar 24 '21 at 15:17
  • No, it does not. – sdex Mar 24 '21 at 15:23
  • Let @Laura answer that. – Eyosiyas Mar 24 '21 at 15:28
  • @Eyosiyas I updated the post with the code. I request location updates from the service so I receive location updates while the app is in background. – Laura Mar 24 '21 at 15:34
  • Oh Ok. I did a project like this kind but the Target SDK was 29(Android 10). In that particular case, I used both foreground and background permissions. ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, FOREGROUND_SERVICE, ACCESS_BACKGROUND_LOCATION – Eyosiyas Mar 24 '21 at 15:41
  • @Eyosiyas starting with Android 11 it is not allowed to add the background permission request together with the foreground permissions. We have to request first the foreground permissions and then request the background, this is why I would not call the background permission if it's not necessary. The sample project seems to work but the documentation is a bit misleading for me. I want to be sure it is ok not to request the background permission. – Laura Mar 24 '21 at 15:46
  • 1
    Declaring it won't affect that. I didn't say anything about requesting at the same time. I asked the Android dev team about Android 11 and location permissions and Fred answered it back on August 11, 2020. He made it perfectly clear that you can't ask for both permissions at the same time the Android framework ignores it. – Eyosiyas Mar 24 '21 at 15:52

0 Answers0