androidkotlinlocationandroid-manifestsettings

Accessing Location In Background In Android


I am creating an application which is inspired by another app in which the application collect my location even if the application is closed I am able to achieve the similar behaviour by creating a broadcast receiver which collect my location but In my application when I collect the location I get warning form the Android Setting that this app access location in the background but for the other app form playstore I have not see any such warning till this time I am using that app for a while now.

How can I achieve similar behaviour.

One thing which I have notice is that when that other app collect the location the android location icon show in the status bar but in case when I collect the location I can't see that. May be this gives some hint.

I even tried to star a foreground service while collection the location while the app is closed but can't start the service form the background.

I get this error:

Failed to start service: startForegroundService() not allowed due to mAllowStartForeground false: service com.kgJr.safecircle/.ui_util.services.LocationUpdateService

So how can I achieve the behaviour I am looking for.

I will be glad if someone can help me on this. Thank You !!!

I am trying the collect the location in the background while avoiding the warning from android default system that <This> app access location in the background like a similar app I am using which also collect the location in the background even if the application is closed and I wont get any such warning.

current code for fetching the location

 @RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
    fun getCurrentLocation(context: Context, callback: (Location?) -> Unit) {
        if (!isLocationPermissionGranted(context)) {
            callback(null)
            return
        }
        val fusedLocationClient = getFusedLocationClient(context)
        fusedLocationClient.lastLocation.addOnSuccessListener { location ->
            callback(location)
        }.addOnFailureListener {
            callback(null)
        }
    }

all the permissions

<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" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />

Solution

  • Ok this is the solution I have Implemented and its actually working super well.

    so, the problem was I need to get location in the background but also I don't want to keep the app up and running all the time and also it should be efficient so, the it wont consume whole bunch of battery

    so this is the solution:

    so first you have to take some permission like for getting location in the background i.e

    <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" />
    

    after this instead of having a single broadcast receiver I have two different type of broadcast receiver i.e

    1. Activity Broadcast Receiver

    2. BootReceiverRestarter

    Only this two because I tried the wifi receiver and several other thing they did not worked that well.

    <receiver android:name="com.kgjr.safecircle.broadcastReceiver.ActivityTransitionReceiver"
        android:exported="true"
        tools:ignore="ExportedReceiver" />
    <receiver
        android:name=".broadcastReceiver.BootReceiverRestarter"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED" />
        </intent-filter>
    </receiver>
    

    After this I also include Alarm manager why because in some case after some time like 1 or 2 day the activity receiver will stop catching the activity this the whole flow will break the alarm manager will ensure that we at the vary least collect the data at fixed interval and thus we can reinitialize the or check for the activity receiver is working fine or not also one important point is that we don't prescedule the whole alarm receiver in advance but we do something like this : After receiving the signal we scheduled for the next one.

    Also for reciving the alarm and keeping the loop going we have to give some especific things I have experimented with other multiple options present but they did not worked that well so, this is the best I can come up with I have searched all over the internet but did not found a fully composed solution.

    so, the below is the xml for the Alarm receiver:

    <receiver
        android:name=".broadcastReceiver.AlarmBootReceiverForLooper"
        android:enabled="true"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED" />
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </receiver>
    <receiver
        android:name=".broadcastReceiver.AlarmReceiver"
        android:exported="true"
        tools:ignore="ExportedReceiver" />
    <receiver
        android:name=".broadcastReceiver.AlarmReceiverLooper"
        android:exported="true"
        tools:ignore="ExportedReceiver" />
    <service
        android:name=".service.AlarmForegroundService"
        android:foregroundServiceType="location"
        android:exported="false" />
    <service
        android:name=".service.AlarmForegroundServiceLooper"
        android:foregroundServiceType="location"
        android:exported="false" />
    

    Also one cool thing about whole this is if for some reason the users phone shuts down the then restart the data collection will began again the user don't need to open the phone again

    so the home screen once the app is loaded for the first time (after giving all the permission) I initialize all this things i.e :

    LaunchedEffect(Unit) {
        /**
         * @Mark: always initialize the below method because it also creates the notification channel.
         */
        LocationActivityManager.cancelPeriodicNotificationWorker(context)
        LocationActivityManager.initializeNotificationAndWorker(context)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            val permission = Manifest.permission.ACTIVITY_RECOGNITION
            if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) {
                delay(5000) // 5-sec delay
                LocationActivityManager.startActivityRecognition(context)
            } else {
                Log.e("SafeCircle", "ACTIVITY_RECOGNITION permission not granted")
            }
        } else {
            Log.d("SafeCircle", "ACTIVITY_RECOGNITION not required for SDK < 29")
        }
    }
    

    here is how I intialize the things

    object LocationActivityManager {
    
        fun initializeNotificationAndWorker(context: Context) {
            createNotificationChannel(context)
            schedulePeriodicNotificationWorker(context) // For confirmation that location will be collect in 15 min max.
        }
    
        @RequiresPermission(Manifest.permission.ACTIVITY_RECOGNITION)
        fun startActivityRecognition(context: Context) {
            val activityRecognitionClient = ActivityRecognition.getClient(context)
            val intent = Intent(context, ActivityTransitionReceiver::class.java)
    
            val pendingIntent = PendingIntent.getBroadcast(
                context,
                0,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
            )
    
            activityRecognitionClient.requestActivityUpdates(3000, pendingIntent) //kind of sating ask for update in every 3 sec...
                .addOnSuccessListener {
                    Log.d("LocationActivityManager", "Activity updates requested successfully")
                }
                .addOnFailureListener {
                    Log.e("LocationActivityManager", "Failed to request activity updates", it)
                }
        }
    
        private fun createNotificationChannel(context: Context) {
            val channel = NotificationChannel(
                UPDATE_LOCATION_CHANNEL_ID,
                "Location Updates",
                NotificationManager.IMPORTANCE_LOW
            ).apply {
                description = "Used for background location update notifications"
            }
    
            val manager = context.getSystemService(NotificationManager::class.java)
            manager.createNotificationChannel(channel)
        }
    
        private fun schedulePeriodicNotificationWorker(context: Context) {
            Log.d("WorkManager", "PeriodicNotificationWorker has been schedule")
            val periodicWorkRequest = PeriodicWorkRequestBuilder<PeriodicLocationUpdater>(
                15, TimeUnit.MINUTES
            ).addTag("PeriodicNotificationWorkerTag").build()
    
            WorkManager.getInstance(context).enqueueUniquePeriodicWork(
                "PeriodicNotificationWorkerTag",
                ExistingPeriodicWorkPolicy.KEEP,
                periodicWorkRequest
            )
    
        }
        fun cancelPeriodicNotificationWorker(context: Context) {
            Log.d("WorkManager", "PeriodicNotificationWorker has been cancelled.")
            WorkManager.getInstance(context)
                .cancelUniqueWork("PeriodicNotificationWorkerTag")
        }
    }
    

    afte this you just need to create the bracast reciver and Alarm manager ie:

    class ActivityTransitionReceiver : BroadcastReceiver() {
        companion object {
            private lateinit var sharedPreferenceManager: SharedPreferenceManager
        }
        private lateinit var notificationService: NotificationService
        @RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.POST_NOTIFICATIONS])
        override fun onReceive(context: Context, intent: Intent) {
            //initializing the managers in the starting...
            sharedPreferenceManager = SharedPreferenceManager(context)
            notificationService = NotificationService(context)
            if (MainApplication.getGoogleAuthUiClient().getSignedInUser()?.userId != null){
                if (ActivityRecognitionResult.hasResult(intent)) {
                    val result = ActivityRecognitionResult.extractResult(intent)
                    val activity = result!!.mostProbableActivity
                    val type = getActivityType(activity.type)
                    val confidence = activity.confidence
                    Log.d("SafeCircle", "Detected activity: $type with confidence: $confidence")
    
                    try {
                        LocationUtils.getCurrentLocation(context) { location ->
                            location?.let {
                                updateLocation(context, type, location) {
                                }
                            }
                        }
                    } catch (e: SecurityException) {
                        e.printStackTrace()
                        Log.e("SafeCircle", "Missing location permission.")
                    }
                }
            }
        }
    }
    

    you can create the function updateLocation() according to your need now.

    this is the broadcast receiver of the case of device boot

    class LocationBroadcastReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent?) {
            Log.d("LocationBroadcast", "Broadcast received: ${intent?.action}")
            when (intent?.action) {
                LocationManager.PROVIDERS_CHANGED_ACTION -> {
                    val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
                    val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
                    val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
                    Log.d("LocationBroadcast", "GPS: $isGpsEnabled, Network: $isNetworkEnabled")
                }
                Intent.ACTION_AIRPLANE_MODE_CHANGED -> {
                    Log.d("LocationBroadcast", "Airplane mode changed")
                }
            }
        }
    }
    
    class BootReceiverRestarter : BroadcastReceiver() {
        @RequiresPermission(Manifest.permission.ACTIVITY_RECOGNITION)
        override fun onReceive(context: Context, intent: Intent?) {
            if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
                Log.d("SafeCircle", "Boot completed - re-registering activity updates")
                val activityRecognitionClient = ActivityRecognition.getClient(context)
    
                val pendingIntent = PendingIntent.getBroadcast(
                    context,
                    0,
                    Intent(context, ActivityTransitionReceiver::class.java),
                    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
                )
    
                activityRecognitionClient.requestActivityUpdates(3000, pendingIntent)
                    .addOnSuccessListener {
                        Log.d("SafeCircle", "Successfully re-registered activity updates")
                    }
                    .addOnFailureListener {
                        Log.e("SafeCircle", "Failed to re-register activity updates", it)
                    }
            }
        }
    }
    

    then this is the alarm broadcast receiver for the looping condition i.e

    class AlarmReceiverLooper : BroadcastReceiver() {
        companion object {
            private lateinit var sharedPreferenceManager: SharedPreferenceManager
        }
        private lateinit var scheduler: AndroidAlarmSchedulerLooper
        private lateinit var notificationService: NotificationService
        @RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION,Manifest.permission.POST_NOTIFICATIONS])
        override fun onReceive(context: Context, intent: Intent) {
            Log.d("SafeCircle", "AlarmReceiver triggered")
            try {
                BackgroundApiManagerUtil.uploadAllPendingData()
            }catch (e: Exception){
                e.printStackTrace()
            }
            scheduler = MainApplication.getScheduler()
            sharedPreferenceManager = SharedPreferenceManager(context)
            notificationService = NotificationService(context)
            print("Looper Status: ${sharedPreferenceManager.getIsUpdateLocationApiCalledLooper()} ")
    //        if (sharedPreferenceManager.getIsUpdateLocationApiCalledLooper() == false && MainApplication.getGoogleAuthUiClient().getSignedInUser()?.userId != null) {
    //            val serviceIntent = Intent(context, AlarmForegroundServiceLooper::class.java).apply {
    //                putExtra("ActivityType", "N.A")
    //            }
    //            ContextCompat.startForegroundService(context, serviceIntent)
    //        }
            LocationUtils.getCurrentLocation(context) { location ->
                location?.let {
                    updateLocation(
                        context = context,
                        activityType = "N.A",
                        currentLocation = location,
                        onCompletion = {
                            scheduler.scheduleAlarm(300)
                        }
                    )
                }
            }
        }
    

    the below one is for single time use just comment this one line

    //                            scheduler.scheduleAlarm(10)
    

    all the other thing will remain the same

    this is the boot receiver for Alarm Scheduler

    class AlarmBootReceiverForLooper : BroadcastReceiver() {
        companion object {
            private lateinit var sharedPreferenceManager: SharedPreferenceManager
        }
        override fun onReceive(context: Context, intent: Intent) {
            if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
                Log.d("SafeCircle", "Device booted. Attempting to reschedule alarm.")
                sharedPreferenceManager = SharedPreferenceManager(context)
                val scheduler = MainApplication.getScheduler()
                scheduler.scheduleAlarm(timeInSec = 1)
                sharedPreferenceManager.saveLooperEnabled(true)
                Log.d("SafeCircle", "Alarm rescheduled after boot.")
            }
        }
    }
    

    this is the core implementation and the idea and thought process behind this

    hope this answers will help.