I have a React-Native application that needs to use GPS location. Below there is a LocationFetcher class that is responsivle for getting current location euther by forcing new location or getting the last one.
Let's say getLocation() method is being invoked at the fixed interval. When I turn off the GPS I would get the messages No location provider found., which is fine. But when I turn the GPS on I would get no messages at all. What is more the promise is neither resolved nor rejected. I could toggle GPS many times and let's say on about 5th time all the promises would be resolved with a current location, then I repeat the process and again and have no location at all.
Sometimes launching another app that uses GPS, for example GPS Viewer would instantly resolve all the promises and sometimes it doesn't. Often toggling GPS on and off would cause no issues and sometimes it would many. Sometimes turning off the Network would have been causing this problem for like 15 minutes and later on it would have no affect.
Did anyone have such problems with GPS?
class LocationFetcher(
val context: Context
) {
companion object {
val LOG_TAG = "LOC_FETCHER"
}
/**
* Gets a location "synchronously" as a Promise
*/
fun getLocation(forceNewLocation: Boolean, promise: Promise) {
try {
if (!areProvidersAvailable()) {
promise.reject(NATIVE_ERROR, "No location provider found.")
return
}
if (!checkForPlayServices()) {
promise.reject(NATIVE_ERROR, "Install Google Play Services First and Try Again.")
return
}
if (!hasPermissions()) {
promise.reject(NATIVE_ERROR, "Appropriate permissions not given.")
return
}
/* --------- */
if (forceNewLocation) {
forceSingleGPSLocationUpdate(promise)
return
}
getLastGPSLocation(promise)
} catch (ex: Exception) {
Log.e(TAG, "Native Location Module ERR - " + ex.toString())
promise.reject(NATIVE_ERROR, ex.toString())
}
}
#RequiresPermission(
anyOf = [
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
]
)
fun getLastGPSLocation(
promise: Promise
) {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
if (locationManager === null) {
Log.e(LOG_TAG, "Location Manager is null")
promise.reject(LOG_TAG, Exception("Location Manager is null"))
return
}
try {
val lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (lastKnownLocation === null) {
Log.e(LOG_TAG, "Last known location is null")
promise.reject(LOG_TAG, "Last known location is null");
return
}
Log.v(LOG_TAG, "Resolving promise with location")
promise.resolve(convertLocationToJSON(lastKnownLocation))
} catch (e: SecurityException) {
Log.e(LOG_TAG, e.message, e)
promise.reject(LOG_TAG, e)
return
} catch (e: Exception) {
Log.e(LOG_TAG, e.message, e)
promise.reject(LOG_TAG, e)
return
}
}
#RequiresPermission(
anyOf = [
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
]
)
fun forceSingleGPSLocationUpdate(
promise: Promise
) {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
if (locationManager === null) {
Log.e(LOG_TAG, "Location Manager is null")
promise.reject(LOG_TAG, Exception("Location Manager is null"))
return
}
try {
val locationListener = object : LocationListener {
override fun onLocationChanged(location: Location?) {
if (location === null) {
Log.e(LOG_TAG, "Location changed is null")
promise.reject(LOG_TAG, Exception("Location changed is null"))
return
}
Log.v(LOG_TAG, "Resolving promise with location")
promise.resolve(convertLocationToJSON(location))
}
override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}
locationManager.requestSingleUpdate(LocationManager.GPS_PROVIDER, locationListener, null)
} catch (e: SecurityException) {
Log.e(LOG_TAG, e.message, e)
promise.reject(LOG_TAG, e)
return
} catch (e: Exception) {
Log.e(LOG_TAG, e.message, e)
promise.reject(LOG_TAG, e)
return
}
}
fun areProvidersAvailable(): Boolean {
val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
return try {
lm.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
} catch (ex: Exception) {
Log.e(LOG_TAG, ex.toString())
false
}
}
fun hasPermissions(): Boolean {
return ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
|| ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
// ~ https://stackoverflow.com/questions/
// 22493465/check-if-correct-google-play-service-available-unfortunately-application-has-s
internal fun checkForPlayServices(): Boolean {
val googleApiAvailability = GoogleApiAvailability.getInstance()
val resultCode = googleApiAvailability.isGooglePlayServicesAvailable(context)
if (resultCode != ConnectionResult.SUCCESS) {
if (googleApiAvailability.isUserResolvableError(resultCode)) {
val map = WritableNativeMap().also {
it.putInt("resultCode", resultCode)
it.putInt("resolutionRequest", PLAY_SERVICES_RESOLUTION_REQUEST)
}
sendLocalEventToModule(LOCAL_PLAY_SERVICES_ERROR, map)
}
return false
}
return true
}
internal fun convertLocationToJSON(l: Location?): WritableMap {
if (l === null) {
return WritableNativeMap().also {
it.putString("error", "Received location was null")
}
}
return WritableNativeMap().also {
it.putDouble("latitude", l.latitude)
it.putDouble("longitude", l.longitude)
it.putDouble("accuracy", l.accuracy.toDouble())
it.putDouble("altitude", l.altitude)
it.putDouble("bearing", l.bearing.toDouble())
it.putString("provider", l.provider)
it.putDouble("speed", l.speed.toDouble())
it.putString("timestamp", l.time.toString())
}
}
internal fun sendLocalEventToModule(eventName: String, data: WritableMap) {
val intent = Intent(eventName).also {
it.putExtra("data", WritableMapWrapper(data))
}
Log.v(TAG, "Sending local event ${eventName}")
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
}
}
I don't know if this is important, but we are getting location via Foreground Service, which is presented below.
class ForegroundLocationService : Service() {
lateinit var locationFetcher : LocationFetcher
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_NOT_STICKY
}
override fun onTaskRemoved(rootIntent: Intent?) {
stopSelf()
}
override fun onCreate() {
super.onCreate()
locationFetcher = LocationFetcher(applicationContext)
/**
* Saving this [ForegroundLocationService] reference to the static variable,
* because when binding a Service using [bindService] it would not stop the
* this service, even though the app would close
*/
LocationModule.foregroundLocationService = this
showNotification()
Log.v(TAG, "Creating Foreground Location Service")
}
override fun onDestroy() {
super.onDestroy()
LocationModule.foregroundLocationService = null
locationFetcher.stopLocationUpdates()
Log.v(TAG, "Destroying Foreground Location Service")
}
fun showNotification() {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel()
} else {
// In Android versions before Oreo channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
}
val notification = NotificationCompat.Builder(this, channelId)
.setOngoing(true)
.setContentTitle(NOTIFICATION_TITLE)
.setSmallIcon(R.mipmap.ic_notification)
.setTicker(NOTIFICATION_TITLE)
.build()
Log.v(TAG, "Showing a notification")
startForeground(NOTIFICATION_ID, notification)
}
#RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(): String {
val channelId = "app_gps_service"
val channelName = NOTIFICATION_TITLE
val chan = NotificationChannel(
channelId,
channelName,
NotificationManager.IMPORTANCE_LOW
)
chan.lightColor = Color.BLUE
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan)
Log.v(TAG, "Created notification channel, because SDK version is ${Build.VERSION.SDK_INT}")
return channelId
}
companion object {
val TAG = "ForegroundLocationSvc"
val NOTIFICATION_ID = 101
val NOTIFICATION_TITLE = "GPS Service"
#JvmStatic
fun start(context: Context) {
Log.v(TAG, "Starting Foreground Location Service")
val intent = Intent(context, ForegroundLocationService::class.java)
context.startService(intent)
}
}
}
Below There is our manifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.myapp">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-feature android:name="android.hardware.location.gps" />
<!-- push notifications permissions -->
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<permission
android:name="com.ticketing.permission.C2D_MESSAGE"
android:protectionLevel="signature"/>
<uses-permission android:name="com.ticketing.permission.C2D_MESSAGE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<permission
android:name="android.permission.ACCESS_COARSE_LOCATION"
android:protectionLevel="signature"/>
<permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:protectionLevel="signature"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:name=".MainApplication"
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:theme="#style/AppTheme">
<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="#string/app_name"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
<!-- GPS Receiver -->
<receiver android:name=".gps.GpsLocationReceiver">
<intent-filter>
<action android:name="android.location.PROVIDERS_CHANGED"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</receiver>
<service
android:name=".location.ForegroundLocationService"
android:description="#string/foreground_location_service_desc"
android:exported="false"
android:stopWithTask="false">
</service>
</application>
</manifest>
Related
I'm trying to apply a tutorial about request permission when the app start, but when I tested on my real device android 12, it's not show the permission dialog it show a snack bar that request from user to go to setting and grant it manually
Manifiest file
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- TODO: Step 1 add in permissions for fine location and background-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<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/AppTheme">
<activity android:name=".HuntMainActivity"
android:label="#string/title_activity_hunt"
android:launchMode="singleInstance"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name=".GeofenceBroadcastReceiver"/>
</application>
</manifest>
this my code
class HuntMainActivity : AppCompatActivity() {
private lateinit var binding: ActivityHuntMainBinding
private lateinit var geofencingClient: GeofencingClient
private lateinit var viewModel: GeofenceViewModel
private val runningQOrLater = android.os.Build.VERSION.SDK_INT >=
android.os.Build.VERSION_CODES.Q
// A PendingIntent for the Broadcast Receiver that handles geofence transitions.
// TODO: Step 8 add in a pending intent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_hunt_main)
viewModel = ViewModelProvider(this, SavedStateViewModelFactory(this.application,
this)).get(GeofenceViewModel::class.java)
binding.viewmodel = viewModel
binding.lifecycleOwner = this
// TODO: Step 9 instantiate the geofencing client
// Create channel for notifications
createChannel(this )
}
override fun onStart() {
super.onStart()
checkPermissionsAndStartGeofencing()
}
/*
* When we get the result from asking the user to turn on device location, we call
* checkDeviceLocationSettingsAndStartGeofence again to make sure it's actually on, but
* we don't resolve the check to keep the user from seeing an endless loop.
*/
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// TODO: Step 7 add code to check that the user turned on their device location and ask
// again if they did not
}
/*
* When the user clicks on the notification, this method will be called, letting us know that
* the geofence has been triggered, and it's time to move to the next one in the treasure
* hunt.
*/
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
val extras = intent?.extras
if(extras != null){
if(extras.containsKey(GeofencingConstants.EXTRA_GEOFENCE_INDEX)){
viewModel.updateHint(extras.getInt(GeofencingConstants.EXTRA_GEOFENCE_INDEX))
checkPermissionsAndStartGeofencing()
}
}
}
/*
* In all cases, we need to have the location permission. On Android 10+ (Q) we need to have
* the background permission as well.
*/
#SuppressLint("MissingSuperCall")
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
Log.d(TAG, "onRequestPermissionResult")
if (
grantResults.isEmpty() ||
grantResults[LOCATION_PERMISSION_INDEX] == PackageManager.PERMISSION_DENIED ||
(requestCode == REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE &&
grantResults[BACKGROUND_LOCATION_PERMISSION_INDEX] ==
PackageManager.PERMISSION_DENIED))
{
Snackbar.make(
binding.activityMapsMain,
R.string.permission_denied_explanation,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.settings) {
startActivity(Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}.show()
} else {
checkDeviceLocationSettingsAndStartGeofence()
}
}
/**
* This will also destroy any saved state in the associated ViewModel, so we remove the
* geofences here.
*/
override fun onDestroy() {
super.onDestroy()
removeGeofences()
}
/**
* Starts the permission check and Geofence process only if the Geofence associated with the
* current hint isn't yet active.
*/
private fun checkPermissionsAndStartGeofencing() {
if (viewModel.geofenceIsActive()) return
if (foregroundAndBackgroundLocationPermissionApproved()) {
checkDeviceLocationSettingsAndStartGeofence()
} else {
requestForegroundAndBackgroundLocationPermissions()
}
}
/*
* Uses the Location Client to check the current state of location settings, and gives the user
* the opportunity to turn on location services within our app.
*/
private fun checkDeviceLocationSettingsAndStartGeofence(resolve:Boolean = true) {
val locationRequest = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_LOW_POWER
}
val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
val settingsClient = LocationServices.getSettingsClient(this)
val locationSettingsResponseTask =
settingsClient.checkLocationSettings(builder.build())
locationSettingsResponseTask.addOnFailureListener { exception ->
if (exception is ResolvableApiException && resolve){
try {
exception.startResolutionForResult(this#HuntMainActivity,
REQUEST_TURN_DEVICE_LOCATION_ON)
} catch (sendEx: IntentSender.SendIntentException) {
Log.d(TAG, "Error getting location settings resolution: " + sendEx.message)
}
} else {
Snackbar.make(
binding.activityMapsMain,
R.string.location_required_error, Snackbar.LENGTH_INDEFINITE
).setAction(android.R.string.ok) {
checkDeviceLocationSettingsAndStartGeofence()
}.show()
}
}
locationSettingsResponseTask.addOnCompleteListener {
if ( it.isSuccessful ) {
addGeofenceForClue()
}
}
}
/*
* Determines whether the app has the appropriate permissions across Android 10+ and all other
* Android versions.
*/
#TargetApi(29)
private fun foregroundAndBackgroundLocationPermissionApproved(): Boolean {
val foregroundLocationApproved = (
PackageManager.PERMISSION_GRANTED ==
ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION))
val backgroundPermissionApproved =
if (runningQOrLater) {
PackageManager.PERMISSION_GRANTED ==
ActivityCompat.checkSelfPermission(
this, Manifest.permission.ACCESS_BACKGROUND_LOCATION
)
} else {
true
}
return foregroundLocationApproved && backgroundPermissionApproved
}
/*
* Requests ACCESS_FINE_LOCATION and (on Android 10+ (Q) ACCESS_BACKGROUND_LOCATION.
*/
#TargetApi(29 )
private fun requestForegroundAndBackgroundLocationPermissions() {
if (foregroundAndBackgroundLocationPermissionApproved())
return
var permissionsArray = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
val resultCode = when {
runningQOrLater -> {
permissionsArray += Manifest.permission.ACCESS_BACKGROUND_LOCATION
REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE
}
else -> REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE
}
Log.d(TAG, "Request foreground only location permission")
ActivityCompat.requestPermissions(
this#HuntMainActivity,
permissionsArray,
resultCode
)
}
/*
* Adds a Geofence for the current clue if needed, and removes any existing Geofence. This
* method should be called after the user has granted the location permission. If there are
* no more geofences, we remove the geofence and let the viewmodel know that the ending hint
* is now "active."
*/
private fun addGeofenceForClue() {
// TODO: Step 10 add in code to add the geofence
}
/**
* Removes geofences. This method should be called after the user has granted the location
* permission.
*/
private fun removeGeofences() {
// TODO: Step 12 add in code to remove the geofences
}
companion object {
internal const val ACTION_GEOFENCE_EVENT =
"HuntMainActivity.treasureHunt.action.ACTION_GEOFENCE_EVENT"
}
}
private const val REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE = 33
private const val REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE = 34
private const val REQUEST_TURN_DEVICE_LOCATION_ON = 29
private const val TAG = "HuntMainActivity"
private const val LOCATION_PERMISSION_INDEX = 0
private const val BACKGROUND_LOCATION_PERMISSION_INDEX = 1
I tested it on my real device, what i excopecting is showing the permission dialog when the app starts
GPS service is not enabled on Android versions 12 and above.
Everything worked correctly on earlier versions
I have read this documentation,
but I'm not sure if I have a problem with android:foregroundServiceType since I basically don't use it in the application.
So here is some of my code AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
And code of my vm:
private fun enableGps(activity: FragmentActivity) {
val manager = activity.getSystemService(Context.LOCATION_SERVICE) as LocationManager
if (!manager.isProviderEnabled(LocationManager.GPS_PROVIDER) && hasGPSDevice(manager)) {
locationManager.enableGps(activity)
}
}
And here is the implementation of the method itself:
override fun enableGps(activity: FragmentActivity) {
if (googleApiClient == null) {
googleApiClient = GoogleApiClient.Builder(context)
.addApi(LocationServices.API)
.addConnectionCallbacks(object : GoogleApiClient.ConnectionCallbacks {
override fun onConnected(bundle: Bundle?) {}
override fun onConnectionSuspended(i: Int) {
googleApiClient?.connect()
}
})
.addOnConnectionFailedListener { connectionResult ->
Timber.e("${connectionResult.errorCode}")
}.build()
googleApiClient?.connect()
}
val locationRequest = LocationRequest.create()
locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
locationRequest.interval = 10000
locationRequest.fastestInterval = 5000
val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
builder.setAlwaysShow(true)
googleApiClient?.let {
val result: PendingResult<LocationSettingsResult> =
LocationServices.SettingsApi.checkLocationSettings(it, builder.build())
result.setResultCallback { result ->
val status: Status = result.status
when (status.statusCode) {
LocationSettingsStatusCodes.RESOLUTION_REQUIRED -> try {
// Show the dialog by calling startResolutionForResult(),
status.startResolutionForResult(
activity,
REQUEST_LOCATION
)
} catch (e: IntentSender.SendIntentException) {
Timber.e(e)
}
LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE -> {
activity.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
}
}
}
}
}
UPD
Here is full code of my DeviceLocationManager, maybe I 'm requesting my permissions somehow wrong here:
https://gist.github.com/mnewlive/0c0a0c1f7ccb26fe58fd6b0fa5dd1dda
I rewrote the following method because it was deprecated:
override fun isLocationProviderActive(): Boolean {
val providerInfo = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.LOCATION_PROVIDERS_ALLOWED
)
return providerInfo?.isNotEmpty() == true
}
to
override fun isLocationStateEnabled(): Boolean {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
return LocationManagerCompat.isLocationEnabled(locationManager)
}
I'm developing an app in which I want to continuously scan for Ble advertisement packets, even if the user locks the screen. With my current implementation this works fine with Android 10, but with Android 11 it stops once the user locks the screen. For scanning Ble packets I first request a few permissions, namely:
coarse and fine location
bluetooth scan
access background location
I start a simple foreground service (also added foreground service permission to my Manifest) with:
private fun startBleService() {
serviceIntent = Intent(baseContext, ScanService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Log.i("Background Act", "Starting foreground service for android 8.0+")
applicationContext.startForegroundService(serviceIntent)
} else {
Log.i("Background Act", "Starting foreground service for versions < android 8.0")
applicationContext.startService(serviceIntent)
}
}
This will call startForeground(notificationID, notification) in the onStartCommand function of my ScanService, thus requesting to run in foreground. After this I start the actual Ble scan functionalities. I also added android:foregroundServiceType="location" to the service in the Manifest.
My ScanService Code:
class ScanService : Service() {
private val channelID = "CustomChannelID"
private val notificationID = 7
private lateinit var bluetoothManager: BluetoothManager
private lateinit var bluetoothAdapter: BluetoothAdapter
private lateinit var bluetoothLeScanner: BluetoothLeScanner
private var scanCounter = 0
private var _bleSingleScanResults = HashMap<String, MutableList<String>>()
// all scans combined in an array, internally used
private var _bleAllScanResults = arrayListOf<HashMap<String, MutableList<String>>>()
// only starts a new scan if its not already scanning
private var scanning = false
private val notificationManager by lazy {getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager}
#RequiresApi(Build.VERSION_CODES.M)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i("OnStartCommand Service", "Is started")
val notification: Notification =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createChannel(notificationManager)
Notification.Builder(this, channelID)
.setContentTitle("Content Title")
.setContentText("Content Text")
.setTicker("Ticker")
.build()
} else {
NotificationCompat.Builder(this, channelID)
.setContentTitle("BLE Scanning Service")
.setContentText("Scanning BLE in the background")
.setTicker("Ticker")
.build()
}
// use custom non-zero notification ID
startForeground(notificationID, notification)
//TODO: start in new thread
scanBle()
// If we get killed, after returning from here, restart, recreating notification again though
return START_STICKY
}
#RequiresApi(Build.VERSION_CODES.M)
fun scanBle() {
bluetoothManager = this.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter
if (bluetoothAdapter.bluetoothLeScanner == null) {
Log.d("BLE", "Device doesn't support BLE")
Toast.makeText(
this,
"It seems like your device does not support BLE. This is a crucial part of this app. \n " +
"Unfortunately you can't contribute to the dataset of scanned locations.",
Toast.LENGTH_LONG
).show()
return
}
bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
runBLEScan()
}
#SuppressLint("MissingPermission") //since we check beforehand in the MainActivity for permissions already
private fun runBLEScan() {
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
val scanFilters = listOf(ScanFilter.Builder().build())
if (!scanning) {
scanning = true
Log.i("BLE", "--- STARTING BLE SCAN ---")
bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallBackLe)
} else Log.d("BLE Scan", "Called scanning function but is currently already scanning!")
}
// ALWAYS ON UI-THREAD
private val scanCallBackLe = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
super.onScanResult(callbackType, result)
// discard result if payload is null
if(result.scanRecord == null || result.scanRecord!!.bytes == null) {
return
}
println("Payload: ${result.scanRecord?.bytes?.toHexString()}")
// check if device already discovered in a scan, if so increase counter, else make new
// entry in the result HashMap
if (_bleSingleScanResults.isEmpty() || !_bleSingleScanResults.containsKey(result.device.toString())) {
// device wasn't seen before
_bleSingleScanResults[result.device.toString()] =
mutableListOf(result.rssi.toString(), result.scanRecord!!.bytes.toHexString(), "1")
} else {
// update already existing entry
val cntr = _bleSingleScanResults[result.device.toString()]!![2].toInt() + 1
_bleSingleScanResults[result.device.toString()]!![2] = cntr.toString()
}
}
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
Log.d("BLE ScanResult", "Scan failed code: $errorCode")
}
}
private fun ByteArray.toHexString() = joinToString("", "[0x", "]") { "%02X".format(it) }
private fun createChannel(notificationManager: NotificationManager) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}
val channel =
NotificationChannel(channelID, "Scan Service", NotificationManager.IMPORTANCE_DEFAULT)
channel.description = "Hello! This is a notification."
notificationManager.createNotificationChannel(channel)
}
#SuppressLint("MissingPermission")
override fun onDestroy() {
Log.d("Destroyed Service", "That's even worse")
bluetoothLeScanner.stopScan(scanCallBackLe)
super.onDestroy()
}
#SuppressLint("MissingPermission")
override fun stopService(name: Intent?): Boolean {
Log.d("Stopped Service", "That's bad")
bluetoothLeScanner.stopScan(scanCallBackLe)
stopSelf()
return super.stopService(name)
}
override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}
Parts of my Manifest:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<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.FOREGROUND_SERVICE" />
<application
...
<service
android:name=".ScanService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location" />
...
</application>
This works fine with Android 10 (tested on a Huawei device), but unfortunately not on Android 11 (Samsung A22). Is there any other permission I need to be able to keep scanning even if the user locks the screen in Android 11?
We have implemented a Geofencing system where our CMS backend operators can send geofence data to the app through FCM. Inside the FirebaseService class that you see below, every time such a notification is sent it triggers the handleSilentNotification method which then attempts to create a Geofence object and add it to the Geofencing client.
We are requesting location access during the onboarding process and if the location permission was not granted, we just ignore the silent notification payload. When the user selects the "Allow all the time" location permission option, everything seems to work fine. Payloads are received, and geofences are created and added to the client for tracking.
The issue occurs when the user selects "Allow only while in use".
In this case, when we attempt to add the geofence to the GeofencingClient, it always fails by throwing the following stack trace:
2022-05-05 11:27:48.107 13685-13685/com.threenitas.ewayprototype I/EwayApp: New location received: 37.95690471,23.72790214
2022-05-05 11:27:55.580 13685-14907/com.threenitas.ewayprototype D/MyFirebaseMessagingService: body = "{\"Id\":\"1234654\",\"Latitude\":37.3495,\"Longitude\":23.4646,\"Radius\":100,\"StatusCode\":\"EnableEvent\",\"Title\":\"test\",\"Description\":\"test\"}"
2022-05-05 11:27:55.593 13685-14907/com.threenitas.ewayprototype D/MyFirebaseMessagingService: New Geofence created: Geofence[CIRCLE id:1234654 transitions:3 37.349500, 23.464600 9999999933815813000000000000000000000m, resp=0s, dwell=-1ms, #-1]
2022-05-05 11:27:55.616 13685-13685/com.threenitas.ewayprototype D/GeofencingManager: Failed to add geofence with id 1234654 to client!
2022-05-05 11:27:55.616 13685-13685/com.threenitas.ewayprototype W/System.err: com.google.android.gms.common.api.ApiException: 1004:
2022-05-05 11:27:55.617 13685-13685/com.threenitas.ewayprototype W/System.err: at com.google.android.gms.common.api.internal.TaskUtil.setResultOrApiException(com.google.android.gms:play-services-base##18.0.1:4)
2022-05-05 11:27:55.617 13685-13685/com.threenitas.ewayprototype W/System.err: at com.google.android.gms.location.zzat.setResult(com.google.android.gms:play-services-location##18.0.0:2)
2022-05-05 11:27:55.617 13685-13685/com.threenitas.ewayprototype W/System.err: at com.google.android.gms.internal.location.zzaw.zzb(com.google.android.gms:play-services-location##18.0.0:3)
2022-05-05 11:27:55.617 13685-13685/com.threenitas.ewayprototype W/System.err: at com.google.android.gms.internal.location.zzaj.zza(com.google.android.gms:play-services-location##18.0.0:9)
2022-05-05 11:27:55.617 13685-13685/com.threenitas.ewayprototype W/System.err: at com.google.android.gms.internal.location.zzb.onTransact(com.google.android.gms:play-services-location##18.0.0:3)
2022-05-05 11:27:55.617 13685-13685/com.threenitas.ewayprototype W/System.err: at android.os.Binder.execTransactInternal(Binder.java:1195)
2022-05-05 11:27:55.617 13685-13685/com.threenitas.ewayprototype W/System.err: at android.os.Binder.execTransact(Binder.java:1159)
2022-05-05 11:27:58.087 13685-14822/com.threenitas.ewayprototype W/s.ewayprototyp: Suspending all threads took: 6.101ms
2022-05-05 11:28:06.071 13685-14822/com.threenitas.ewayprototype W/s.ewayprototyp: Suspending all threads took: 5.875ms
2022-05-05 11:28:06.089 13685-14822/com.threenitas.ewayprototype W/s.ewayprototyp: Suspending all threads took: 8.474ms
2022-05-05 11:28:06.106 13685-14822/com.threenitas.ewayprototype W/s.ewayprototyp: Suspending all threads took: 6.250ms
2022-05-05 11:28:06.136 13685-14822/com.threenitas.ewayprototype W/s.ewayprototyp: Suspending all threads took: 6.388ms
Is this a design decision on google's end like this bug tracker post seems to claim or am I missing something in my implementation?
App.kt:
class App : Application() {
lateinit var appContext: Context
lateinit var preferences: SharedPreferences
lateinit var editor: SharedPreferences.Editor
lateinit var geofencingClient: GeofencingClient
val inactiveGeofenceIds = arrayListOf<String>()
val geofenceEventInfo = mutableMapOf<String, GeoEventData>()
val geofencePendingIntent: PendingIntent by lazy {
val intent = Intent(appContext, GeofenceBroadcastReceiver::class.java)
// We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when calling
// addGeofences() and removeGeofences().
PendingIntent.getBroadcast(appContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
val locationListener: LocationListener = object : LocationListener {
override fun onLocationChanged(location: Location) {
Log.i("EwayApp", "New location received: ${location.latitude},${location.longitude}")
}
override fun onLocationChanged(locations: MutableList<Location>) {}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}
fun isGeofencingClientInitialized() = this::geofencingClient.isInitialized
...........
}
FirebaseMessagingService:
class MyFirebaseMessagingService : FirebaseMessagingService() {
private val TAG = MyFirebaseMessagingService::class.java.simpleName
private var geofencingManager: GeofencingManager = getKoin().get()
override fun onNewToken(token: String) {
super.onNewToken(token)
NotificationHub.getInstance(App.instance)
.registerForPushNotifications(this, fetchIntegrationIdForLetsPlace())
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
// Check if message contains a data payload
remoteMessage.data.isNotEmpty().let {
handleNotification(remoteMessage.data)
}
}
private fun handleNotification(jsonData: MutableMap<String, String>) {
val title = jsonData["Title"]
val description = jsonData["Description"]
val body = jsonData["Body"]
if (title!!.isEmpty()) {
if (hasLocationPermission()) handleSilentNotification(body)
else Log.d(TAG, "No location permission has been granted!")
} else {
showNotification(
title,
description!!
)
}
}
private fun hasLocationPermission() = ContextCompat.checkSelfPermission(
App.instance.appContext,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
private fun showNotification(
title: String,
description: String
) {
// val intentForNotification = Intent(application, SplashScreen::class.java)
// intentForNotification.putExtra("notificationInboxBody", inboxBody)
// intentForNotification.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val hub = NotificationHub.getInstance(App.instance)
val id = hub.lastRandomId ?: 0
hub.lastRandomId = id + 1
//val pendingIntent = PendingIntent.getActivity(application, hub.lastRandomId!! /* Request code */, intentForNotification, PendingIntent.FLAG_UPDATE_CURRENT)
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val notificationBuilder = NotificationCompat.Builder(application, "channel1")
.setSmallIcon(R.drawable.splash_screen_logo)
.setContentTitle(title)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setPriority(Notification.PRIORITY_HIGH)
.setCategory(Notification.CATEGORY_MESSAGE)
.setDefaults(Notification.DEFAULT_ALL)
//.setContentIntent(pendingIntent)
.setContentText(description)
with(NotificationManagerCompat.from(this)) {
// notificationId is a unique int for each notification that you must define
notify(id, notificationBuilder.build())
}
}
private fun handleSilentNotification(body: String?) {
Log.d(TAG, "body = $body")
val geoEventData = jsonToGeoEvent(body)
if (geoEventData == null) {
Log.d(TAG, "Geo event data conversion failed!")
return;
}
if (!App.instance.isGeofencingClientInitialized()) { // app is killed , store the payload for later use
Log.d(TAG, "persist called")
persistGeoData(geoEventData)
return
}
if (geoEventData.statusCode == GeoEventStatus.DisableEvent.name) {
geofencingManager.removeGeofence(geoEventData.id)
return
}
val geofence = geofencingManager.createGeofence(geoEventData)
geofencingManager.storeGeofenceInfo(geofence, geoEventData)
Log.d(TAG, "New Geofence created: $geofence")
geofencingManager.addGeofenceToClient(geofence)
}
private fun persistGeoData(geoEventData: GeoEventData) {
val gson = Gson()
val serializedData = gson.toJson(geoEventData, GeoEventData::class.java)
App.instance.inactiveGeofenceIds.add(geoEventData.id)
App.instance.preferences.edit().putString(geoEventData.id, serializedData).apply()
}
private fun jsonToGeoEvent(body: String?): GeoEventData? {
if (body == null) return null
val gson = Gson()
val json = body.replace("\\", "")
val jo = JSONObject(json.substring(1, json.length - 1))
return gson.fromJson(jo.toString(), GeoEventData::class.java)
}
}
GeofencingManagerImpl:
class GeofencingManagerImpl : GeofencingManager {
private val tag: String
get() = GeofencingManager::class.java.simpleName
#SuppressLint("MissingPermission")
override fun addGeofenceToClient(geofence: Geofence) {
App.instance.geofencingClient.addGeofences(
getGeofencingRequest(geofence),
App.instance.geofencePendingIntent
).run {
addOnSuccessListener {
Log.d(tag, "Geofence with id ${geofence.requestId} added to client!")
}
addOnFailureListener {
Log.d(tag, "Failed to add geofence with id ${geofence.requestId} to client!")
it.printStackTrace()
}
}
}
override fun createGeofence(geoEventData: GeoEventData) =
Geofence.Builder()
.setRequestId(geoEventData.id)
.setCircularRegion(
geoEventData.latitude,
geoEventData.longitude,
geoEventData.radius
)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
.build()
override fun storeGeofenceInfo(
geofence: Geofence,
geoEventData: GeoEventData
) {
App.instance.geofenceEventInfo[geofence.requestId] = geoEventData
}
override fun getGeofencingRequest(geofence: Geofence): GeofencingRequest {
return GeofencingRequest.Builder()
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.addGeofence(geofence)
.build()
}
override fun removeGeofence(id: String) {
App.instance.geofenceEventInfo.remove(id)
App.instance.geofencingClient.removeGeofences(listOf(id)).run {
addOnSuccessListener {
Log.d(tag, "Geofence with id $id removed from client!")
}
addOnFailureListener {
Log.d(tag, "Failed to remove geofence with id $id from client!")
it.printStackTrace()
}
}
}
}
BroadcastReceiver:
class GeofenceBroadcastReceiver : BroadcastReceiver() {
private val TAG = GeofenceBroadcastReceiver::class.java.simpleName
override fun onReceive(context: Context?, intent: Intent?) {
val geofencingEvent = GeofencingEvent.fromIntent(intent!!)
if (geofencingEvent.hasError()) {
val errorMessage = GeofenceStatusCodes
.getStatusCodeString(geofencingEvent.errorCode)
Log.e(TAG, errorMessage)
return
}
val geofenceTransition = geofencingEvent.geofenceTransition
if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT
) {
val triggeringGeofences = geofencingEvent.triggeringGeofences
if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {
// user entered or is already inside a geofence
for (geofence in triggeringGeofences) {
val eventData = App.instance.geofenceEventInfo[geofence.requestId] ?: continue
if (eventData.statusCode == GeoEventStatus.EnableEvent.name) showNotification(
eventData.title, eventData.description
)
}
Log.d(TAG, "User entered geofence!")
} else {
Log.d(TAG, "User exited geofence!")
}
} else {
Log.e(
TAG, "Invalid transition type"
)
}
}
private fun showNotification(title: String, description: String) {
// val intentForNotification = Intent(application, SplashScreen::class.java)
// intentForNotification.putExtra("notificationInboxBody", inboxBody)
// intentForNotification.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val hub = NotificationHub.getInstance(App.instance)
val id = hub.lastRandomId ?: 0
hub.lastRandomId = id + 1
//val pendingIntent = PendingIntent.getActivity(application, hub.lastRandomId!! /* Request code */, intentForNotification, PendingIntent.FLAG_UPDATE_CURRENT)
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val notificationBuilder = NotificationCompat.Builder(App.instance.appContext, "channel1")
.setSmallIcon(R.drawable.splash_screen_logo)
.setContentTitle(title)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setPriority(Notification.PRIORITY_HIGH)
.setCategory(Notification.CATEGORY_MESSAGE)
.setDefaults(Notification.DEFAULT_ALL)
//.setContentIntent(pendingIntent)
.setContentText(description)
with(NotificationManagerCompat.from(App.instance.appContext)) {
// notificationId is a unique int for each notification that you must define
notify(id, notificationBuilder.build())
}
}
}
Manifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.threenitas.ewayprototype">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<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:name=".App"
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.EWayPrototype"
android:usesCleartextTraffic="true">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />
<receiver android:name=".geofencing.GeofenceBroadcastReceiver"/>
<service android:foregroundServiceType="location" android:enabled="true" android:name=".geofencing.GeofenceForegroundService"/>
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
android:label="#string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
</application>
</manifest>
The Android geofencing documentation clearly says:
To use geofencing, your app must request the following:
ACCESS_FINE_LOCATION
ACCESS_BACKGROUND_LOCATION if your app targets Android 10 (API level 29) or higher
That second permission is the "Allow all the time" permission.
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")
}
}
}
}