Blog

Blog not found? Check your connection and try again.

David Jones
Jul 06, 2017

Somehow, despite living in a country where strong mobile and wifi networks are commonplace, I still regularly find myself confronted with apps asking me to refresh the screen, or try my previous action again. Often, this can be anticipated; I know that as I walk away from my flat in the morning, at some point my phone will drop off the wifi and reconnect to the mobile network.

If I was using an app attempting to connect to a server, then the request might not get through. This isn’t always the case, and my connection is solid for the vast majority of the time. However, when it isn’t, it can be a frustrating experience.

This is very much a first world problem, I know. It may seem tedious, or overkill, to worry about such small disconnects and error states in an app, but it is a significant problem both for new apps and those aimed at a global audience. If a new application is focused on gaining traction in a crowded marketplace, every moment counts. If users experience frustration, or are prevented from doing what they want to do, that’s a reason to uninstall and find something else.

Then there’s the rest of the world to consider. While many of us enjoy high-speed mobile networks and wifi everywhere we go, technology continues its incessant march around the globe. The next billion users, however, will not have the privilege of existing super-fast 4G networks, with many of them still relying on GSM and EDGE. Despite continuing investments, these infrastructures take years to build, so if we want to create apps for such users, we need to build them for the network they have now, not the one they will have in a few years.

 

image3.png

Graph from the Ericsson Mobility Report 2016 displaying current and estimated mobile network subscriptions 

 

So how do we develop apps that combat this problem? A user’s network is not something an app developer can control. Nor is it something that can be predicted. As such, the only real way to protect a user against connectivity problems is to never assume they will have strong connectivity in the first place. Most apps already implement this in a fairly simple way; there may be a check to ensure the user has an active connection before any network communication. If this is not available, it typically results in a ‘check your connection’ screen or a message informing the user they have no connection and should therefore try again later.

The problem with this solution is that the burden of fixing the issue now lies with the user. Their device might only have been disconnected for a second, which means they can retry straight away. At other times, they may experience connection issues for several minutes, leaving them stuck as they are forced to retry over and over again. On other occasions, the user might not even be aware of the problem, leading them to blame the app rather than the network.

 

image1.png

 WhatsApp demands a network sacrifice in order to update your avatar, please select and crop your image again

 

Some apps therefore incorporate a more intensive approach: registering with the system’s ConnectivityManager framework. This allows the app to register a broadcast receiver, which is notified of changes in connectivity as they happen. The app can thereby manage its own queue of tasks and handle waiting for a successful connection.

Unfortunately, this is a really bad approach. Not only is it expensive to run an app in the background, listening for connectivity; there is likewise no way to filter these broadcasts down. As such, any app that receives connectivity updates must receive all of them; low connectivity, disconnected, reconnected, etc.

This is so wasteful that, as of the release of Android 7.0 (Nougat), apps can no longer register for connectivity broadcasts in their manifest files, so must be active when listening. Therefore, assuming the old adage of ‘Google giveth and Google taketh away’ is true, what support does Android have for managing network tasks now that ConnectivityManager is out of the picture? In Android 5.0 Lollipop, Google added an API called Job Scheduler, which allows an app to schedule various types of jobs against the framework that guarantees these jobs will be completed.

Job Scheduler is a different way of looking at background tasks, as it doesn’t execute work based on time, but instead based on the current system conditions. This allows the system to perform actions such as batching jobs together, persisting jobs over device resets, scheduling jobs for later, running jobs once connected to a network or power, and even being aware of devices’ current memory pressure for balancing intensive tasks.

When introducing a new way of looking at background tasks, however, we also need to consider how we keep data in sync and the user informed. Job Scheduler relies a lot on the principle of acting locally and syncing globally, whereby data models are persistent, updated from external sources, and utilising callbacks to keep the user's state up-to-date.

For instance, as Job Scheduler guarantees that the job will be executed, when a user performs a task we can update the locally stored data on the device and notify the UI before we even begin the background work. To the user, it appears the request was instantaneous and the UI has already updated.

We can then perform the work in the background, and any amendments to this data can be made later during the task, even if the amendment involves rolling back the changes due to a failure. Doing this keeps the UI snappy and responsive, but it does come with some caveats like increased difficulty when it comes to managing errors and requirements on design and architecture.

 

Act Locally, Sync Globally

- Yigit Boyar, Android Toolkit Developer

 

Those responsible for design and architecture need to be involved and aware of such issues when building an app using these principles, as it is not simply down to the developers’ implementation. Rather, the way the data models are architectured must be considered, as must the design of the user interface. It is important to ensure the data can be created and stored locally, for instance. This may require a variable indicating the current synced status. Designers must also be aware of the flow the data will take and what information the user should actually see relating to this state.

Now that we’ve discussed the benefits of Job Scheduler, the question remains as to how we actually go about implementing it. The following example takes the idea of an automated home chatbot (because the best apps are those that smash two concepts together) where users can communicate in a chat-style interface with their home devices, such as lighting and the wifi router. This allows a fairly simple UI, which displays the chain of messages along with the state a message is currently in.

 

 

On Android, there are many ways to implement Job Scheduler. As mentioned above, the native API is only available on Android 5.0 (Lollipop) and above. However included in Google Play Services is an API called “GCM Network Manager” which performs many of the same tasks and supports all the way back to Android 2.2 (Froyo). In addition to this, there are several libraries that act as wrappers around the GCM Network Manager and Job Scheduler, including Firebase Job Dispatcher, Evernote Android Job, and Android Priority Job Queue.

As opposed to a straight up native approach, this example uses the Android Priority Job Queue library by Yigit Boyar to implement Job Scheduling. There are several reasons for this, including maintaining compatibility. Priority Job Queue uses Job Scheduler when available. However, when the operating system is older than Android 5.0, it will switch to Network Manager, provided Google Play Services is available. When it isn’t, it can instead make use of Alarm Manager.

In addition, while the native Job Scheduler is intended for tasks that can be deferred indefinitely, Priority Job Queue is designed to be used for all background tasks. It gives developers a greater amount of control over functionality such as persisting jobs across restarts, queuing jobs to run sequential and prioritising jobs against each other. These benefits led me to use this library, as opposed to any other approach, and allowed all background tasks to use the Job Queue instead of Async tasks or services.

 


/**
* {@link Job} used to send messages and receive responses
*/
public class SendMessageJob extends BaseJob {
  private static final String TAG = SendMessageJob.class.getSimpleName();

  private static final int PRIORITY = 1;

  private Message mMessage;

  /**
   * Creates a new {@link Job} to send a {@link Message}, it sets up the Job to require a network
   * connection, persists it across app closes and resets, and sets a tag.
   *
   * This constructor also checks the network connection to set the correct sync status. This check
   * is unnecessary but used here to update the UI in this example to show users the message will
   * send once the connection has been reestablished.
   *
   * @param context    {@link Context}
   * @param message  {@link Message} to send
   */
  public SendMessageJob(Context context, Message message) {
     super(new Params(PRIORITY)
        .requireNetwork()  // Prevent the job from executing without a network connection
        .persist() // Persist this job across app closures and device resets
        .addTags(TAG)  // The TAG is used for identification and can be used for cancelling or querying jobs
     );

     mMessage = message;

     if (CommonUtils.isNetworkConnected(context)) {
        mMessage.setSyncStatus(SyncStatus.PENDING);
     } else {
        mMessage.setSyncStatus(SyncStatus.SEND_ONCE_CONNECTED);
     }
  }

  /**
   * onAdded is called once the {@link Job} has been scheduled and serialized to disk, guaranteeing
   * that this {@link Job} will run.
   */
  @Override
  public void onAdded() {
     ProviderUtils.insertOrUpdateMessage(getApplicationContext().getContentResolver(), mMessage);
  }

  /**
   * The actual method that should to the work.
   * It should finish w/o any exception. If it throws any exception,
   * {@link #shouldReRunOnThrowable(Throwable, int, int)} will be called to
   * decide either to dismiss the job or re-run it.
   *
   * @throws Throwable Can throw and exception which will mark job run as failed
   */
@Override
public void onRun() throws Throwable {
  ContentResolver contentResolver = getApplicationContext().getContentResolver();

  mMessage.setSyncStatus(SyncStatus.SENDING);
  ProviderUtils.insertOrUpdateMessage(contentResolver, mMessage);

  try {
     // Fake network latency
     Thread.sleep(1000);
  } catch (Exception e) {
     Timber.e("Thread sleep interrupted");
  }

  // If the message contains fail, act like the job was cancelled for the example
  if (mMessage.getMessage().toLowerCase().contains("fail")) {
     onCancel(-1, null);
     return;
  }

  Response response = mAutoHomeAPI.sendMessage(mMessage).execute();

  if (response.isSuccessful()) {
     mMessage.setSyncStatus(SyncStatus.SENT);
     ProviderUtils.insertOrUpdateMessage(contentResolver, mMessage);

     ProviderUtils.insertOrUpdateMessage(contentResolver, response.body());
  } else {
     mMessage.setSyncStatus(SyncStatus.FAILED);
     ProviderUtils.insertOrUpdateMessage(contentResolver, mMessage);
  }
}

  /**
   * Called when a job is cancelled.
   *
   * @param cancelReason Reason the job was cancelled
   * @param throwable The exception that was thrown from the last execution of {@link #onRun()}
   */
  @Override
  protected void onCancel(int cancelReason, @Nullable Throwable throwable) {
     mMessage.setSyncStatus(SyncStatus.FAILED);
     ProviderUtils.insertOrUpdateMessage(getApplicationContext().getContentResolver(), mMessage);
  }

  /**
   * If {@code onRun} method throws an exception, this method is called.
   *
   * @param throwable The exception that was thrown from {@link #onRun()}
   * @param runCount The number of times this job run. Starts from 1.
   * @param maxRunCount The max number of times this job can run. Decided by {@link #getRetryLimit()}
   *
   * @return A {@link RetryConstraint} to decide whether this Job should be tried again or not and
   * if yes, whether we should add a delay or alter its priority. Returning null from this method
   * is equal to returning {@link RetryConstraint#RETRY}.
   */
  @Override
  protected RetryConstraint shouldReRunOnThrowable(@NonNull Throwable throwable, int runCount, int maxRunCount) {
     return RetryConstraint.createExponentialBackoff(runCount, 1000);
  }
}

 

So what’s actually going on here? When a user clicks the send button in the UI, the job constructor is run, which sets up the Job parameters. These include ‘requireNetwork’, which indicates the job can run only when there’s an internet connection, and ‘persist’, which guarantees the job will eventually run even after device reboots etc.

Right after the Job has been synchronised to disk, since a dependency injector has been defined, the Retrofit API is injected into the Job (see the BaseJob class), which allows us to access the mocked Auto Home API. Following this, almost immediately the onAdded method is called, indicating a Job has been accepted. It is here where we can inform the UI to display the message data to the user, with the “Pending” or “Waiting for connection” sync status.

Once all the conditions as defined by the Jobs constructor have been met (which in this case means Internet connection is available) the onRun method is called, which contains the actual work of this job, and naturally gets executed in the background. 

Once the conditions are met, the Job Queue is then responsible for continually calling the onRun method until it either completes successfully or reaches a retry limit. In the example, the onRun method simply calls through to a Mocked Retrofit API, which adds a few delays, then creates a response message. onRun then adds this response to the data store.

If an exception is thrown in onRun, then the shouldReRunOnThrowable method gets called, informing the job whether it should be retried or not. If this method returns false, or all retry attempts have failed, then the onCancel method gets called. Here, the data is updated to reflect the fact the message was unable to send. We can also use this method to remove the message from the database, and inform the user etc.

 

 

Finally there is the matter of storing and persisting data. On Android, the Context class is easily considered a God object. It provides information about the app environment, access to resources and classes, as well as a whole lot more. Context is often used to store data, whether that be through the app’s shared preference, accessing a file, or accessing a database.

However Contexts are also responsible for a wide variety of memory leaks in Android apps, as they are often passed around a system, held in objects and background tasks, outliving their own lifecycle. Fortunately, Android Priority Job Queue also handles this for us by providing an instance of the Application Context at any point during the job’s execution.

There are many ways to persist data and update the UI on Android, such as ORM databases, EventBus, or even RxJava/RxAndroid. For this example, however, the messages are stored in a SQLite database, wrapped in a Content Provider. This allows direct access to the data by getting the Content Resolver from the provided Applications Context. Using a Content Provider was useful in this example for several reasons - firstly because with access to context, it is trivial to access a Content Resolver that handles executing database queries, including insert and update; and secondly because of the Content Observer capabilities.

Content Observers, classes that can be registered with a Content Provider, listen for updates to the underlying data. As such, in this example, the main fragment creates and registers an observer for the Messages table.

Whenever we update anything on this table (using the Content Provider), this class receives a callback which then notifies the RecyclerViews adapter to update the data displayed to the user. Since the observer is registered and unregistered in the Fragments onStart and onStop methods respectively, it also means that the callback does not need to worry about the state of the Fragment, if it’s alive, or even if it’s attached to an activity.

 


/**
* {@link ContentObserver} to update the {@link RecyclerView} with the updated {@link Message} data
*/
private class MessageObserver extends ContentObserver {
  /**
   * Creates a content observer.
   *
   * @param handler The handler to run {@link #onChange} on, or null if none.
   */
  MessageObserver(Handler handler) {
     super(handler);
  }

  @Override
  public void onChange(boolean selfChange) {
     this.onChange(selfChange, null);
  }

  /**
   * Retrieve the latest {@link Message} data from the database and update the {@link MessageAdapter}
   * @param selfChange   True if this is a self-change notification
   * @param uri  {@link Uri} of the changed content, or null if unknown
   */
  @Override
  public void onChange(boolean selfChange, Uri uri) {
     if (uri == null) return;

     Cursor cursor = getContext().getContentResolver().query(uri, null, null, null, null);
     if (cursor != null && cursor.moveToFirst()) {
        Message msg = ProviderUtils.cursorToMessage(cursor);
        mMessageAdapter.updateMessage(msg);
     }
     if (cursor != null) {
        cursor.close();
     }

     mRecyclerview.scrollToPosition(mMessageAdapter.getItemCount() - 1);
  }
}

 

Using Job Scheduler is not bulletproof. It won’t suddenly fix all the problems associated with networking, asynchronous tasks, and the Android lifecycle, and it actually makes informing users about errors even more difficult. But it’s a start, and one that decouples application logic away from activities and fragments, provides a clear system for dealing with network communication, and avoids using Async Tasks and dealing with their life cycle.

It’s also worth noting that there is a whole lot more to Job Scheduler than networking; this post barely scratches the surface. Android has been making steps towards locking down how Android apps behave in the background for a while now, and Android 8.0 (O) looks to continue this trend. Job Scheduler and similar services are the replacements, designed to be smarter and more coordinated with the system as a whole, aiming to improve Android performance and battery life. 

As always, there is still a lot of room for improvement and a long way to go. But the source code can be found my gitlab repository (get in touch if you want access), or you can use the app in your browser here. Comments and pull requests are welcome.

If you found this topic interesting, and want to know how to maximise the benefits of Job Scheduler with functionality such as live data observables and SQLite database ORMs, check out the "Architecture Components" talks from Google I/O 2017.

 


 

Did you know that Android O lets users group notifications into a notification channel?

the_best_android_features_you_should_know-2-2.jpg

 

Learn about the new features announced in Android O

 

Want to work with us?

Get in touch