Real-time Location Updates on Android 10

This entry was posted on April 22, 2020 by Neda Milosavljević, Senior Android & iOS Developer.

Android 10 Real Time Location Updates

Developing an app that delivers real-time precise location updates while the user is not actively using it can be tricky. In addition, you should be careful about device battery, because apps using too much battery power can be suspended by Android itself, or deleted by users. Android also introduced background location restrictions, making developers’ lives even more complicated.

The main problem we need to solve is: how to develop an app that is constantly collecting precise location information, is not killed by the system, and survives device reboots?

This is a complete guide on how to create such an app.

Step 1: Define and request location permissions

Apps that use location services must request location permissions, so first, we need to make sure we have included fine location permission in Manifest:

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

Then, we need to request the user approvement permission at runtime (on Android 6.0 and higher):

if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
   // Permission is not granted
   // Request the permission
   ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQUEST_CODE);
} else {
   // Permission has already been granted
}

It’s important to notice that we call the checkSelfPermission method to check if the user has already granted permission. When requestPermissions is called, a dialog is presented for the user to grant or deny permission. The return of the requestPermissions method call is managed in onRequestPermissionResult method callback. In the callback, we check if the user has granted permission or not. If yes, we can (in this case) start location update service.

@Override
public void onRequestPermissionsResult(int requestCode, @NotNull String[] permissions, @NotNull int[] grantResults) {
   if (requestCode == REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS) {
       if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
           //Permission is granted
           startLocationUpdateService();
       } else {
           //Permission denied
           //Notify user that base app functionality will not work without this permission and try asking again
       }
   }
}

Step 2: Creating and starting the foreground service

After the user allows the location permission, we can start with the location updates. As per Android developer documentation, “In an effort to reduce power consumption, Android 8.0 (API level 26) limits how frequently background apps can retrieve the user's current location. Apps can receive location updates only a few times each hour.” In other words: we cannot rely on the background services for frequent location updates for apps running on Android 8.0 and later. The solution for this “problem” is that we run a foreground service that can run regardless of the application's activity. First, register the foreground service in Manifest: 

<!-- Foreground services in Q+ require type. -->
<service
   android:name="com.packagename.LocationUpdateService"
   android:enabled="true"
   android:foregroundServiceType="location" />

We also need to add one more permission to Manifest for apps targeting API level 28 or higher: FOREGROUND_SERVICE. This is normal permission, so the system grants it automatically and we don’t need to request it at runtime.

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

Service class basic definition:

public class LocationUpdateService extends Service {

   @Override
   public int onStartCommand(Intent intent, int flags, int startId) {
       requestLocationUpdates();
       startForeground(FOREGROUND_NOTIFICATION_ID, getForegroundNotification(getApplicationContext()));
       // Tells the system to try to recreate the service after it has been killed.
       return START_STICKY;
   }
}

Foreground services must display a notification, and they continue running even when the user isn't interacting with the app.

For apps targeting API level 26 or higher, the system requires that startForegroundService() is called for creating a service. This method creates the background service, but it notifies the system that the service will promote itself to the foreground. When the service has been created, it must call its startForeground() method within five seconds. We call it in the onStartCommand() method with an ongoing notification provided. The notification is created by the getForegroundNotification() method:

public Notification getForegroundNotification(Context context, String input) {
   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
       NotificationChannel serviceChannel = new NotificationChannel(
               CHANNEL_DEFAULT_IMPORTANCE,
               "Foreground Service Channel",
               NotificationManager.IMPORTANCE_LOW
       );

       //As notification is always present, we don't need the badge displayed by the system
       serviceChannel.setShowBadge(false);

       NotificationManager manager = context.getSystemService(NotificationManager.class);
       if (manager != null) {
           manager.createNotificationChannel(serviceChannel);
       }
   }
   Intent notificationIntent = new Intent(context, MainActivity.class);
   PendingIntent pendingIntent = PendingIntent.getActivity(context,
           0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);

   Notification foregroundNotification = new NotificationCompat.Builder(context, CHANNEL_DEFAULT_IMPORTANCE)
           .setContentTitle("Title")
           .setContentText(input)
           .setSmallIcon(R.drawable.ic_notification_icon)
           .setContentIntent(pendingIntent)
           .build();

   return foregroundNotification;
}

Now we can safely start our service from MainActivity, using the following method:

private void startLocationUpdateService() {
   //Declare intent
   Intent serviceIntent = new Intent(this, LocationUpdateService.class);
   //Start foreground service
   ContextCompat.startForegroundService(this, serviceIntent);
}

It’s important to check if the location permission is granted before starting the service, so make sure not to skip that step.

Step 3: Request periodic location updates

Before we request location updates, we have to define location request, fused location client, and location callback:

/**
* Contains parameters used by {@link FusedLocationProviderClient}.
*/
private LocationRequest locationRequest;

/**
* Provides access to the Fused Location Provider API.
*/
private FusedLocationProviderClient fusedLocationClient;

/**
* Callback for location changes.
*/
private LocationCallback locationCallback;

Fused Location Provider Client is part of the Google Play Services Location dependency. So, we need to add the following dependency in the build.gradle file:

implementation 'com.google.android.gms:play-services-location:17.0.0'

Get client instance:

fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);

Location callback defines an action performed on every location update:

locationCallback = new LocationCallback() {
   @Override
   public void onLocationResult(LocationResult locationResult) {
     //locationResult - location result holding location(s)
     //Custom method for handling location updates
     onNewLocation(locationResult.getLastLocation());
   }
};

And finally, we create the location request:

/**
* The desired interval for location updates. Inexact. Updates may be more or less frequent.
*/
private final long UPDATE_INTERVAL_IN_MILLISECONDS = 10000;

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

/**
* Sets the location request parameters.
*/
private void createLocationRequest() {
   if(locationRequest == null)
       locationRequest = new LocationRequest();
   locationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS);
   locationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS);
   locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
}

LocationRequest is a data object that contains service parameters for requests to the fused location provider API. In our case, we want the best possible accuracy, but if your app doesn’t require high accuracy and frequency, LocationRequest provides more priority values, for example, PRIORITY_BALANCED_POWER_ACCURACY or PRIORITY_NO_POWER (the app will not trigger any location updates, but will receive locations triggered by other apps).

With this approach, we cannot specify the exact location source, such as GPS. In fact, the system usually has multiple location providers running and fuses the results from several sources into a single Location object. When Wifi is connected, it’s more likely that network location is used, which is very useful when locating indoors or when GPS signals interfere with buildings since it is easier to calculate the location via routers that surround us in these modern times.

We do these initializations inside the onCreate() method, right after the service is created. Now we can start with location updates. In our case, service has the sole purpose of collecting locations so updates can start immediately in the onStartCommand method. We use location client, request, and callback defined above. Here is the method that requests location updates (note that the request can fail):

private void requestLocationUpdates() {
   try {
       mFusedLocationClient.requestLocationUpdates(locationRequest,
               mLocationCallback, Looper.myLooper());
   } catch (SecurityException exception) {
       Log.e(TAG, "Lost location permission. Could not request updates. " + exception);
   }
}

Location updates can be stopped simply by calling the removeLocationUpdates() method:

/**
* Removes location updates
*/
private void removeLocationUpdates() {
   try {
       mFusedLocationClient.removeLocationUpdates(mLocationCallback);
   } catch (SecurityException securityException) {
       Log.e(TAG, "Lost location permission. Could not remove updates. " + securityException);
   }
}

We can call the request/remove methods at any time, but for our purposes, we remove location updates inside the service’s onDestroy() method:

@Override
public void onDestroy() {
   ...
   removeLocationUpdates();
   super.onDestroy();
   ...
}

Step 4: Stop foreground service

When we want to stop the foreground service, we use an intent similar to the one used when the service was started:

private void stopLocationUpdateService(Context context) {
   Intent serviceIntent = new Intent(context, LocationUpdateService.class);
   context.stopService(serviceIntent);
}

This method is typically called from Activity or Fragment. For example, it can be bound to a Button’s click listener, as well as a method for starting the service. When stopService is called, the system, among other things, calls service’s onDestroy() method where we remove location updates.

Step 5: Start foreground service after device restarts

For receiving boot-completed system intent, we will use BroadcastReceiver:

public class BootReceiver extends BroadcastReceiver {
   @Override
   public void onReceive(Context context, Intent intent) {
       if(intent.getAction() != null && intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
           //Call start service
       }
   }
}

In order to receive broadcasts, this receiver needs to be registered within the app’s Manifest:

<receiver android:name=".BootReceiver">
   <intent-filter>
       <action android:name="android.intent.action.BOOT_COMPLETED"/>
   </intent-filter>
</receiver>

Don’t forget the permission:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

Note that the user needs to have started the application at least once before you can receive the BOOT_COMPLETED action. The receiver is automatically registered, and can be disabled and enabled using these methods:

public static void enableBootReceiver(Context context) {
   ComponentName receiver = new ComponentName(context, BootReceiver.class);
   context.getPackageManager().setComponentEnabledSetting(receiver, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
}

public static void disableBootReceiver(Context context) {
   ComponentName receiver = new ComponentName(context, BootReceiver.class);
   context.getPackageManager().setComponentEnabledSetting(receiver, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
}

With the boot-completed receiver, we can start our service as soon as the device restarts. The system sends a broadcast that is received by our registered receiver and the onReceive(Context context, Intent intent) method is called. Here we check if intent action is equal to the one we are expecting and can proceed with service restart.

Wrap Up

For more flexibility, we can combine this service with an activity recognition in order to collect locations only when the user is moving or performs a specific activity, like running or driving a bike. Also, there is a possibility of setting the time frame in which the service works. The possibilities are numerous and will be the subject of our future articles.

This entry was posted in Mobile Applications and tagged Web Development, SyncIt Group, Web, Mobile Apps, Mobile App Development, Android, Android App, Android 10, Location Updates on April 22, 2020 by Neda Milosavljević, Senior Android & iOS Developer .