How We Do Automatic Token Refreshing using Android’s New Work Manager
Rolling with the new black…
It’s no surprise that the Android ecosystem has been fragmented more than the fragmentation you used to see in your Windows’ disk defragmentation tool. With new APIs breaking old ones or discontinuing support frequently, it was hard to get things done in a reliable and future-proof way.
Naturally, when we needed to run automatic periodic token refreshing in our Android SDK, the first question that popped in our heads was which API to use.
There was JobScheduler
on one side, which had support only from Android Lollipop and upwards. There was AlarmManager
which had support for versions before Android Lollipop but failed to survive the wrath of Marshmallow’s new Doze mode.
Well, thank you, Google. You’ve successfully made our lives miserable.
Just before all hope was lost, Evernote’s wonderful engineering team came up with a solution to this gap that has been created. Their Android-Job library bridged the gap by providing a unified API to perform all your background scheduling.
But then, a miracle happened. Finally, years after they committed the sin, our friends at Google realised this gap and tried to bring a standard solution to this nuisance. Thus, WorkManager was born.
Given the perfect timing of WorkManager’s arrival and all the praises by developers around the world, it was an obvious choice that we use this for our SDK instead of using some third-party library.
But before diving into the specifics of the project, let’s have a quick look at what were our requirements and why WorkManager was the perfect fit.
What was the job to be done?
For the Mitter Android SDK to be friendly, it needed to deal with its own problems without constantly nagging the developers for their time and effort. One such problem was automatic token refreshing.
You see, at Mitter, the authentication tokens are valid for only a day. This means that if the SDK doesn’t grab a new token before the old one expires, all hell breaks loose.
Also, it would be very irritating for the developers if the SDK relied on them to schedule jobs for renewing tokens. This was a basic functionality that needed to be embedded into the heart of the SDK.
Therefore, we decided the SDK would fetch a new token from the platform and revoke the old one, every hour, automatically in the background.
Why every hour?
Because, even if one window is missed due to poor connectivity or the device being switched off, the SDK would still have a chance to grab a new token in the new cycle. Thus, keeping the cycle short played as an advantage.
Now, since we needed to run background jobs, we started looking out for reliable yet out-of-the-box solutions to do background processing without breaking our heads over it. You know, Android can be messy sometimes. **Burns**.
Behold, WorkManager
With Android’s new arsenal of libraries bundled under the name Jetpack, came the answer to our problems — WorkManager. It was perfect, from the first look. It was from the Android team, had a unified API which worked across a wide range of Android versions and easy to implement.
But, developers are generally skeptical. We didn’t want to jump into this new thing without giving it a deep c-section. So, we did.
Turns out, everyone loved it. And our search came to an end.
We decided that we would go with WorkManager to perform our background jobs, which, right now is only the token refreshing process.
Now, let’s have a look at how we actually implemented the library to achieve what we wanted.
Preparing the token refresh job
Our job was simple:
- Get a new token against the existing one
- Revoke the old token
This translated to two serial API calls in the background and some SharedPreferences
access to update the auth token. Using WorkManager
this was a walk in the park.
Now, this article will not be covering the specifics of WorkManager and how to schedule your jobs. You can find plenty of tutorials for that. One being this lovely article.
Keeping that aside, let’s see how WorkManager worked for us.
Firstly, we created a RefreshTokenWorker
class which extended the Worker
class provided by the WorkManager library. This is the first step to getting your jobs done by WorkManager.
All of our API calls and local storage strategy, went into our freshly minted RefreshTokenWorker
class as follows:
class RefreshTokenWorker : Worker() {
override fun doWork(): Result {
/** Initializing credentials manager code goes here **/
val existingTokenId = credentialsManager.getToken().id
val authToken = try {
credentialsManager
.issueToken()
.blockingGet()
} catch (exception: Exception) {
return Result.RETRY
}
credentialsManager.saveToken(authToken)
try {
credentialsManager
.revokeTokens(listOf(existingTokenId))
.blockingGet()
} catch (exception: Exception) {
return Result.SUCCESS
}
return Result.SUCCESS
}
}
One of the great things about WorkManager is the ability to effortlessly retry your jobs with a single flag, in case it has failed.
As you can see in the example above, we make use of 2 flags to decide whether to retry the token refresh job or not:
Result.SUCCESS
Result.RETRY
This lifted a huge amount of work from our plate. Now, even if the job failed at some point due to network issue or any other cause, it would be retried because the api call to get a new token would fail.
Now, we didn’t mark the token revoking job as retry-able because that is not a critical API call. Even if the call fails, it doesn’t break anything. The app still has the new token and the old token will be swept out of the system after it has expired, anyways.
Scheduling the job
Well, now that our little worker’s ready, the only thing that was left to do is to put it on a routine.
First, we needed to define a unique job name, so that if a new job came in while a previous job with the same name was waiting to be executed, the new job would replace the previous one. This way, there would always be only one job from the SDK in the queue.
Let’s define the name:
uniqueWorkName = "${getApplicationId()}:refresh-token-work"
Here we chose to prefix the word refresh-token-work with an application ID because the SDK is capable of managing multiple applications at once. This ensures that a job from one application doesn’t replace the jobs of the other Mitter applications.
Now that we got the job uniqueness in place, it was time to schedule the job. This was done using the following piece of code:
fun startPeriodicRefreshTokenTask() {
val workConstraints = Constraints
.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val applicationData = Data.Builder()
.putString(Keys.APP_ID, getApplicationId())
.putString(Keys.LOGGING_LEVEL, mitterConfig.loggingLevel.toString())
.build()
val periodicRefreshTokenWork = PeriodicWorkRequest.Builder(
RefreshTokenWorker::class.java,
1,
TimeUnit.HOURS
).setConstraints(workConstraints)
.setInputData(applicationData)
.build()
WorkManager.getInstance()
?.enqueueUniquePeriodicWork(
uniqueWorkName,
ExistingPeriodicWorkPolicy.REPLACE,
periodicRefreshTokenWork
)
}
Here, we did a couple of things:
- We set a work constraint to run the job only if an active internet connection is available
- We prepared some custom data to be passed to the worker
- We prepared the job with the constraint and the input data and also set a periodic running interval of 1 hour
- We enqueued the job to WorkManager, to be run as and when necessary
And that was all that was needed to be done. Our job was up and running every hour. That was easy, wasn’t it?
How reliable is this method?
I know, you might be wondering, given the fragmentation and new optimisations in Android, what’s the guarantee that this job will run?
Well, WorkManager performs excellently in pretty much all cases and devices. We’ve tested the periodic task for a stretch of 2 days, it worked without fail. Therefore, it’s safe to say that it’s a good choice to run your period background tasks.
Nevertheless, we did notice some hiccups on a handful of devices. Especially, OnePlus phones. OnePlus has this weird battery optimisation strategy where it pretty much cancels every background activity to save your battery.
WorkManager was no exception. We had to specifically mark our app as “Don’t Optimize” under the battery settings for our job to run. However, after making this quick change, the job ran smoothly.
Do note, that these kinds of optimisations are at the OS level differing from manufacturer to manufacturer and no API can absolutely guarantee a background job execution on a 100% of Android devices. Even apps like WhatsApp and Gmail fall prey to this.
Nevertheless, you can always ask your users to turn off optimisations for your app if this issue occurs.
Looking forward
Using WorkManager has been a total delight. As you have seen in this article, scheduling a periodic retry-able job in the background was pretty straightforward.
Whether you’re creating a new app, or refactoring an existing one, we would highly recommend using WorkManager to do your job scheduling. It’s truly effortless and well supported across versions.
Liked this article? Please 👏🏻 👏🏻 👏🏻 to show your love and encouragement to write more. 😍
This blog was written by Rahul —
the Android guy at Mitter.io who took the whole messaging thing a bit too seriously and now refuses to communicate using speech. You’ll find him at his desk, calmly typing out code, or his next message.
Follow our blog for more stories from the Mitter.io team, and their quest to find the right tools and the best solutions for all things dev.