GCMTaskService / Nougat / Services

Nougat – GCM Network Manager

Nougat has now been released in to the wild and there are a number of new features which developers should be considering. In this loosely-related series of articles we’re going to be looking at various aspects of these new features to see how we can best make use of them. In this first article we’re going to look at the ramifications that changes to doze mode have for apps running on Nougat.

nougat-smallBefore we begin let’s explain a little bit about the app we’re going to create – a pseudo-messaging app which simulates incoming messages. The reason for this is that it will enable us to explore a number of features of Nougat during the coming articles. It uses AutoValue to create immutable Java objects representing domain objects; Gson to marshall these to and from JSON. I won’t give a full explanation of AutoValue here – for those unfamiliar with it Ryan Harter has written an excellent introduction which should give you sufficient knowledge to understand how I’m using it. The Message class is an AutoValue representing a message within our messenger app; Rather than having a real messaging back-end we’re going to simulate incoming messages by running a periodic service to generate a new message – the Messenger class will generate messages based upon strings from Dirty Phrasebook.

The first think that we need to do in our simulated messaging app is to schedule a periodic task to generate messages. Traditionally we’d look to AlarmManager to do things like this, but there are hints (as voiced by Mark Murphy) that background processing may change quite considerably over coming iterations of Android, so let’s consider a more up-to-date approach which is likely to be somewhat more future-proof. The recommended way of scheduling periodic tasks is to use JobScheduler which enables you to specify finer grained criteria, such as network and / or charging states, for when your task should run. The big issue with JobScheduler is that it is only available in API 21 (Lollipop) and later. However there is a very similar component named GCMNetworkManager in the Play Services Google Cloud Messaging (GCM) package, and it’s this that we’ll utilise to schedule our periodic messages. GCM Network Manager is nothing new to Nougat as such, but we’ll use it here in response to some changes in Nougat which may suggest that AlarmManger may not be the best long term solution.

Before we continue it is worth acknowledging that we cannot use GCM in all projects – specifically those which target app ecosystems other than Google’s. However I’ve opted for an approach will will provide compatibility with more devices than simply using JobScheduler, but JobScheduler itself works in exactly the same way as we’ll be covering here. For projects which need to support pre-Lollipop devices and cannot use Play Service then they should stick with AlarmManager for now.

It is also worth mentioning that there is a work-in-progress library named Firebase JobDispatcher which will provide a wrapper around engines such as GCM Network Manager which will potentially remove the ties to the Google app ecosystem. Currently it only supports GCM Network Manager, but there’s a pluggable driver layer which will open things up on alternate ecosystems such as Amazon as soon as appropriate drivers are created. This library has not been formally released as yet and, despite the readme stating that it replaces GCM Network Manager, I see no tangible benefits of using it until it has a formal release and has more options than the solitary GCM Network Manager driver.

So, on to GCM Network Manager. The first thing we need to do is add the dependency to our build.gradle:

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'

android {
    compileSdkVersion 24
    buildToolsVersion "24.0.2"
    defaultConfig {
        applicationId "com.stylingandroid.nougat"
        minSdkVersion 17
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    apt 'com.google.auto.value:auto-value:1.2'
    provided 'com.google.auto.value:auto-value:1.2'
    compile 'com.android.support:appcompat-v7:24.2.0'
    compile 'com.google.android.gms:play-services-gcm:9.4.0'
    compile 'com.google.code.gson:gson:2.7'
    testCompile 'junit:junit:4.12'
    testCompile 'org.assertj:assertj-core:2.5.0'
    testCompile 'org.mockito:mockito-core:1.10.19'
}

Next we need to create a worker Service which will be triggered periodically to generate messages for us:

public class MessengerService extends GcmTaskService {
    private static final String TAG = MessengerService.class.getCanonicalName();

    private Messenger messenger;
    private ServiceScheduler serviceScheduler;

    public MessengerService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        messenger = Messenger.newInstance(this);
        serviceScheduler = ServiceScheduler.newInstance(this);
    }

    @Override
    public int onRunTask(TaskParams taskParams) {
        Message message = messenger.generateNewMessage();
        Log.d(TAG, message.toString());
        serviceScheduler.scheduleService();
        return GcmNetworkManager.RESULT_SUCCESS;
    }

    @Override
    public void onDestroy() {
        messenger = null;
        serviceScheduler = null;
        super.onDestroy();
    }
}

The important things here are that we must subclass GcmTaskService in order to have our Service triggered by GCM Network Manager, but GcmTaskService itself subclasses android.app.Service so it’s actually a normal Android Service under the covers.

However GcmTaskService requires us to implement an onRunTask() method and it is this which will get triggered periodically to do our work. The only thing worthy of note here is the return value. In this case we always return RESULT_SUCCESS but if we are unable to complete the task then we can either return RESULT_FAILURE which indicates that the task failed to execute, or RESULT_RESCHEDULE which also indicates a failure but will request that the task is retried after a back-off. This latter return value can be useful if the data connection drops during our processing and we can defer all of the back-off / retry logic to GCM Network Manager.

As this is an Android Service we need to register it in our manifest:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="com.stylingandroid.nougat">

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

  <application
    android:allowBackup="false"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
    tools:ignore="GoogleAppIndexingWarning">
    .
    .
    .
    <service
      android:name=".messenger.MessengerService"
      android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
      <intent-filter>
        <action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
      </intent-filter>
    </service>
  </application>

</manifest>

We need to specify the BIND_NETWORK_TASK_SERVICE permission and the IntentFilter with the ACTION_TASK_READY action to register this Service with GCM Network Manager.

All that remains is to schedule a task and this is done by ServiceScheduler:

public final class ServiceScheduler {
    private static final String TAG = ServiceScheduler.class.getCanonicalName();

    private static final String MESSENGER_ENABLED = "com.stylingandroid.nougat.messenger.MESSENGER_ENABLED";

    private static final long MINUTES_IN_SECONDS = 60;
    private static final long HOURS_IN_MINUTES = 60;
    private static final long HOURS_IN_SECONDS = HOURS_IN_MINUTES * MINUTES_IN_SECONDS;
    private static final int WINDOW_MAX_OFFSET = 8;
    private static final long WINDOW_SIZE = 30 * MINUTES_IN_SECONDS;

    private final GcmNetworkManager networkManager;
    private final SharedPreferences sharedPreferences;
    private boolean isEnabled;

    public static ServiceScheduler newInstance(Context context) {
        Context safeContext = context.getApplicationContext();
        GcmNetworkManager networkManager = GcmNetworkManager.getInstance(safeContext);
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(safeContext);
        boolean isEnabled = sharedPreferences.getBoolean(MESSENGER_ENABLED, true);
        return new ServiceScheduler(networkManager, sharedPreferences, isEnabled);
    }

    private ServiceScheduler(GcmNetworkManager networkManager, SharedPreferences sharedPreferences, boolean isEnabled) {
        this.networkManager = networkManager;
        this.sharedPreferences = sharedPreferences;
        this.isEnabled = isEnabled;
    }

    public void startService() {
        isEnabled = true;
        saveEnabledState();
        scheduleService();
    }

    public void stopService() {
        cancelScheduledService();
        isEnabled = false;
        saveEnabledState();
    }

    public boolean isEnabled() {
        return isEnabled;
    }

    private void saveEnabledState() {
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putBoolean(MESSENGER_ENABLED, isEnabled);
        editor.apply();
    }

    void scheduleService() {
        Random random = new Random();
        long nextStart = (random.nextInt(WINDOW_MAX_OFFSET) + 1) * HOURS_IN_SECONDS;
        long nextEnd = nextStart + WINDOW_SIZE;

        Task task = new OneoffTask.Builder()
                .setRequiredNetwork(Task.NETWORK_STATE_ANY)
                .setRequiresCharging(false)
                .setService(MessengerService.class)
                .setExecutionWindow(nextStart, nextEnd)
                .setTag(TAG)
                .setUpdateCurrent(false)
                .setPersisted(false)
                .build();
        Log.d(TAG, String.format("Scheduled between: %d and %d", nextStart, nextEnd));
        networkManager.schedule(task);
    }

    private void cancelScheduledService() {
        Log.d(TAG, "Cancelled Service");
        networkManager.cancelTask(TAG, MessengerService.class);
    }
}

ServiceScheduler persists whether the service is enabled or disabled to SharedPreferences (this will be important later in the series) and gives us some simple API points to start and stop things. The real area of interest for this article is the scheduleService() method.

The requirement is that we want to schedule our MessengerService periodically, but with random intervals. So we start by generating a random start time in seconds which will be up to 8 hours from now. We then need to define the end of the execution window – and this will be 30 minutes after the start. So essentially we define a 30 minute window at a random offset. When we schedule a task within this window GCM Network Manager will schedule to task to run within this window. We cannot specify an exact time because we cannot determine when the device will be in Doze mode so we rely on GCM Network Manager to trigger our Service when the device is within a Doze mode maintenance window.

Next we define our Task and the criteria for when it should be run. In our case we want to create a new Task each time we run because we have a variable offset between runs so we create a OneoffTask which will be run once and then removed.. However if we had a periodic Task with a fixed, static interval then we could use PeriodicTask instead which would run periodically until we cancelled it.

Next we define certain criteria which govern when our task should be run. In our case we don’t require a data connection, nor are we going to consume much battery so we are fairly lenient here. But if your service requires data then you should specify NETWORK_STATE_CONNECTED which will only trigger the Service when a data connection is available; and if it’s going to consume a lot of data then consider using NETWORK_STATE_UNMETERED when there is an unmetered data connection (i.e. one which isn’t charged by the byte such as WiFi) is available. Also if your service is battery heavy you may want to consider scheduling two separate service – one which is scheduled more frequently but will only be triggered when the device is charging; and another which is scheduled less frequently but will only be triggered when the device is not charging.

Next we define the Service class – in this case our MessengerService – this is the Service that will be triggered if the conditions are met during the execution window – and that is what we specify next.

We now give the Task a TAG value which identifies the Task and allows us to cancel it, if necessary. We can create multiple Tasks by using different Tag values, but if we re-use a TAG then we’ll only ever have a single Task with that TAG value.

The next thing we set is setUpdateCurrent(false). This will only schedule a new task if we don’t already have one scheduled. If we set this to true, then it would update the execution window each time this method is called and we could end up deferring the task indefinitely if it was called frequently.

The final this we set is setPersisted(). If we want our Task to survive a device reboot we can set this to true and we don’t need to manually re-schedule if the device re-boots. However, for reasons which will become apparent later in the series, we don’t want this behaviour so we’ve set it to false.

All that remains is to build() the task, and schedule it to run using a GCMNetworkManager instance.

Although there are a fair few criteria and settings which need consideration, actually creating an scheduling tasks is actually pretty easy. There are no hard and fast rules which apply to setting the criteria here – different projects and different types of Service will have different criteria. Properly understanding your Service requirements and the problem domain that the Service is trying to address should make it fairly obvious how tasks need to be scheduled.

But always be defensive – while you may specify NETWORK_STATE_CONNECTED this condition may be true when your Service is triggered but the data connection may drop while you have a network transaction in flight. In this case you ca trigger a back-off / retry by returning RESULT_RESCHEDULE. But if your Service needs a data connection in order to successfully complete then you really should not be specifying a network state of NETWORK_STATE_ANY.

So now that we have a message being generated periodically at random intervals, we’ll next look at a new feature introduced in Nougat which enables us to start and stop the Messenger Service.

The source code for this article is available here.

© 2016, Mark Allison. All rights reserved.

Copyright © 2016 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

3 Comments

  1. hi , this is a great tutorial you have just help me a lot really, and this gcm network manager at the moment deos not have a lot of tutorials that really breaks down the concept down to the basic for beginners really. so this is really handy

  2. I’d say this is not useful for no-network purposes. The tasks don’t run unless there’s a network status change, even if I don’t specify a requiredNetwork in the builder.

    From the documentation (https://developers.google.com/android/reference/com/google/android/gms/gcm/OneoffTask)

    “Note that you can request a one-off task to be executed at any point in the future, but to prevent abuse the scheduler will only set an alarm at a minimum of 30 seconds in the future. Your task can still be run earlier than this if some network event occurs to wake up the scheduler.”

    From the tests I’ve done (Nexus 5x on Android 7.0), the task won’t run unless there’s a network status change. I’ve tried setting small executing windows (0, 50 seconds), (30 seconds, 60 seconds) and nothing happens unless the network changes.

    So, it’s a great tool to schedule a task in the future when we gain network, but not for any other purpose.

    Is this behaviour different on pre-Nougat devices?

    1. That is not my experience. My test device has a permanent WiFi connection so I get no network state transitions and the example app for this series runs as expected.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.