Background BLE scan in DOZE mode on Android devices

Written by navigine | Published 2020/02/21
Tech Story Tags: android | mobile-sdk | programming | startups | coding | technology | iot | java

TLDR Background BLE scan in DOZE mode on Android devices could be divided into two different groups depending on the mechanisms used for scheduling background tasks. Most of the new solutions will not work for old Android versions and all the old solutions will be killed by Android in new versions. We will review two different strategies for scanning in the background — for versions before API 26 (Oreo) and after. Android will not allow starting the service when your app is killed so here we need better solutions. Each solution could be used if you will decide not to fight with Doze mode.via the TL;DR App

Hi there! We are the Navigine team. For 8 years we have been providing integrated positioning mobile technologies that enable advanced indoor navigation and proximity solutions. Today we decided to open the doors to our technology and talk about how to scan BLE devices when the Android application is killed and in background mode.
Background BLE scans for Android devices could be divided into two different groups depending on the mechanisms used for scheduling background tasks. During the last few years, Android changed background processing and added Doze mode, limited implicit broadcasts, and limited background behavior and. But most of the new solutions will not work for old Android versions and all the old solutions will be killed by Android in new versions. So in this article, we will review two different strategies for scanning in the background — for versions before API 26 (Oreo) and after.

Background tasks in Android devices with an API level less than 26 (Oreo)

For all old android versions up to Android Oreo, there is a working method for scanning in the background using alarm manager and service with the broadcast receiver. The broadcast receiver will get wake up alarm and start service where Navigine SDK will be initialized and started for 10 seconds. After this time SDK will be finished and service will be killed. This alarm could be set up with at least a one-minute intervals and will work without any restrictions in old devices.
public class NavigineReceiverExample extends BroadcastReceiver {
  
  // here you need to receive some action from intent and depending on this action start service or set repeating alarm
  public void onReceive(Context context, Intent intent) {
    String  action = intent.getAction();

    try{
      if (action.equals("START")) {        
        PendingIntent pendingIntent = PendingIntent.getBroadcast(
            context, 0,
            new Intent("WAKE"),
            PendingIntent.FLAG_UPDATE_CURRENT);

        AlarmManager alarmService = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
        if (alarmService != null)
          alarmService.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5000, 60 * 1000, pendingIntent);
      } else if (action.equals("WAKE")) {
        context.startService(new Intent(context, NavigineService.class));
      }
    } catch (Throwable e) {
      Logger.d(TAG, 1, Log.getStackTraceString(e));
    }
  }
}
NavigineReceiverExample.java for devices with an API level less than 26
Inside the service class, you can declare handling intent from the broadcast receiver and starting Navigine SDK or other manipulations you want to do. This solution will not work in Android devices with the version starting from 26 because Android will not allow starting the service when your app is killed so here we need better solutions.

Background tasks starting from Android Oreo

Starting from API 26 Android started blocking services running when the device is killed and provided few solutions for running your tasks in the background. But the main difficulty is, these solutions also will not work in Doze mode (If a user leaves a device unplugged and stationary for a period of time, with the screen off, the device enters Doze mode). Here is the restrictions list when app entered Doze mode:
Screenshot from Google developers website
Further, we will talk about each solution that we found and will provide code examples. Each of them could be used if you will decide not to fight with Doze mode. As we need to scan BLE devices every few minutes we need to run periodic background tasks. Also, we will provide two more solutions that will ignore Doze mode but will need more resources or will have extra notifications.
Periodic Job Scheduler.
Job Scheduler could be used starting from Android API level 23 and replaced Service + Alarm Manager for running background tasks. Periodic Job Scheduler could be waked up at least every 15 minutes and has a lot of opportunities if you need some conditions for your job scheduler like requiring to run only if the device has an internet connection, should be charging, etc. For using this method you should extend the JobService class and override onStartJob method for starting your task. And start Job Scheduler to run a periodic job. Do not forget to add your service to your manifest file. Here is the code example of how to start periodic job scheduler:
JobScheduler jobShedulerService = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobShedulerService.schedule(new JobInfo.Builder(ID, new ComponentName(context, NavigineJobService.class.getName()))
  .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // any network type
  .setPeriodic(15 * 60 * 1000) // every 15 minutes
  .setRequiresCharging(false)  // if device should be charging
  .build());
Periodic job scheduler
This solution runs perfectly while the device is not in the Doze mode, otherwise, it will just skip jobs and start new periods. In the screenshot below you can see how well it works. In this particular case, we added flex-time to the periodic job and it starts earlier than it should.
Logcat screenshot for periodic job scheduler.
Alarm with .setAndAllowWhileIdle() and JobScheduler.
Common Alarm Managers with .set() or .setRepeating() will work well only when the device is not in Doze mode because Android will ignore them and reschedule for next time. Using .setAndAllowWhileIdle() will allow the alarm to be called, but the job scheduler will be ignored even in this case. 
However, you can add . setOverrideDeadline(time in milliseconds) method to your job scheduler and it will run this scheduled job when the device exits Doze mode. Here is the code example for the onReceive method of our BroadcastReceiver extension:
public class NavigineBroadcastReceiverExample extends BroadcastReceiver {
  
  // here you need to receive some action from intent and depending on this action start service or set repeating alarm
  public void onReceive(Context context, Intent intent) {
    String  action = intent.getAction();

    try{
      if (action.equals("START")) {   
        Intent wakeIntent = new Intent("WAKE");
          wakeIntent.setClass(context, YourReceiver.class);
        
        PendingIntent pendingIntent = PendingIntent.getBroadcast(
            context, 0,
            wakeIntent,
            PendingIntent.FLAG_UPDATE_CURRENT);

        AlarmManager alarmService = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
        if (alarmService != null)
          alarmService.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 15 * 60 * 1000, pendingIntent);
      } else if (action.equals("WAKE")) {
        
        JobScheduler jobShedulerService = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        jobShedulerService.schedule(new JobInfo.Builder(1, new ComponentName(context, YourJobClass.class))
                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
                .setMinimumLatency(0L)
                .setOverrideDeadline(15 * 60 * 1000)
                .build()); 

        Intent wakeIntent = new Intent("WAKE");
        wakeIntent.setClass(context, YourReceiver.class);

        PendingIntent pendingIntent = PendingIntent.getBroadcast(
            context, 0,
            wakeIntent,
            PendingIntent.FLAG_UPDATE_CURRENT);

        AlarmManager alarmService = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
        if (alarmService != null)
          alarmService.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + Default.NAVIGINE_JOB_SERVICE_WAKEUP_TIME * 1000, pendingIntent);
      }
    } catch (Throwable e) {
      Logger.d(TAG, 1, Log.getStackTraceString(e));
    }
  }
}
Broadcast Receiver example using Alarm Manager and Job Scheduler
In our case, we decided to kill these jobs if more than 30 minutes passed after they have been scheduled. This solution also will not solve the problem with running background tasks in Doze mode, but jobs will not be skipped like in a periodic job scheduler. Keep in mind, that even in Doze mode these alarms will be ignored if you try to call them more then once every 9 minutes.
Waking up every 5 minutes with an Alarm Manager and starting Job Scheduler. (Not in Doze mode)
Unfortunately, none of these methods will allow you to execute background tasks in Doze mode, even WorkManager will not help you with this task. But there are few other solutions you can use.
Running foreground service.
Foreground services will run without any cancels and without entering Doze mode, so your background tasks will run infinitely. But this solution has one disadvantage, your app will always have pinned notification in the devices’ status bar. 
Screenshot of foreground service running after the app has been killed.
As you can see in this screenshot, the user will always see this notification which could not be canceled. Using foreground service will increase the chances that your application will be removed faster than it could be. Also, it’s worth noting that your application will use more power and device resources.
Here is the foreground service code example:
public class ForegroundService extends Service {

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String input = intent.getStringExtra("inputExtra");
        createNotificationChannel();
        
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, YourActivity.class), 0);
        
        Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle("Foreground Service")
                .setContentText(input)
                .setSmallIcon(R.drawable.ic_stat_name)
                .setContentIntent(pendingIntent)
                .build();
        
        startForeground(1, notification);
        
        // here is the code you wanna run in background
        
        return START_NOT_STICKY;
    }

    private void createNotificationChannel() {
        NotificationChannel serviceChannel = new NotificationChannel(
                CHANNEL_ID,
                "Foreground Service Channel",
                NotificationManager.IMPORTANCE_DEFAULT);
      
        getSystemService(NotificationManager.class).createNotificationChannel(serviceChannel);
    }
}
Foreground Service example
Using push notifications.
Another way to run background tasks even in Doze mode to send notifications to your application from the server. These notifications will force your application to exit Doze mode and yourи job could be done. But in this case, you will need to spend more resources on running and maintaining your service. You can find more info about using Firebase Cloud Messaging to run your background services by this link

Conclusion

Over time Google hardens the ability to run background tasks, so developers need to come up with new solutions and tricks, like sending push notifications for waking the device up from Doze mode. In defense of the android, we can say that they are using their actions to protect users from unwanted and unnecessary actions in the background that will use the phone’s resources and perform unwanted, and sometimes malicious tasks. By the way, the above solutions will help you to work respecting the requirements of Android or bypass the Doze mode.

Published by HackerNoon on 2020/02/21