Our migration story to Android O

Alizée Camarasa
Mar 28, 2018 · 7 min read
Image for post
Image for post

Since August 2017, Google released Android Oreo (8.0) with API 26. At BlaBlaCar, we believe it is important to keep our targetSdkVersion as up to date as possible:

  • It makes it possible to use the most recent APIs like notification channels or fonts in XML
  • It is important for security reasons, the latest APIs being optimized for security and performance
  • And finally, it includes improvements to avoid excessive use of battery and memory, allowing for a better user experience.

We released a version of our app with targetSdkVersion set to 26 around mid December 2017, last year.

A couple of days later, Google announced that all published applications on the Play Store will be required to target the most recent API level:

In the second half of 2018, Play will require that new apps and app updates target a recent Android API level. This will be required for new apps in August 2018, and for updates to existing apps in November 2018. This is to ensure apps are built on the latest APIs optimized for security and performance.

For more information see, Improving app security and performance on Google Play for years to come by Google

All new versions of Android come with some breaking changes and this one is no exception. There are two in particular in Android O:


Background execution limits

In this article I will focus on how we transition/migrate the BlaBlaCar application, but you can use services and broadcast receivers for many purposes. For more information, checkout the migration guide and this article from Joe Birch.

Background services

Image for post
Image for post
Our app before we fixed background services

To work around this limitation, a few options can be considered. Foreground services and scheduled jobs are the ones we used. You can choose the solution that fits your need the best.

  • First option: use a foreground service

One of the services we use in our application is for uploading user profiles and car pictures. This is the type of service that can be promoted to the foreground, because it is the consequence of a user action, for which the progress can be seen in a notification.

This is what our method looked like before:

private fun sendPicture(bitmap: Bitmap) {
...
val pictureUploadIntent = Intent(context, UploadPictureService::class.java)
pictureUploadIntent.putExtra(EXTRA_PICTURE_FILE, file)
startService(pictureUploadIntent)
...
}

This service displays a push notification to explain that the upload started, and uploads the photo.

class UploadPictureService : Service(){

override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {

// display notification to show that the loading is starting
sendNotification()

compositeDisposable.add(repository.changeProfilePicture(file)
.subscribeOn(ioScheduler)
.observeOn(scheduler)
.subscribe({ onSuccess() }, { onError(it) }))
}

fun onSuccess() {
// Do things in case of success
stopSelf()
}

fun onError(error: Throwable) {
// Do things in case of error
stopSelf()
}
}

There is one caveat though. If the app is in the background when startService() is executed, the app will crash.

Even if the app is in the foreground but moves into the background just after, the service will be killed after a small amount of time even if the task is not finished.

We now need to use startForegroundService() instead of startService().

This is our method today. If the SDK version is Android Oreo or above, we start a foreground service.

private fun sendPicture(bitmap: Bitmap) {
...
val pictureUploadIntent = Intent(context, UploadPictureService::class.java)
pictureUploadIntent.putExtra(EXTRA_PICTURE_FILE, file)
startUploadPictureService(pictureUploadIntent)
...
}

private fun startUploadPictureService(pictureUploadIntent: Intent) {
ContextCompat.startForegroundService(this, pictureUploadIntent)
}

And our service looks like that now. It is launched as a foreground service, and the notification is automatically cancelled when the upload is finished.

class UploadPictureService : Service() {

override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {

startUploadForegroundService()
compositeDisposable.add(repository.changeProfilePicture(file)
.subscribeOn(ioScheduler)
.observeOn(scheduler)
.subscribe({ onSuccess() }, { onError(it) }))
}

fun startUploadForegroundService() {
val notification = NotificationCompat.Builder(context, channel_id)
.setContentText("Uploading photo…")
.setOngoing(true)
.setAutoCancel(false)

.setProgress(0, 0, true)
.build()
startForeground(1, notification)
}

fun onSuccess() {
// Do things in case of success
stopSelf()
}

fun onError(error: Throwable) {
// Do things in case of error
stopSelf()
}
}
  • Second option: use a Job scheduler

We had another service that sends the push notification token of the user to our backend when the user logged in or when the push token was refreshed.

When the token is updated, onTokenRefresh() is triggered. This may occur if the security of the previous token had been compromised, or when it is initially generated.

InstanceIDListenerService.kt (before migration)

class InstanceIDListenerService: FirebaseInstanceIdService(){
override fun onTokenRefresh() {
startService(Intent(this, SendPushTokenIntentService::class.java))
}
}

And SendPushTokenIntentService.kt

class SendPushTokenIntentService : IntentService() {

override fun onHandleIntent(intent: Intent) {
val token = firebaseInstanceId.getToken()
sendToken(token)
}
}
}

If onTokenRefresh() is called when the app is not on the foreground, the app will crash with an IllegalStateException, like in the previous example.

Our first attempt was to use FirebaseJobDispatcher library.

class InstanceIDListenerService : FirebaseInstanceIdService() {
internal val firebaseJobScheduler:AppFirebaseJobScheduler

override fun onTokenRefresh() {
...
firebaseJobScheduler.schedule(TAG_SEND_PUSH_TOKEN, SendPushTokenIntentJobService::class.java)
}
}

Our AppFirebaseJobScheduler creates a Job from a given JobService, then schedules it.

import com.firebase.jobdispatcher.FirebaseJobDispatcher
import com.firebase.jobdispatcher.Job
import com.firebase.jobdispatcher.JobService

class AppFirebaseJobScheduler constructor(private val firebaseJobDispatcher: FirebaseJobDispatcher) {

private fun createJob(tag: String, cls: Class<out JobService>): Job =
firebaseJobDispatcher.newJobBuilder()
.setService(cls).setTag(tag).build()


fun schedule(tag: String, cls: Class<out JobService>) {
val jobToSchedule = createJob(tag, cls)
firebaseJobDispatcher.mustSchedule(jobToSchedule)
}
}

For the case of the push token, the jobService is responsible for starting SendPushTokenIntentService, which sends the new push token to our backend.

import com.firebase.jobdispatcher.JobService

class SendPushTokenIntentJobService : JobService() {

override fun onStartJob(job: JobParameters): Boolean {
startService(Intent (this, SendPushTokenIntentService))
return false // the job is finished
}
}

Unfortunately, shortly after integrating Firebase JobDispatcher we noticed a spike in the number of ANRs. A quick analysis of the Trace showed that Firebase JobDispatcher was causing the application to hang randomly when scheduling a new job. If you are curious to know more about how and why, you can check the issue “Deadlock occurring when scheduling a job” that we created on their GitHub page.

Given this issue, we decided to try another library to schedule those jobs. We settled on AndroidJob library from Evernote because it uses different implementations depending on the underlying Android version (JobScheduler, GcmNetworkManager or AlarmManager).

This is how we use it:

class InstanceIDListenerService : FirebaseInstanceIdService() {
override fun onTokenRefresh() {
...
SendTokenJob.schedule()
}
}

SendTokenJob.schedule() is also called when the user logs in and logs out. It schedules the job service below, which sends the push token to our backend and Braze.

class SendTokenJob : Job() {

@Inject
internal lateinit var firebaseInstanceId: FirebaseInstanceId

companion object {
fun schedule(): Int {
return JobRequest.Builder(TAG_JOB)
. // add options
.schedule()
}
}

@SuppressLint("CheckResult")
@WorkerThread
override fun onRunJob(params: Job.Params): Job.Result {
...
val token = firebaseInstanceId.token
token?.let {
try {
sendToken(token)
} catch (e: Exception) {
return Job.Result.RESCHEDULE
}
} ?: return Job.Result.RESCHEDULE
return Job.Result.SUCCESS
}
}

There are other ways to use services with Android 8.0, like using FCM to send a notification with high priority in order to trigger a service. You can find out more on Exploring Background Execution Limits on Android Oreo by Joe Birch.

Broadcast receivers

In the BlaBlaCar application, we use it mainly for pending intent for actions on push notifications.

Image for post
Image for post

We noticed that if you define your pending intent like this:

val seeBookingIntent = Intent()
seeIntent.setAction(INTENT_SEE_BOOKING_ACTION)
return PendingIntent.getBroadcast(context, requestCode, seeBookingIntent, PendingIntent.FLAG_ONE_SHOT)

it doesn’t work anymore with Android O if you register the broadcast in your manifest. You need to precise the name of your receiver.

Note that you shouldn’t use it like in the example above because of security reasons, even before Android O. In this configuration, any app could listen to this INTENT_SEE_BOOKING_ACTION.

val seeBookingIntent = Intent(context, DriverApprovalBroadcastReceiver.class)
seeIntent.setAction(INTENT_SEE_BOOKING_ACTION)
return PendingIntent.getBroadcast(context, requestCode, seeBookingIntent, PendingIntent.FLAG_ONE_SHOT)

Notification channels

From now on, each notification should belong to a channel. The user can enable or disable notifications per channel rather than doing it for all of them at once.

In the BlaBlaCar application, we support two channels. One for transactional push notifications like when you receive a message or a booking request, and the other for marketing.

You can put everything in only one channel, but the risk is that the user disables all app notifications. Some notifications, like our transactional ones, are critical to our product. That’s why we put them in another channel, with a higher priority.

When you create a push notification, you now need to specify the ID of the channel.

NotificationCompat.Builder(context, CHANNEL_MARKETING_ID).build()

To define your channel(s):

val marketingChannel = NotificationChannel(CHANNEL_MARKETING_ID, "News, fun and rewards", NotificationManager.IMPORTANCE_DEFAULT)
newChannel.description = "Notifications about news, fun and rewards such as our Ambassador program."
newChannel.enableVibration(true)
val notificationManager = getNotificationManager()
notificationManager.createNotificationChannel(marketingChannel)

We do this at the launch of the app, in order to let the user manage channels at any time, even before he received a notification from this channel.
If there is already a channel created with this ID, it will be ignored except if you want to update an information like the name.


Image for post
Image for post

That’s it about our journey to Android Oreo! I hope this article will help you if you are going through this transition, or if you intend to do it soon.

If you have done it already, feel free to share some comments and feedback.

BlaBlaCar

The stories behind BlaBlaCar, the world’s leading multimodal mobility platform.

Thanks to amokranechentir, Gaëlle Guillot, Nazim Benbourahla, Thomas Désert, Roman Wuattier, Nicolas Tricot, Maxime Fouilleul, Mathieu Vidal, Adrien Loison, and Nicola-Marie O'Donovan

Alizée Camarasa

Written by

BlaBlaCar

BlaBlaCar

BlaBlaCar is the go-to marketplace for shared mobility, combining carpooling, buses and e-scooters. In building the future of mobility, we set ourselves high and ambitious targets, and bring tech and data to the heart of our product experience and company strategy.

Alizée Camarasa

Written by

BlaBlaCar

BlaBlaCar

BlaBlaCar is the go-to marketplace for shared mobility, combining carpooling, buses and e-scooters. In building the future of mobility, we set ourselves high and ambitious targets, and bring tech and data to the heart of our product experience and company strategy.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store