DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 967,611 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Rojan Thomas
Rojan Thomas

Posted on • Updated on

Your WorkerFactory could throw ClassNotFoundException if you rename, move or delete your Worker classes

Background

Most Android developers have come across WorkManager and have used it at least once, but in case you haven't it's a powerful and useful framework for handling the execution of background tasks with constraints.

By default the Android system creates your workers based on the WorkRequest you provided to it e.g.OneTimeWorkRequest or PeriodicWorkRequest, through its own WorkerFactory.

However in certain situations you may want to use your own WorkerFactory instead of the default one provided by the framework. One such situation is when you want to inject dependencies into your worker using Dagger. The following is a common code sample given when integrating your worker with Dagger.

class WorkerFactory @Inject constructor(
        private val workerFactories: Map<Class<out ListenableWorker>, @JvmSuppressWildcards Provider<ChildWorkerFactory>>
) : WorkerFactory() {

    override fun createWorker(
            appContext: Context,
            workerClassName: String,
            workerParameters: WorkerParameters
    ): ListenableWorker? {
        val foundEntry =
                workerFactories.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) }
        val factoryProvider = foundEntry?.value
                ?: throw IllegalArgumentException("unknown worker class name: $workerClassName")
        return factoryProvider.get().create(appContext, workerParameters)
    }
}

You would then set this as the WorkerFactory for your app as described here.

That's cool, what's the problem?

The problem

When the Android system wants to start your worker it uses the class name, seen above as the parameter workerClassName in the createWorker function.

package me.rojan.worker

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import javax.inject.Inject

class ExampleWorker @Inject constructor(
        appContext: Context,
        params: WorkerParameters
) : Worker(appContext, params) {

    override fun doWork(): Result {
        //Do work

        return Result.success()
    }
}

For the above worker, the class name would be me.rojan.worker.ExampleWorker, your code in WorkerFactory would then try to find this class and then create the worker.

Let's assume the device is not currently idle and we enqueue our worker with the constraint. setRequiresDeviceIdle(true). So in this case it's only going to be created and executed when the device has been deemed idle by the system.

What if we then install an update over our app that has renamed/moved/delete our worker class in the code? Let's rename our worker to me.rojan.worker.ExampleWorker2 and update the app.

Can you guess what might happen when your device is deemed idle by the Android system? It's going to invoke createWorker on your WorkerFactory with the workerClassName parameter as me.rojan.worker.ExampleWorker. This is the same value that was provided when enqueuing the worker prior to updating the app. But wait! We renamed this class, the WorkerFactory is not going to be able to find your class and create your worker in the updated version of the app. It's going to crash!

Caused by java.lang.ClassNotFoundException: me.rojan.worker.ExampleWorker
       at java.lang.Class.classForName(Class.java)
       at java.lang.Class.forName + 453(Class.java:453)
       at java.lang.Class.forName + 378(Class.java:378)
       at me.rojan.worker.WorkerFactory.createWorker + 20(WorkManagerFactory.java:20)
       at androidx.work.WorkerFactory.createWorkerWithDefaultFallback + 81(WorkerFactory.java:81)
       at androidx.work.impl.WorkerWrapper.runWorker + 228(WorkerWrapper.java:228)
       at androidx.work.impl.WorkerWrapper.run + 127(WorkerWrapper.java:127)
       at androidx.work.impl.utils.SerialExecutor$Task.run + 91(SerialExecutor.java:91)
       at java.util.concurrent.ThreadPoolExecutor.processTask + 1187(ThreadPoolExecutor.java:1187)
       at java.util.concurrent.ThreadPoolExecutor.runWorker + 1152(ThreadPoolExecutor.java:1152)
       at java.util.concurrent.ThreadPoolExecutor$Worker.run + 641(ThreadPoolExecutor.java:641)
       at java.lang.Thread.run + 784(Thread.java:784)

What are our options?

  • Map workerClassName of an old class name to their new names. If that fails to find the class, let's return null which will dequeue the worker but this will probably result in your worker and any data associated with it lost.
object WorkerClassNameFixer {

    fun fixIfRequired(className: String): String {
        return when (className) {
            exampleWorkerClassName.first -> exampleWorkerClassName.second
            else -> className
        }
    }

    private val exampleWorkerClassName = Pair(
            "me.rojan.worker.ExampleWorker",
            "me.rojan.worker.ExampleWorker2"
    )
}
class WorkerFactory @Inject constructor(
        private val workerFactories: Map<Class<out ListenableWorker>, @JvmSuppressWildcards Provider<ChildWorkerFactory>>
) : WorkerFactory() {

    override fun createWorker(
            appContext: Context,
            workerClassName: String,
            workerParameters: WorkerParameters
    ): ListenableWorker? {
        val clazz = try {
            val fixedWorkerClassName = WorkerClassNameFixer.fixIfRequired(workerClassName)
            Class.forName(fixedWorkerClassName)
        } catch (e: ClassNotFoundException) {
            return null
        }

        val foundEntry = workerFactories.entries.find { clazz.isAssignableFrom(it.key) }

        val factoryProvider = foundEntry?.value
                ?: throw IllegalArgumentException("unknown worker class name: $workerClassName")

        return factoryProvider.get().create(appContext, workerParameters)
    }
}
  • Alternatively, we can catch the ClassNotFoundException and return null. This is what the default WorkerFactory does. Like the previous option, it would result in your worker being removed from the queue and data associated with it probably lost.
class WorkerFactory @Inject constructor(
        private val workerFactories: Map<Class<out ListenableWorker>, @JvmSuppressWildcards Provider<ChildWorkerFactory>>
) : WorkerFactory() {

    override fun createWorker(
            appContext: Context,
            workerClassName: String,
            workerParameters: WorkerParameters
    ): ListenableWorker? {
        val clazz = try {
            Class.forName(workerClassName)
        } catch (e: ClassNotFoundException) {
            return null
        }

        val foundEntry = workerFactories.entries.find { clazz.isAssignableFrom(it.key) }

        val factoryProvider = foundEntry?.value
                ?: throw IllegalArgumentException("unknown worker class name: $workerClassName")

        return factoryProvider.get().create(appContext, workerParameters)
    }
}

Feel free to share your own approaches or opinions! :)

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.