Real-time Location Updates on Android 10

Android 10 Location Updates

Developing a mobile application that requires real-time precise location updates can be tricky. Even more so if it is supposed to deliver updates while the user is not interacting with the app.

Needless to say, you cannot neglect battery consumption. Android reserves the right to suspend apps that are using too much battery power. What is more, users can easily delete an app that doesn’t suit them. On top of it all, there are Android’s background location restrictions. These surely make developers’ lives even more complicated.

But, there is no need to worry. Here is a complete guide on how to create this kind of 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 you call requestPermissions, you will see a dialog and be able to grant or deny permission.

Also, you can manage the return of the requestPermissions method call in onRequestPermissionResult method callback. In the callback, you can check if the user has granted permission or not. If they have, you can (in this case) start the 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 stated in the 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 to this “problem” is that we run a foreground service that can run regardless of the application’s activity.

First, you need to 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" />
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. 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.

Once 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, values like 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 the network location is used. This is very useful when locating indoors or when GPS signals interfere with buildings. It’s easier to calculate the location via routers that surround us in these modern times.

Location Updates

We do these initializations inside the onCreate() method, right after creating the service. Then, we can start with location updates. In our case, the 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);
   }
}

When needed, you can stop location updates 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:

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 you can disable or enable it by 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. Our registered receiver receives it and calls the onReceive(Context context, Intent intent) method. Here we check if intent action is equal to the one we are expecting and can proceed with service restart.

Wrap Up

And that’s it! Now you have an app for Android 10 with real-time precise location updates. It is active even when the user is not and it saves your battery!

Of course, this is just the beginning. For more flexibility, we can combine this service with activity recognition. It can be set to collect locations only when the user is running, driving a bike, etc. Also, it is possible to set the time frame in which the service works.

Some of the additional possibilities will be the subject of our future articles. In the meantime, you can contact us at [email protected] for more info on Mobile App Development.

0 0 vote
Article Rating
Subscribe
Notify of
guest
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Srđan Nikolić
Srđan Nikolić
4 years ago

Very concise and informative text. I was really enjoying while reading it.
*Thumbsup*