main

square/leakcanary

Last updated at: 29/12/2023 09:38

RemoteLeakCanaryWorkerService.kt

TLDR

This file contains the implementation of the RemoteLeakCanaryWorkerService class, which is a subclass of RemoteWorkerService from the Android WorkManager library. It also includes a nested class FakeAppContextConfigurationProvider that provides a custom configuration for the WorkManager instance.

Classes

RemoteLeakCanaryWorkerService

This class extends RemoteWorkerService and is used by the LeakCanary library to run background tasks related to memory leak detection in a separate process. It includes the following methods:

  • getApplicationContext(): Overrides the getApplicationContext() method from RemoteWorkerService to return a fake app context that provides a custom configuration for WorkManager.
  • onCreate(): Overrides the onCreate() method from RemoteWorkerService to install AppWatcher and manually trigger its installation in the application.

FakeAppContextConfigurationProvider

This nested class extends ContextWrapper and implements Configuration.Provider. It provides a fake app context and a custom configuration for WorkManager. It includes the following methods:

  • getApplicationContext(): Overrides the getApplicationContext() method from ContextWrapper to return itself as the application context.
  • getWorkManagerConfiguration(): Overrides the getWorkManagerConfiguration() method from Configuration.Provider to return a Configuration instance with a default process name.
package leakcanary.internal

import android.app.Application
import android.content.Context
import android.content.ContextWrapper
import androidx.work.Configuration
import androidx.work.multiprocess.RemoteWorkerService
import leakcanary.AppWatcher

/**
 * Using a custom class name instead of RemoteWorkerService so that
 * hosting apps can use RemoteWorkerService without a naming conflict.
 */
class RemoteLeakCanaryWorkerService : RemoteWorkerService() {

  /**
   * RemoteLeakCanaryWorkerService is running in the :leakcanary process, and androidx startup only
   * initializes WorkManager in the main process. In RemoteWorkerService.onCreate(),
   * WorkManager has not been init, so it would crash. We can't blindly call init() as developers
   * might be calling WorkManager.initialize() from Application.onCreate() for all processes, in
   * which case a 2nd init would fail. So we want to init if nothing has init WorkManager before.
   * But there's no isInit() API. However, WorkManager will automatically pull in the application
   * context and if that context implements Configuration.Provider then it'll pull the
   * configuration from it. So we cheat WorkManager by returning a fake app context that provides
   * our own custom configuration.
   */
  class FakeAppContextConfigurationProvider(base: Context) : ContextWrapper(base),
    Configuration.Provider {

    // No real app context for you, sorry!
    override fun getApplicationContext() = this

    override fun getWorkManagerConfiguration() = Configuration.Builder()
      // If the default package name is not set, WorkManager will cancel all runnables
      // when initialized as it can't tell that it's not running in the main process.
      // This would lead to an extra round trip where the canceling reaches the main process
      // which then cancels the remote job and reschedules it and then only the work gets done.
      .setDefaultProcessName(packageName)
      .build()
  }

  private val fakeAppContext by lazy {
    // We set the base context to the real app context so that getting resources etc still works.
    FakeAppContextConfigurationProvider(super.getApplicationContext())
  }

  override fun getApplicationContext(): Context {
    return fakeAppContext
  }

  override fun onCreate() {
    // Ideally we wouldn't need to install AppWatcher at all here, however
    // the installation triggers InternalsLeakCanary to store the application instance
    // which is then used by the event listeners that respond to analysis progress.
    if (!AppWatcher.isInstalled) {
      val application = super.getApplicationContext() as Application
      AppWatcher.manualInstall(
        application,
        // Nothing to watch in the :leakcanary process.
        watchersToInstall = emptyList()
      )
    }
    super.onCreate()
  }
}