### Description

```
android.content.res.Resources$NotFoundException: Resource ID #0x7f050008 type #0x5 is not valid
        at android.content.res.Resources.getBoolean(Resources.java:1088)
        at leakcanary.RootViewWatcher$listener$1.onRootViewAdded(RootViewWatcher.kt:49)
        at curtains.OnRootViewAddedListener$DefaultImpls.onRootViewsChanged(Listeners.kt:37)
        at leakcanary.RootViewWatcher$listener$1.onRootViewsChanged(RootViewWatcher.kt:43)
        at curtains.internal.RootViewsSpy$delegatingViewList$1.add(RootViewsSpy.kt:25)
        at curtains.internal.RootViewsSpy$delegatingViewList$1.add(RootViewsSpy.kt:23)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:350)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
        at android.app.Dialog.show(Dialog.java:329)
        at org.chromium.content.browser.input.SelectPopupDialog.show(SelectPopupDialog.java:146)
        at org.chromium.content.browser.input.SelectPopup.show(SelectPopup.java:162)
        at android.os.MessageQueue.nativePollOnce(Native Method)
        at android.os.MessageQueue.next(MessageQueue.java:326)
        at android.os.Looper.loop(Looper.java:165)
        at android.app.ActivityThread.main(ActivityThread.java:6821)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)
```
### Steps to Reproduce

just intergration to Project

**Expected behavior:**  be normal

### Version Information

* LeakCanary version: 2.7
* Android OS version: android 9.0
* Gradle version:6.1.1

### Additional Information

diff --git a/leakcanary-object-watcher-android/src/main/java/leakcanary/RootViewWatcher.kt b/leakcanary-object-watcher-android/src/main/java/leakcanary/RootViewWatcher.kt
index 4fbf1d535..c417e5a03 100644
--- a/leakcanary-object-watcher-android/src/main/java/leakcanary/RootViewWatcher.kt
+++ b/leakcanary-object-watcher-android/src/main/java/leakcanary/RootViewWatcher.kt
@@ -46,7 +46,12 @@ class RootViewWatcher(
         when (rootView.phoneWindow?.callback?.wrappedCallback) {
           // Activities are already tracked by ActivityWatcher
           is Activity -> false
-          is Dialog -> rootView.resources.getBoolean(R.bool.leak_canary_watcher_watch_dismissed_dialogs)
+          is Dialog -> {
+            // Use app context resources to avoid NotFoundException
+            // https://github.com/square/leakcanary/issues/2137
+            val resources = rootView.context.applicationContext.resources
+            resources.getBoolean(R.bool.leak_canary_watcher_watch_dismissed_dialogs)
+          }
           // Probably a DreamService
           else -> true
         }

RootViewWatcher onRootViewAdd crashed
### Description

```
android.content.res.Resources$NotFoundException: Resource ID #0x7f050008 type #0x5 is not valid
        at android.content.res.Resources.getBoolean(Resources.java:1088)
        at leakcanary.RootViewWatcher$listener$1.onRootViewAdded(RootViewWatcher.kt:49)
        at curtains.OnRootViewAddedListener$DefaultImpls.onRootViewsChanged(Listeners.kt:37)
        at leakcanary.RootViewWatcher$listener$1.onRootViewsChanged(RootViewWatcher.kt:43)
        at curtains.internal.RootViewsSpy$delegatingViewList$1.add(RootViewsSpy.kt:25)
        at curtains.internal.RootViewsSpy$delegatingViewList$1.add(RootViewsSpy.kt:23)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:350)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
        at android.app.Dialog.show(Dialog.java:329)
        at org.chromium.content.browser.input.SelectPopupDialog.show(SelectPopupDialog.java:146)
        at org.chromium.content.browser.input.SelectPopup.show(SelectPopup.java:162)
        at android.os.MessageQueue.nativePollOnce(Native Method)
        at android.os.MessageQueue.next(MessageQueue.java:326)
        at android.os.Looper.loop(Looper.java:165)
        at android.app.ActivityThread.main(ActivityThread.java:6821)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)
```
### Steps to Reproduce

just intergration to Project

**Expected behavior:**  be normal

### Version Information

* LeakCanary version: 2.7
* Android OS version: android 9.0
* Gradle version:6.1.1

### Additional Information

package leakcanary

import android.app.Application
import android.os.Handler
import android.os.Looper
import com.squareup.leakcanary.core.R
import leakcanary.internal.InternalLeakCanary
import leakcanary.internal.InternalLeakCanary.FormFactor.TV
import leakcanary.internal.NotificationType.LEAKCANARY_MAX
import leakcanary.internal.Notifications
import leakcanary.internal.activity.LeakActivity
import leakcanary.internal.activity.db.HeapAnalysisTable
import leakcanary.internal.activity.db.LeaksDbHelper
import leakcanary.internal.activity.screen.HeapAnalysisFailureScreen
import leakcanary.internal.activity.screen.HeapDumpScreen
import leakcanary.internal.activity.screen.HeapDumpsScreen
import leakcanary.internal.activity.screen.LeakTraceWrapper
import leakcanary.internal.navigation.Screen
import leakcanary.internal.tv.TvToast
import leakcanary.internal.friendly.mainHandler
import shark.HeapAnalysis
import shark.HeapAnalysisFailure
import shark.HeapAnalysisSuccess
import shark.SharkLog

/**
 * Default [OnHeapAnalyzedListener] implementation, which will store the analysis to disk and
 * show a notification summarizing the result.
 */
class DefaultOnHeapAnalyzedListener private constructor(private val applicationProvider: () -> Application) :
  OnHeapAnalyzedListener {

  // Kept this constructor for backward compatibility of public API.
  @Deprecated(
    message = "Use DefaultOnHeapAnalyzedListener.create() instead",
    replaceWith = ReplaceWith("DefaultOnHeapAnalyzedListener.create()")
  )
  constructor(application: Application) : this({ application })

  private val application: Application by lazy { applicationProvider() }

  override fun onHeapAnalyzed(heapAnalysis: HeapAnalysis) {
    SharkLog.d { "\u200B\n${LeakTraceWrapper.wrap(heapAnalysis.toString(), 120)}" }

    val db = LeaksDbHelper(application).writableDatabase
    val id = HeapAnalysisTable.insert(db, heapAnalysis)
    db.releaseReference()

    val (contentTitle, screenToShow) = when (heapAnalysis) {
      is HeapAnalysisFailure -> application.getString(
        R.string.leak_canary_analysis_failed
      ) to HeapAnalysisFailureScreen(id)
      is HeapAnalysisSuccess -> {
        val retainedObjectCount = heapAnalysis.allLeaks.sumBy { it.leakTraces.size }
        val leakTypeCount = heapAnalysis.applicationLeaks.size + heapAnalysis.libraryLeaks.size
        application.getString(
          R.string.leak_canary_analysis_success_notification, retainedObjectCount, leakTypeCount
        ) to HeapDumpScreen(id)
      }
    }

    if (InternalLeakCanary.formFactor == TV) {
      showToast(heapAnalysis)
      printIntentInfo()
    } else {
      showNotification(screenToShow, contentTitle)
    }
  }

  private fun showNotification(
    screenToShow: Screen,
    contentTitle: String
  ) {
    val pendingIntent = LeakActivity.createPendingIntent(
      application, arrayListOf(HeapDumpsScreen(), screenToShow)
    )

    val contentText = application.getString(R.string.leak_canary_notification_message)

    Notifications.showNotification(
      application, contentTitle, contentText, pendingIntent,
      R.id.leak_canary_notification_analysis_result,
      LEAKCANARY_MAX
    )
  }

  /**
   * Android TV devices do not have notifications, therefore the only easy and non-invasive way
   * to communicate with user is via Toast messages. These are used just to grab user attention and
   * to direct them to Logcat where a much more detailed report will be printed.
   */
  private fun showToast(heapAnalysis: HeapAnalysis) {
    mainHandler.post {
      val resumedActivity = InternalLeakCanary.resumedActivity ?: return@post
      val message: String = when (heapAnalysis) {
        is HeapAnalysisSuccess -> {
          application.getString(
            R.string.leak_canary_tv_analysis_success,
            heapAnalysis.applicationLeaks.size,
            heapAnalysis.libraryLeaks.size
          )
        }
        is HeapAnalysisFailure -> application.getString(R.string.leak_canary_tv_analysis_failure)
      }
      TvToast.makeText(resumedActivity, message)
        .show()
    }
  }

  /**
   * Android TV with API 26+ has a bug where the launcher icon doesn't appear, so users won't know how
   * to launch LeakCanary Activity.
   * This method prints an adb command that launched LeakCanary into the logcat
   */
  private fun printIntentInfo() {
    val leakClass = LeakActivity::class.java
    SharkLog.d {
      """====================================
  ANDROID TV LAUNCH INTENT
  ====================================
  Run the following adb command to display the list of leaks:

  adb shell am start -n "${application.packageName}/${leakClass.`package`?.name}.LeakLauncherActivity"
  ===================================="""
    }
  }

  companion object {
    fun create(): OnHeapAnalyzedListener =
      DefaultOnHeapAnalyzedListener { InternalLeakCanary.application }
  }
}

package leakcanary

import android.content.Intent
import leakcanary.LeakCanary.config
import leakcanary.internal.HeapDumpControl
import leakcanary.internal.InternalLeakCanary
import leakcanary.internal.activity.LeakActivity
import shark.AndroidMetadataExtractor
import shark.AndroidObjectInspectors
import shark.AndroidReferenceMatchers
import shark.FilteringLeakingObjectFinder
import shark.HeapAnalysisSuccess
import shark.IgnoredReferenceMatcher
import shark.KeyedWeakReferenceFinder
import shark.LeakingObjectFinder
import shark.LibraryLeakReferenceMatcher
import shark.MetadataExtractor
import shark.ObjectInspector
import shark.ReferenceMatcher
import shark.SharkLog

/**
 * The entry point API for LeakCanary. LeakCanary builds on top of [AppWatcher]. AppWatcher
 * notifies LeakCanary of retained instances, which in turns dumps the heap, analyses it and
 * publishes the results.
 *
 * LeakCanary can be configured by updating [config].
 */
object LeakCanary {

  /**
   * LeakCanary configuration data class. Properties can be updated via [copy].
   *
   * @see [config]
   */
  data class Config(
    /**
     * Whether LeakCanary should dump the heap when enough retained instances are found. This needs
     * to be true for LeakCanary to work, but sometimes you may want to temporarily disable
     * LeakCanary (e.g. for a product demo).
     *
     * Defaults to true.
     */
    val dumpHeap: Boolean = true,
    /**
     * If [dumpHeapWhenDebugging] is false then LeakCanary will not dump the heap
     * when the debugger is attached. The debugger can create temporary memory leaks (for instance
     * if a thread is blocked on a breakpoint).
     *
     * Defaults to false.
     */
    val dumpHeapWhenDebugging: Boolean = false,
    /**
     * When the app is visible, LeakCanary will wait for at least
     * [retainedVisibleThreshold] retained instances before dumping the heap. Dumping the heap
     * freezes the UI and can be frustrating for developers who are trying to work. This is
     * especially frustrating as the Android Framework has a number of leaks that cannot easily
     * be fixed.
     *
     * When the app becomes invisible, LeakCanary dumps the heap after
     * [AppWatcher.Config.watchDurationMillis] ms.
     *
     * The app is considered visible if it has at least one activity in started state.
     *
     * A higher threshold means LeakCanary will dump the heap less often, therefore it won't be
     * bothering developers as much but it could miss some leaks.
     *
     * Defaults to 5.
     */
    val retainedVisibleThreshold: Int = 5,

    /**
     * Known patterns of references in the heap, added here either to ignore them
     * ([IgnoredReferenceMatcher]) or to mark them as library leaks ([LibraryLeakReferenceMatcher]).
     *
     * When adding your own custom [LibraryLeakReferenceMatcher] instances, you'll most
     * likely want to set [LibraryLeakReferenceMatcher.patternApplies] with a filter that checks
     * for the Android OS version and manufacturer. The build information can be obtained by calling
     * [shark.AndroidBuildMirror.fromHeapGraph].
     *
     * Defaults to [AndroidReferenceMatchers.appDefaults]
     */
    val referenceMatchers: List<ReferenceMatcher> = AndroidReferenceMatchers.appDefaults,

    /**
     * List of [ObjectInspector] that provide LeakCanary with insights about objects found in the
     * heap. You can create your own [ObjectInspector] implementations, and also add
     * a [shark.AppSingletonInspector] instance created with the list of internal singletons.
     *
     * Defaults to [AndroidObjectInspectors.appDefaults]
     */
    val objectInspectors: List<ObjectInspector> = AndroidObjectInspectors.appDefaults,

    /**
     * Called on a background thread when the heap analysis is complete.
     * If you want leaks to be added to the activity that lists leaks, make sure to delegate
     * calls to a [DefaultOnHeapAnalyzedListener].
     *
     * Defaults to [DefaultOnHeapAnalyzedListener]
     */
    val onHeapAnalyzedListener: OnHeapAnalyzedListener = DefaultOnHeapAnalyzedListener.create(),

    /**
     * Extracts metadata from a hprof to be reported in [HeapAnalysisSuccess.metadata].
     * Called on a background thread during heap analysis.
     *
     * Defaults to [AndroidMetadataExtractor]
     */
    val metadataExtractor: MetadataExtractor = AndroidMetadataExtractor,

    /**
     * Whether to compute the retained heap size, which is the total number of bytes in memory that
     * would be reclaimed if the detected leaks didn't happen. This includes native memory
     * associated to Java objects (e.g. Android bitmaps).
     *
     * Computing the retained heap size can slow down the analysis because it requires navigating
     * from GC roots through the entire object graph, whereas [shark.HeapAnalyzer] would otherwise
     * stop as soon as all leaking instances are found.
     *
     * Defaults to true.
     */
    val computeRetainedHeapSize: Boolean = true,

    /**
     * How many heap dumps are kept on the Android device for this app package. When this threshold
     * is reached LeakCanary deletes the older heap dumps. As several heap dumps may be enqueued
     * you should avoid going down to 1 or 2.
     *
     * Defaults to 7.
     */
    val maxStoredHeapDumps: Int = 7,

    /**
     * LeakCanary always attempts to store heap dumps on the external storage if the
     * WRITE_EXTERNAL_STORAGE is already granted, and otherwise uses the app storage.
     * If the WRITE_EXTERNAL_STORAGE permission is not granted and
     * [requestWriteExternalStoragePermission] is true, then LeakCanary will display a notification
     * to ask for that permission.
     *
     * Defaults to false because that permission notification can be annoying.
     */
    val requestWriteExternalStoragePermission: Boolean = false,

    /**
     * Finds the objects that are leaking, for which LeakCanary will compute leak traces.
     *
     * Defaults to [KeyedWeakReferenceFinder] which finds all objects tracked by a
     * [KeyedWeakReference], ie all objects that were passed to
     * [ObjectWatcher.expectWeaklyReachable].
     *
     * You could instead replace it with a [FilteringLeakingObjectFinder], which scans all objects
     * in the heap dump and delegates the decision to a list of
     * [FilteringLeakingObjectFinder.LeakingObjectFilter]. This can lead to finding more leaks
     * than the default and shorter leak traces. This also means that every analysis during a
     * given process life will bring up the same leaking objects over and over again, unlike
     * when using [KeyedWeakReferenceFinder] (because [KeyedWeakReference] instances are cleared
     * after each heap dump).
     *
     * The list of filters can be built from [AndroidObjectInspectors]:
     *
     * ```
     * LeakCanary.config = LeakCanary.config.copy(
     *     leakingObjectFinder = FilteringLeakingObjectFinder(
     *         AndroidObjectInspectors.appLeakingObjectFilters
     *     )
     * )
     * ```
     */
    val leakingObjectFinder: LeakingObjectFinder = KeyedWeakReferenceFinder,

    /**
     * Deprecated: This is a no-op, set a custom [leakingObjectFinder] instead.
     */
    @Deprecated("This is a no-op, set a custom leakingObjectFinder instead")
    val useExperimentalLeakFinders: Boolean = false
  ) {

    /**
     * Construct a new Config via [LeakCanary.Config.Builder].
     * Note: this method is intended to be used from Java code only. For idiomatic Kotlin use
     * `copy()` to modify [LeakCanary.config].
     */
    @Suppress("NEWER_VERSION_IN_SINCE_KOTLIN")
    @SinceKotlin("999.9") // Hide from Kotlin code, this method is only for Java code
    fun newBuilder() = Builder(this)

    /**
     * Builder for [LeakCanary.Config] intended to be used only from Java code.
     *
     * Usage:
     * ```
     * LeakCanary.Config config = LeakCanary.getConfig().newBuilder()
     *    .retainedVisibleThreshold(3)
     *    .build();
     * LeakCanary.setConfig(config);
     * ```
     *
     * For idiomatic Kotlin use `copy()` method instead:
     * ```
     * LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 3)
     * ```
     */
    class Builder internal constructor(config: Config) {
      private var dumpHeap = config.dumpHeap
      private var dumpHeapWhenDebugging = config.dumpHeapWhenDebugging
      private var retainedVisibleThreshold = config.retainedVisibleThreshold
      private var referenceMatchers = config.referenceMatchers
      private var objectInspectors = config.objectInspectors
      private var onHeapAnalyzedListener = config.onHeapAnalyzedListener
      private var metadataExtractor = config.metadataExtractor
      private var computeRetainedHeapSize = config.computeRetainedHeapSize
      private var maxStoredHeapDumps = config.maxStoredHeapDumps
      private var requestWriteExternalStoragePermission =
        config.requestWriteExternalStoragePermission
      private var leakingObjectFinder = config.leakingObjectFinder

      /** @see [LeakCanary.Config.dumpHeap] */
      fun dumpHeap(dumpHeap: Boolean) =
        apply { this.dumpHeap = dumpHeap }

      /** @see [LeakCanary.Config.dumpHeapWhenDebugging] */
      fun dumpHeapWhenDebugging(dumpHeapWhenDebugging: Boolean) =
        apply { this.dumpHeapWhenDebugging = dumpHeapWhenDebugging }

      /** @see [LeakCanary.Config.retainedVisibleThreshold] */
      fun retainedVisibleThreshold(retainedVisibleThreshold: Int) =
        apply { this.retainedVisibleThreshold = retainedVisibleThreshold }

      /** @see [LeakCanary.Config.referenceMatchers] */
      fun referenceMatchers(referenceMatchers: List<ReferenceMatcher>) =
        apply { this.referenceMatchers = referenceMatchers }

      /** @see [LeakCanary.Config.objectInspectors] */
      fun objectInspectors(objectInspectors: List<ObjectInspector>) =
        apply { this.objectInspectors = objectInspectors }

      /** @see [LeakCanary.Config.onHeapAnalyzedListener] */
      fun onHeapAnalyzedListener(onHeapAnalyzedListener: OnHeapAnalyzedListener) =
        apply { this.onHeapAnalyzedListener = onHeapAnalyzedListener }

      /** @see [LeakCanary.Config.metadataExtractor] */
      fun metadataExtractor(metadataExtractor: MetadataExtractor) =
        apply { this.metadataExtractor = metadataExtractor }

      /** @see [LeakCanary.Config.computeRetainedHeapSize] */
      fun computeRetainedHeapSize(computeRetainedHeapSize: Boolean) =
        apply { this.computeRetainedHeapSize = computeRetainedHeapSize }

      /** @see [LeakCanary.Config.maxStoredHeapDumps] */
      fun maxStoredHeapDumps(maxStoredHeapDumps: Int) =
        apply { this.maxStoredHeapDumps = maxStoredHeapDumps }

      /** @see [LeakCanary.Config.requestWriteExternalStoragePermission] */
      fun requestWriteExternalStoragePermission(requestWriteExternalStoragePermission: Boolean) =
        apply { this.requestWriteExternalStoragePermission = requestWriteExternalStoragePermission }

      /** @see [LeakCanary.Config.leakingObjectFinder] */
      fun leakingObjectFinder(leakingObjectFinder: LeakingObjectFinder) =
        apply { this.leakingObjectFinder = leakingObjectFinder }

      fun build() = config.copy(
        dumpHeap = dumpHeap,
        dumpHeapWhenDebugging = dumpHeapWhenDebugging,
        retainedVisibleThreshold = retainedVisibleThreshold,
        referenceMatchers = referenceMatchers,
        objectInspectors = objectInspectors,
        onHeapAnalyzedListener = onHeapAnalyzedListener,
        metadataExtractor = metadataExtractor,
        computeRetainedHeapSize = computeRetainedHeapSize,
        maxStoredHeapDumps = maxStoredHeapDumps,
        requestWriteExternalStoragePermission = requestWriteExternalStoragePermission,
        leakingObjectFinder = leakingObjectFinder
      )
    }
  }

  /**
   * The current LeakCanary configuration. Can be updated at any time, usually by replacing it with
   * a mutated copy, e.g.:
   *
   * ```
   * LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 3)
   * ```
   *
   * In Java, use [LeakCanary.Config.Builder] instead:
   * ```
   * LeakCanary.Config config = LeakCanary.getConfig().newBuilder()
   *    .retainedVisibleThreshold(3)
   *    .build();
   * LeakCanary.setConfig(config);
   * ```
   */
  @JvmStatic @Volatile
  var config: Config = Config()
    set(newConfig) {
      val previousConfig = field
      field = newConfig
      logConfigChange(previousConfig, newConfig)
      HeapDumpControl.updateICanHasHeap()
    }

  private fun logConfigChange(
    previousConfig: Config,
    newConfig: Config
  ) {
    SharkLog.d {
      val changedFields = mutableListOf<String>()
      Config::class.java.declaredFields.forEach { field ->
        field.isAccessible = true
        val previousValue = field[previousConfig]
        val newValue = field[newConfig]
        if (previousValue != newValue) {
          changedFields += "${field.name}=$newValue"
        }
      }
      val changesInConfig =
        if (changedFields.isNotEmpty()) changedFields.joinToString(", ") else "no changes"

      "Updated LeakCanary.config: Config($changesInConfig)"
    }
  }

  /**
   * Returns a new [Intent] that can be used to programmatically launch the leak display activity.
   */
  fun newLeakDisplayActivityIntent() = LeakActivity.createIntent(InternalLeakCanary.application)

  /**
   * Dynamically shows / hides the launcher icon for the leak display activity.
   * Note: you can change the default value by overriding the `leak_canary_add_launcher_icon`
   * boolean resource:
   *
   * ```
   * <?xml version="1.0" encoding="utf-8"?>
   * <resources>
   *   <bool name="leak_canary_add_launcher_icon">false</bool>
   * </resources>
   * ```
   */
  fun showLeakDisplayActivityLauncherIcon(showLauncherIcon: Boolean) {
    InternalLeakCanary.setEnabledBlocking(
      "leakcanary.internal.activity.LeakLauncherActivity", showLauncherIcon
    )
  }

  /**
   * Immediately triggers a heap dump and analysis, if there is at least one retained instance
   * tracked by [AppWatcher.objectWatcher]. If there are no retained instances then the heap will not
   * be dumped and a notification will be shown instead.
   */
  fun dumpHeap() = InternalLeakCanary.onDumpHeapReceived(forceDump = true)
}

package leakcanary

import shark.HeapAnalysis

/**
 * Listener set in [LeakCanary.Config] and called by LeakCanary on a background thread when the
 * heap analysis is complete.
 *
 * This is a functional interface with which you can create a [OnHeapAnalyzedListener] from a lambda.
 *
 * Usage:
 *
 * ```kotlin
 * val listener = OnHeapAnalyzedListener { heapAnalysis ->
 *   process(heapAnalysis)
 * }
 * ```
 */
fun interface OnHeapAnalyzedListener {

  /**
   * @see OnHeapAnalyzedListener
   */
  fun onHeapAnalyzed(heapAnalysis: HeapAnalysis)

  companion object {
    /**
     * Utility function to create a [OnHeapAnalyzedListener] from the passed in [block] lambda
     * instead of using the anonymous `object : OnHeapAnalyzedListener` syntax.
     *
     * Usage:
     *
     * ```kotlin
     * val listener = OnHeapAnalyzedListener {
     *
     * }
     * ```
     */
    inline operator fun invoke(crossinline block: (HeapAnalysis) -> Unit): OnHeapAnalyzedListener =
      object : OnHeapAnalyzedListener {
        override fun onHeapAnalyzed(heapAnalysis: HeapAnalysis) {
          block(heapAnalysis)
        }
      }
  }
}

package leakcanary

import org.junit.runner.Description
import org.junit.runner.notification.RunListener

/**
 * A JUnit [RunListener] extending [FailTestOnLeakRunListener] to detecting memory
 * leaks in Android instrumentation tests only when the [FailTestOnLeak] annotation
 * is used.
 *
 * @see FailTestOnLeak
 */
class FailAnnotatedTestOnLeakRunListener : FailTestOnLeakRunListener() {

  override fun skipLeakDetectionReason(description: Description) =
    if (description.getAnnotation(FailTestOnLeak::class.java) != null) {
      null
    } else {
      "test is not annotated with @FailTestOnLeak"
    }
}

package leakcanary

/**
 * An [Annotation] class to be used in conjunction with [FailAnnotatedTestOnLeakRunListener]
 * for detecting memory leaks. When using [FailAnnotatedTestOnLeakRunListener], the tests
 * should be annotated with this class in order for the listener to detect memory leaks.
 *
 * @see FailAnnotatedTestOnLeakRunListener
 */
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class FailTestOnLeak

package leakcanary

import android.app.Activity
import android.app.Application
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import leakcanary.InstrumentationLeakDetector.Result.AnalysisPerformed
import leakcanary.internal.TestResultPublisher
import org.junit.runner.Description
import org.junit.runner.Result
import org.junit.runner.notification.Failure
import org.junit.runner.notification.RunListener
import shark.HeapAnalysis
import shark.HeapAnalysisFailure
import shark.HeapAnalysisSuccess
import shark.SharkLog
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit.SECONDS
import leakcanary.internal.friendly.noOpDelegate

/**
 *
 * A JUnit [RunListener] that uses [InstrumentationLeakDetector] to detect memory leaks in Android
 * instrumentation tests. It waits for the end of a test, and if the test succeeds then it will
 * look for retained objects, trigger a heap dump if needed and perform an analysis.
 *
 *  [FailTestOnLeakRunListener] can be subclassed to override [skipLeakDetectionReason] and
 *  [onAnalysisPerformed]
 *
 * @see InstrumentationLeakDetector
 */
open class FailTestOnLeakRunListener : RunListener() {
  private var _currentTestDescription: Description? = null
  private val currentTestDescription: Description
    get() = _currentTestDescription!!

  private var skipLeakDetectionReason: String? = null

  private lateinit var testResultPublisher: TestResultPublisher

  @Volatile
  private var allActivitiesDestroyedLatch: CountDownLatch? = null

  override fun testRunStarted(description: Description) {
    testResultPublisher = TestResultPublisher.install()
    trackActivities()
  }

  private fun trackActivities() {
    val instrumentation = getInstrumentation()!!
    val application = instrumentation.targetContext.applicationContext as Application
    application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks by noOpDelegate() {

      var activitiesWaitingForDestroyed = 0

      override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
      ) {
        if (activitiesWaitingForDestroyed == 0) {
          allActivitiesDestroyedLatch = CountDownLatch(1)
        }
        activitiesWaitingForDestroyed++
      }

      override fun onActivityDestroyed(activity: Activity) {
        activitiesWaitingForDestroyed--
        if (activitiesWaitingForDestroyed == 0) {
          allActivitiesDestroyedLatch!!.countDown()
        }
      }
    })
  }

  override fun testRunFinished(result: Result) {
  }

  override fun testStarted(description: Description) {
    _currentTestDescription = description
    skipLeakDetectionReason = skipLeakDetectionReason(description)
    if (skipLeakDetectionReason != null) {
      return
    }
  }

  /**
   * Can be overridden to skip leak detection based on the description provided when a test
   * is started. Return null to continue leak detection, or a string describing the reason for
   * skipping otherwise.
   */
  protected open fun skipLeakDetectionReason(description: Description): String? {
    return null
  }

  override fun testFailure(failure: Failure) {
    skipLeakDetectionReason = "failed"
  }

  override fun testIgnored(description: Description) {
    skipLeakDetectionReason = "was ignored"
  }

  override fun testAssumptionFailure(failure: Failure) {
    skipLeakDetectionReason = "had an assumption failure"
  }

  override fun testFinished(description: Description) {
    if (skipLeakDetectionReason == null) {
      detectLeaks()
    } else {
      SharkLog.d { "Skipping leak detection because the test $skipLeakDetectionReason" }
      skipLeakDetectionReason = null
    }
    AppWatcher.objectWatcher.clearWatchedObjects()
    _currentTestDescription = null
    testResultPublisher.publishTestFinished()
  }

  private fun detectLeaks() {
    val allActivitiesDestroyed = allActivitiesDestroyedLatch?.await(2, SECONDS) ?: true
    if (!allActivitiesDestroyed) {
      SharkLog.d { "Leak detection proceeding with some activities still not in destroyed state" }
    }

    val leakDetector = InstrumentationLeakDetector()
    val result = leakDetector.detectLeaks()

    if (result is AnalysisPerformed) {
      onAnalysisPerformed(heapAnalysis = result.heapAnalysis)
    } else {
      SharkLog.d { "No heap analysis performed" }
    }
  }

  /**
   * Called when a heap analysis has been performed and a result is available.
   *
   * The default implementation call [failTest] if the [heapAnalysis] failed or if
   * [HeapAnalysisSuccess.applicationLeaks] is not empty.
   */
  protected open fun onAnalysisPerformed(heapAnalysis: HeapAnalysis) {
    when (heapAnalysis) {
      is HeapAnalysisFailure -> {
        failTest(
          "$currentTestDescription failed because heap analysis failed:\n" + Log.getStackTraceString(
            heapAnalysis.exception
          )
        )
      }
      is HeapAnalysisSuccess -> {
        val applicationLeaks = heapAnalysis.applicationLeaks
        if (applicationLeaks.isNotEmpty()) {
          failTest(
            "$currentTestDescription failed because application memory leaks were detected:\n$heapAnalysis"
          )
        } else {
          SharkLog.d { "Heap analysis found 0 application leaks:\n$heapAnalysis" }
        }
      }
    }
  }

  /**
   * Reports that the test has failed, with the provided [trace].
   */
  protected fun failTest(trace: String) {
    SharkLog.d { trace }
    testResultPublisher.publishTestFailure(currentTestDescription, trace)
  }
}

/*
 * Copyright (C) 2018 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary

import android.os.Debug
import android.os.SystemClock
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import leakcanary.GcTrigger.Default.runGc
import leakcanary.InstrumentationLeakDetector.Result.AnalysisPerformed
import leakcanary.InstrumentationLeakDetector.Result.NoAnalysis
import org.junit.runner.notification.RunListener
import shark.HeapAnalysis
import shark.HeapAnalysisException
import shark.HeapAnalysisFailure
import shark.HeapAnalysisSuccess
import shark.HeapAnalyzer
import shark.SharkLog
import java.io.File

/**
 * [InstrumentationLeakDetector] can be used to detect memory leaks in instrumentation tests.
 *
 * To use it, you need to add an instrumentation test listener (e.g. [FailTestOnLeakRunListener])
 * that will invoke [detectLeaks].
 *
 * ### Add an instrumentation test listener
 *
 * LeakCanary provides [FailTestOnLeakRunListener], but you can also implement
 * your own [RunListener] and call [detectLeaks] directly if you need a more custom
 * behavior (for instance running it only once per test suite).
 *
 * All you need to do is add the following to the defaultConfig of your build.gradle:
 *
 * `testInstrumentationRunnerArgument "listener", "leakcanary.FailTestOnLeakRunListener"`
 *
 * Then you can run your instrumentation tests via Gradle as usually, and they will fail when
 * a memory leak is detected:
 *
 * `./gradlew leakcanary-sample:connectedCheck`
 *
 * If instead you want to run UI tests via adb, add a *listener* execution argument to
 * your command line for running the UI tests:
 * `-e listener leakcanary.FailTestOnLeakRunListener`. The full command line
 * should look something like this:
 * ```
 * adb shell am instrument \\
 * -w com.android.foo/android.support.test.runner.AndroidJUnitRunner \\
 * -e listener leakcanary.FailTestOnLeakRunListener
 * ```
 *
 * ### Rationale
 * Instead of using the [InstrumentationLeakDetector], one could simply enable LeakCanary in
 * instrumentation tests.
 *
 * This approach would have two disadvantages:
 *
 *  - Heap dumps freeze the VM, and the leak analysis is IO and CPU heavy. This can slow down
 * the test and introduce flakiness
 *  - The leak analysis is asynchronous by default. This means the tests could finish and the
 *  process dies before the analysis is finished.
 *
 * The approach taken here is to collect all objects to watch as you run the test, but not
 * do any heap dump during the test. Then, at the end, if any of the watched objects is still in
 * memory we dump the heap and perform a blocking analysis. There is only one heap dump performed,
 * no matter the number of objects retained.
 */
class InstrumentationLeakDetector {

  /**
   * The result of calling [detectLeaks], which is either [NoAnalysis] or [AnalysisPerformed].
   */
  sealed class Result {
    class NoAnalysis(val reason: String) : Result()
    class AnalysisPerformed(val heapAnalysis: HeapAnalysis) : Result()
  }

  /**
   * Looks for retained objects, triggers a heap dump if needed and performs an analysis.
   */
  @Suppress("ReturnCount")
  fun detectLeaks(): Result {
    val leakDetectionTime = SystemClock.uptimeMillis()
    val watchDurationMillis = AppWatcher.config.watchDurationMillis
    val instrumentation = getInstrumentation()
    val context = instrumentation.targetContext
    val refWatcher = AppWatcher.objectWatcher

    if (!refWatcher.hasWatchedObjects) {
      return NoAnalysis("No watched objects.")
    }

    instrumentation.waitForIdleSync()
    if (!refWatcher.hasWatchedObjects) {
      return NoAnalysis("No watched objects after waiting for idle sync.")
    }

    runGc()
    if (!refWatcher.hasWatchedObjects) {
      return NoAnalysis("No watched objects after triggering an explicit GC.")
    }

    // Waiting for any delayed UI post (e.g. scroll) to clear. This shouldn't be needed, but
    // Android simply has way too many delayed posts that aren't canceled when views are detached.
    SystemClock.sleep(2000)

    if (!refWatcher.hasWatchedObjects) {
      return NoAnalysis("No watched objects after delayed UI post is cleared.")
    }

    // Aaand we wait some more.
    // 4 seconds (2+2) is greater than the 3 seconds delay for
    // FINISH_TOKEN in android.widget.Filter
    SystemClock.sleep(2000)

    val endOfWatchDelay = watchDurationMillis - (SystemClock.uptimeMillis() - leakDetectionTime)
    if (endOfWatchDelay > 0) {
      SystemClock.sleep(endOfWatchDelay)
    }

    runGc()

    if (!refWatcher.hasRetainedObjects) {
      return NoAnalysis("No watched objects after a second explicit GC.")
    }

    // We're always reusing the same file since we only execute this once at a time.
    val heapDumpFile = File(context.filesDir, "instrumentation_tests_heapdump.hprof")

    val config = LeakCanary.config

    val heapDumpUptimeMillis = SystemClock.uptimeMillis()
    KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis

    val heapDumpDurationMillis: Long

    try {
      Debug.dumpHprofData(heapDumpFile.absolutePath)
      heapDumpDurationMillis = SystemClock.uptimeMillis() - heapDumpUptimeMillis
    } catch (exception: Exception) {
      SharkLog.d(exception) { "Could not dump heap" }
      return AnalysisPerformed(
        HeapAnalysisFailure(
          heapDumpFile = heapDumpFile,
          createdAtTimeMillis = System.currentTimeMillis(),
          dumpDurationMillis = SystemClock.uptimeMillis() - heapDumpUptimeMillis,
          analysisDurationMillis = 0,
          exception = HeapAnalysisException(exception)
        )
      )
    }

    refWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)

    val listener = shark.OnAnalysisProgressListener.NO_OP

    // Giving an extra 2 seconds to flush the hprof to the file system. We've seen several cases
    // of corrupted hprof files and assume this could be a timing issue.
    SystemClock.sleep(2000)

    val heapAnalyzer = HeapAnalyzer(listener)
    val fullHeapAnalysis = when (val heapAnalysis = heapAnalyzer.analyze(
      heapDumpFile = heapDumpFile,
      leakingObjectFinder = config.leakingObjectFinder,
      referenceMatchers = config.referenceMatchers,
      computeRetainedHeapSize = config.computeRetainedHeapSize,
      objectInspectors = config.objectInspectors
    )) {
      is HeapAnalysisSuccess -> heapAnalysis.copy(dumpDurationMillis = heapDumpDurationMillis)
      is HeapAnalysisFailure -> heapAnalysis.copy(dumpDurationMillis = heapDumpDurationMillis)
    }
    return AnalysisPerformed(fullHeapAnalysis)
  }

  companion object {

    @Deprecated(
      "This is a no-op as LeakCanary automatically detects tests",
      replaceWith = ReplaceWith("")
    )
    fun updateConfig() = Unit
  }
}

package leakcanary

import android.app.ActivityManager
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import shark.SharkLog

/**
 * Used to determine whether the current process is the LeakCanary analyzer process. By depending
 * on the `leakcanary-android-process` artifact instead of the `leakcanary-android`, LeakCanary
 * will automatically run its analysis in a separate process.
 *
 * As such, you'll need to be careful to do any custom configuration of LeakCanary in both the main
 * process and the analyzer process.
 */
object LeakCanaryProcess {

  @Volatile private var isInAnalyzerProcess: Boolean? = null

  /**
   * Whether the current process is the process running the heap analyzer, which is
   * a different process than the normal app process.
   */
  fun isInAnalyzerProcess(context: Context): Boolean {
    val analyzerServiceClass: Class<out Service>
    @Suppress("UNCHECKED_CAST")
    try {
      analyzerServiceClass =
        Class.forName("leakcanary.internal.HeapAnalyzerService") as Class<out Service>
    } catch (e: Exception) {
      return false
    }

    var isInAnalyzerProcess: Boolean? = isInAnalyzerProcess
    // This only needs to be computed once per process.
    if (isInAnalyzerProcess == null) {
      isInAnalyzerProcess = isInServiceProcess(context, analyzerServiceClass)
      this.isInAnalyzerProcess = isInAnalyzerProcess
    }
    return isInAnalyzerProcess
  }

  @Suppress("ReturnCount")
  private fun isInServiceProcess(
    context: Context,
    serviceClass: Class<out Service>
  ): Boolean {
    val packageManager = context.packageManager
    val packageInfo: PackageInfo
    try {
      packageInfo = packageManager.getPackageInfo(context.packageName, PackageManager.GET_SERVICES)
    } catch (e: Exception) {
      SharkLog.d(e) { "Could not get package info for ${context.packageName}" }
      return false
    }

    val mainProcess = packageInfo.applicationInfo.processName

    val component = ComponentName(context, serviceClass)
    val serviceInfo: ServiceInfo
    try {
      serviceInfo =
        packageManager.getServiceInfo(component, PackageManager.GET_DISABLED_COMPONENTS)
    } catch (ignored: PackageManager.NameNotFoundException) {
      // Service is disabled.
      return false
    }

    if (serviceInfo.processName == null) {
      SharkLog.d { "Did not expect service $serviceClass to have a null process name" }
      return false
    } else if (serviceInfo.processName == mainProcess) {
      SharkLog.d { "Did not expect service $serviceClass to run in main process $mainProcess" }
      // Technically we are in the service process, but we're not in the service dedicated process.
      return false
    }

    val myPid = android.os.Process.myPid()
    val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    var myProcess: ActivityManager.RunningAppProcessInfo? = null
    val runningProcesses: List<ActivityManager.RunningAppProcessInfo>?
    try {
      runningProcesses = activityManager.runningAppProcesses
    } catch (exception: SecurityException) {
      // https://github.com/square/leakcanary/issues/948
      SharkLog.d { "Could not get running app processes $exception" }
      return false
    }

    if (runningProcesses != null) {
      for (process in runningProcesses) {
        if (process.pid == myPid) {
          myProcess = process
          break
        }
      }
    }
    if (myProcess == null) {
      SharkLog.d { "Could not find running process for $myPid" }
      return false
    }

    return myProcess.processName == serviceInfo.processName
  }
}

package leakcanary

import android.app.Application
import leakcanary.internal.BackgroundListener
import leakcanary.internal.friendly.checkMainThread
import shark.SharkLog
import java.util.concurrent.Executor

class BackgroundTrigger(
  private val application: Application,
  private val analysisClient: HeapAnalysisClient,
  /**
   * The executor on which the analysis is performed and on which [analysisCallback] is called.
   * This should likely be a single thread executor with a background thread priority.
   */
  private val analysisExecutor: Executor,

  processInfo: ProcessInfo = ProcessInfo.Real,

  /**
   * Called back with a [HeapAnalysisJob.Result] after the app has entered background and a
   * heap analysis was attempted. This is called on the same thread that the analysis was
   * performed on.
   *
   * Defaults to logging to [SharkLog] (don't forget to set [SharkLog.logger] if you do want to see
   * logs).
   */
  private val analysisCallback: (HeapAnalysisJob.Result) -> Unit = { result ->
    SharkLog.d { "$result" }
  },
) {

  @Volatile
  private var currentJob: HeapAnalysisJob? = null

  private val backgroundListener = BackgroundListener(processInfo) { appInBackgroundNow ->
    if (appInBackgroundNow) {
      check(currentJob == null) {
        "Current job set to null when leaving background"
      }

      val job =
        analysisClient.newJob(JobContext(BackgroundTrigger::class))
      currentJob = job
      analysisExecutor.execute {
        val result = job.execute()
        currentJob = null
        analysisCallback(result)
      }
    } else {
      currentJob?.cancel("app left background")
      currentJob = null
    }
  }

  fun start() {
    checkMainThread()
    backgroundListener.install(application)
  }

  fun stop() {
    checkMainThread()
    backgroundListener.uninstall(application)
  }
}

package leakcanary

import leakcanary.HeapAnalysisInterceptor.Chain
import leakcanary.HeapAnalysisJob.Result

/**
 * An interceptor that runs only when [evaluateCondition] returns true.
 */
class ConditionalInterceptor(
  private val delegate: HeapAnalysisInterceptor,
  private val evaluateCondition: (HeapAnalysisJob) -> Boolean
) : HeapAnalysisInterceptor {
  override fun intercept(chain: Chain): Result {
    if (evaluateCondition(chain.job)) {
      return delegate.intercept(object : Chain {
        override val job = chain.job

        override fun proceed(): Result {
          return chain.proceed()
        }
      })
    } else {
      return chain.proceed()
    }
  }
}

package leakcanary

import android.os.Build
import leakcanary.HeapAnalysisInterceptor.Chain

class GoodAndroidVersionInterceptor : HeapAnalysisInterceptor {
  private val errorMessage: String? by lazy {
    val sdkInt = Build.VERSION.SDK_INT
    if (// findObjectById() sometimes failing. See #1759
      sdkInt != 23 &&
      // findObjectById() sometimes failing. See #1759
      sdkInt != 25 &&
      // Android 11 seem to sometimes have super slow heap dumps.
      // See https://issuetracker.google.com/issues/168634429
      sdkInt < 30
    ) {
      null
    } else {
      "Build.VERSION.SDK_INT $sdkInt not supported"
    }
  }

  override fun intercept(chain: Chain): HeapAnalysisJob.Result {
    errorMessage?.let {
      chain.job.cancel(it)
    }
    return chain.proceed()
  }
}

package leakcanary

import android.app.Application
import leakcanary.internal.RealHeapAnalysisJob
import leakcanary.internal.RealHeapAnalysisJob.Companion.HPROF_PREFIX
import leakcanary.internal.RealHeapAnalysisJob.Companion.HPROF_SUFFIX
import java.io.File

class HeapAnalysisClient(
  /**
   * Provides the directory where heap dumps should be stored.
   * The passed in directory SHOULD be part of the app internal storage, and private to the
   * app, to guarantee no other app can read the heap dumps.
   *
   * You should probably pass in [android.app.Application.getCacheDir] or a sub directory
   * of the cache directory, as the cache directory will never be backed up and it will be
   * cleared if the user needs space.
   *
   * This will be invoked on the thread used to execute analysis jobs. The main reason for
   * the delayed invocation is because [android.app.Application.getCacheDir] might perform
   * IO and trigger StrictMode violations.
   */
  private val heapDumpDirectoryProvider: () -> File,
  private val config: HeapAnalysisConfig,
  private val interceptors: List<HeapAnalysisInterceptor>
) {

  fun newJob(context: JobContext = JobContext()): HeapAnalysisJob {
    return RealHeapAnalysisJob(heapDumpDirectoryProvider, config, interceptors, context)
  }

  fun deleteHeapDumpFiles() {
    val heapDumpFiles = heapDumpDirectoryProvider().listFiles { _, name ->
      name.startsWith(HPROF_PREFIX) && name.endsWith(HPROF_SUFFIX)
    }
    heapDumpFiles?.forEach { it.delete() }
  }

  companion object {
    fun defaultInterceptors(application: Application): List<HeapAnalysisInterceptor> {
      return listOf(
        GoodAndroidVersionInterceptor(),
        MinimumDiskSpaceInterceptor(application),
        MinimumMemoryInterceptor(application),
        MinimumElapsedSinceStartInterceptor(),
        OncePerPeriodInterceptor(application),
        SaveResourceIdsInterceptor(application.resources)
      )
    }
  }
}

package leakcanary

import shark.AndroidMetadataExtractor
import shark.AndroidObjectInspectors
import shark.AndroidReferenceMatchers
import shark.FilteringLeakingObjectFinder
import shark.LeakingObjectFinder
import shark.MetadataExtractor
import shark.ObjectInspector
import shark.ReferenceMatcher

data class HeapAnalysisConfig(

  /**
   * Known patterns of references in the heap, added here either to ignore them
   * ([IgnoredReferenceMatcher]) or to mark them as library leaks ([LibraryLeakReferenceMatcher]).
   *
   * When adding your own custom [LibraryLeakReferenceMatcher] instances, you'll most
   * likely want to set [LibraryLeakReferenceMatcher.patternApplies] with a filter that checks
   * for the Android OS version and manufacturer. The build information can be obtained by calling
   * [shark.AndroidBuildMirror.fromHeapGraph].
   *
   * Defaults to [AndroidReferenceMatchers.appDefaults]
   */
  val referenceMatchers: List<ReferenceMatcher> = AndroidReferenceMatchers.appDefaults,

  /**
   * List of [ObjectInspector] that provide LeakCanary with insights about objects found in the
   * heap. You can create your own [ObjectInspector] implementations, and also add
   * a [shark.AppSingletonInspector] instance created with the list of internal singletons.
   *
   * Defaults to [AndroidObjectInspectors.appDefaults]
   */
  val objectInspectors: List<ObjectInspector> = AndroidObjectInspectors.appDefaults,

  /**
   * Extracts metadata from a hprof to be reported in [shark.HeapAnalysisSuccess.metadata].
   * Called on a background thread during heap analysis.
   *
   * Defaults to [AndroidMetadataExtractor]
   */
  val metadataExtractor: MetadataExtractor = AndroidMetadataExtractor,

  /**
   * Whether to compute the retained heap size, which is the total number of bytes in memory that
   * would be reclaimed if the detected leaks didn't happen. This includes native memory
   * associated to Java objects (e.g. Android bitmaps).
   *
   * Computing the retained heap size can slow down the analysis because it requires navigating
   * from GC roots through the entire object graph, whereas [shark.HeapAnalyzer] would otherwise
   * stop as soon as all leaking instances are found.
   *
   * Defaults to true.
   */
  val computeRetainedHeapSize: Boolean = true,

  /**
   * Finds the objects that are leaking, for which LeakCanary will compute leak traces.
   *
   * Defaults to a [FilteringLeakingObjectFinder] that scans all objects in the heap dump and
   * delegates the decision to [AndroidObjectInspectors.appLeakingObjectFilters].
   */
  val leakingObjectFinder: LeakingObjectFinder = FilteringLeakingObjectFinder(
    AndroidObjectInspectors.appLeakingObjectFilters
  ),

  /**
   * Whether the first step after a heap dump should be to replace the content of all arrays with
   * zeroes. This increases the overall processing time but limits the amount of time the heap
   * dump exists on disk with potential PII.
   */
  val stripHeapDump: Boolean = false
)

package leakcanary

fun interface HeapAnalysisInterceptor {

  fun intercept(chain: Chain): HeapAnalysisJob.Result

  interface Chain {
    val job: HeapAnalysisJob

    fun proceed(): HeapAnalysisJob.Result
  }
}

package leakcanary

import shark.HeapAnalysis

/**
 * A [HeapAnalysisJob] represents a single prepared request to analyze the heap. It cannot be
 * executed twice.
 */
interface HeapAnalysisJob {

  /**
   * In memory store, mutable and thread safe. This allows passing data to interceptors.
   */
  val context: JobContext

  /**
   * true if [execute] has been called. It is an
   * error to call [execute] more than once.
   */
  val executed: Boolean

  /**
   * true of [cancel] has been called or if an [HeapAnalysisInterceptor] has returned
   * [Result.Canceled] from [HeapAnalysisInterceptor.intercept].
   */
  val canceled: Boolean

  /**
   * Starts the analysis job immediately, and blocks until a result is available.
   *
   * @return Either [Result.Done] if the analysis was attempted or [Result.Canceled]
   */
  fun execute(): Result

  /** Cancels the job, if possible. Jobs that are already complete cannot be canceled. */
  fun cancel(cancelReason: String)

  sealed class Result {

    data class Done(
      val analysis: HeapAnalysis,
      /**
       * The time spent stripping the hprof of any data if [HeapAnalysisConfig.stripHeapDump] is
       * true, null otherwise.
       */
      val stripHeapDumpDurationMillis: Long? = null
      ) : Result()

    data class Canceled(val cancelReason: String) : Result()
  }
}

package leakcanary

import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass

/**
 * In memory store that can be used to store objects in a given [HeapAnalysisJob] instance.
 * This is a simple [MutableMap] of [String] to [Any], but with unsafe generics access.
 *
 * By convention, [starter] should be the class that triggered the start of the job.
 */
class JobContext constructor(val starter: Class<*>? = null) {

  constructor(starter: KClass<*>) : this(starter.java)

  private val store = ConcurrentHashMap<String, Any?>()

  operator fun <T> get(key: String): T? {
    @Suppress("UNCHECKED_CAST")
    return store[key] as T?
  }

  /**
   * @see MutableMap.getOrPut
   */
  fun <T> getOrPut(
    key: String,
    defaultValue: () -> T
  ): T {
    @Suppress("UNCHECKED_CAST")
    return store.getOrPut(key, {
      defaultValue()
    }) as T
  }

  /**
   * @see MutableMap.set
   */
  operator fun <T> set(
    key: String,
    value: T
  ) {
    store[key] = (value as Any?)
  }

  /**
   * @see MutableMap.containsKey
   */
  operator fun contains(key: String): Boolean {
    return store.containsKey(key)
  }

  /**
   * @see MutableMap.remove
   */
  operator fun minusAssign(key: String) {
    @Suppress("UNCHECKED_CAST")
    store -= key
  }
}

package leakcanary

import android.app.Application
import android.os.Build.VERSION.SDK_INT
import android.os.StatFs
import leakcanary.HeapAnalysisInterceptor.Chain
import leakcanary.HeapAnalysisJob.Result

class MinimumDiskSpaceInterceptor(
  private val application: Application,
  private val minimumDiskSpaceBytes: Long = 200_000_000,
  private val processInfo: ProcessInfo = ProcessInfo.Real
) : HeapAnalysisInterceptor {

  override fun intercept(chain: Chain): Result {
    val availableDiskSpace = processInfo.availableDiskSpaceBytes(application.filesDir!!)
    if (availableDiskSpace < minimumDiskSpaceBytes) {
      chain.job.cancel("availableDiskSpace $availableDiskSpace < minimumDiskSpaceBytes $minimumDiskSpaceBytes")
    }
    return chain.proceed()
  }
}

package leakcanary

import android.annotation.SuppressLint
import leakcanary.HeapAnalysisInterceptor.Chain
import leakcanary.HeapAnalysisJob.Result
import java.util.concurrent.TimeUnit

@SuppressLint("NewApi")
class MinimumElapsedSinceStartInterceptor(
  private val minimumElapsedSinceStartMillis: Long = TimeUnit.SECONDS.toMillis(30),
  private val processInfo: ProcessInfo = ProcessInfo.Real
) : HeapAnalysisInterceptor {

  override fun intercept(chain: Chain): Result {
    if (processInfo.elapsedMillisSinceStart < minimumElapsedSinceStartMillis) {
      chain.job.cancel("app started less than $minimumElapsedSinceStartMillis ms ago.")
    }
    return chain.proceed()
  }
}

package leakcanary

import android.app.Application
import leakcanary.HeapAnalysisInterceptor.Chain
import leakcanary.ProcessInfo.AvailableRam.BelowThreshold
import leakcanary.ProcessInfo.AvailableRam.LowRamDevice
import leakcanary.ProcessInfo.AvailableRam.Memory

class MinimumMemoryInterceptor(
  private val application: Application,
  private val minimumRequiredAvailableMemoryBytes: Long = 100_000_000,
  private val processInfo: ProcessInfo = ProcessInfo.Real
) : HeapAnalysisInterceptor {

  override fun intercept(chain: Chain): HeapAnalysisJob.Result {
    when (val memory = processInfo.availableRam(application)) {
      LowRamDevice -> {
        chain.job.cancel("low ram device")
      }
      BelowThreshold -> {
        chain.job.cancel("low memory")
      }
      is Memory -> {
        if (memory.bytes < minimumRequiredAvailableMemoryBytes) {
          chain.job.cancel(
            "not enough free memory: available ${memory.bytes} < min $minimumRequiredAvailableMemoryBytes"
          )
        }
      }
    }

    return chain.proceed()
  }
}

package leakcanary

import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import leakcanary.HeapAnalysisInterceptor.Chain
import leakcanary.HeapAnalysisJob.Result
import java.util.concurrent.TimeUnit

/**
 * Proceeds once per [period] (of time) and then cancels all follow up jobs until [period] has
 * passed.
 */
class OncePerPeriodInterceptor(
  application: Application,
  private val periodMillis: Long = TimeUnit.DAYS.toMillis(1)
) : HeapAnalysisInterceptor {

  private val preference: SharedPreferences by lazy {
    application.getSharedPreferences("OncePerPeriodInterceptor", Context.MODE_PRIVATE)!!
  }

  override fun intercept(chain: Chain): Result {
    val lastStartTimestamp = preference.getLong(LAST_START_TIMESTAMP_KEY, 0)
    val now = System.currentTimeMillis()
    val elapsedMillis = now - lastStartTimestamp

    if (elapsedMillis < periodMillis) {
      chain.job.cancel("not enough time elapsed since last analysis: elapsed $elapsedMillis ms < period $periodMillis ms")
    }

    return chain.proceed().apply {
      if (this is Result.Done) {
        preference.edit().putLong(LAST_START_TIMESTAMP_KEY, now).apply()
      }
    }
  }

  fun forget() {
    preference.edit().clear().apply()
  }

  companion object {
    private const val LAST_START_TIMESTAMP_KEY = "last_start_timestamp"
  }
}

package leakcanary

import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.ActivityManager.MemoryInfo
import android.app.ActivityManager.RunningAppProcessInfo
import android.content.Context
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Process
import android.os.StatFs
import android.os.SystemClock
import android.system.Os
import android.system.OsConstants
import leakcanary.ProcessInfo.AvailableRam.BelowThreshold
import leakcanary.ProcessInfo.AvailableRam.LowRamDevice
import leakcanary.ProcessInfo.AvailableRam.Memory
import java.io.BufferedReader
import java.io.File
import java.io.FileReader

interface ProcessInfo {

  val isImportanceBackground: Boolean

  val elapsedMillisSinceStart: Long

  fun availableDiskSpaceBytes(path: File): Long

  sealed class AvailableRam {
    object LowRamDevice : AvailableRam()
    object BelowThreshold : AvailableRam()
    class Memory(val bytes: Long) : AvailableRam()
  }

  fun availableRam(context: Context): AvailableRam

  @SuppressLint("NewApi")
  object Real : ProcessInfo {
    private val memoryOutState = RunningAppProcessInfo()
    private val memoryInfo = MemoryInfo()

    private val processStartUptimeMillis by lazy {
      Process.getStartUptimeMillis()
    }

    private val processForkRealtimeMillis by lazy {
      readProcessForkRealtimeMillis()
    }

    override val isImportanceBackground: Boolean
      get() {
        ActivityManager.getMyMemoryState(memoryOutState)
        return memoryOutState.importance >= RunningAppProcessInfo.IMPORTANCE_BACKGROUND
      }

    override val elapsedMillisSinceStart: Long
      get() = if (Build.VERSION.SDK_INT >= 24) {
        SystemClock.uptimeMillis() - processStartUptimeMillis
      } else {
        SystemClock.elapsedRealtime() - processForkRealtimeMillis
      }

    @SuppressLint("UsableSpace")
    override fun availableDiskSpaceBytes(path: File) = path.usableSpace

    override fun availableRam(context: Context): AvailableRam {
      val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager

      if (SDK_INT >= 19 && activityManager.isLowRamDevice) {
        return LowRamDevice
      } else {
        activityManager.getMemoryInfo(memoryInfo)

        return if (memoryInfo.lowMemory || memoryInfo.availMem <= memoryInfo.threshold) {
          BelowThreshold
        } else {
          val systemAvailableMemory = memoryInfo.availMem - memoryInfo.threshold

          val runtime = Runtime.getRuntime()
          val appUsedMemory = runtime.totalMemory() - runtime.freeMemory()
          val appAvailableMemory = runtime.maxMemory() - appUsedMemory

          val availableMemory = systemAvailableMemory.coerceAtMost(appAvailableMemory)
          Memory(availableMemory)
        }
      }
    }

    /**
     * See https://dev.to/pyricau/android-vitals-when-did-my-app-start-24p4#process-fork-time
     */
    private fun readProcessForkRealtimeMillis(): Long {
      val myPid = Process.myPid()
      val ticksAtProcessStart = readProcessStartTicks(myPid)

      val ticksPerSecond = if (SDK_INT >= 21) {
        Os.sysconf(OsConstants._SC_CLK_TCK)
      } else {
        val tckConstant = try {
          Class.forName("android.system.OsConstants").getField("_SC_CLK_TCK").getInt(null)
        } catch (e: ClassNotFoundException) {
          Class.forName("libcore.io.OsConstants").getField("_SC_CLK_TCK").getInt(null)
        }
        val os = Class.forName("libcore.io.Libcore").getField("os").get(null)!!
        os::class.java.getMethod("sysconf", Integer.TYPE).invoke(os, tckConstant) as Long
      }
      return ticksAtProcessStart * 1000 / ticksPerSecond
    }

    // Benchmarked (with Jetpack Benchmark) on Pixel 3 running
    // Android 10. Median time: 0.13ms
    private fun readProcessStartTicks(pid: Int): Long {
      val path = "/proc/$pid/stat"
      val stat = BufferedReader(FileReader(path)).use { reader ->
        reader.readLine()
      }
      val fields = stat.substringAfter(") ")
        .split(' ')
      return fields[19].toLong()
    }
  }
}

package leakcanary

import android.content.res.Resources
import android.content.res.Resources.NotFoundException
import leakcanary.HeapAnalysisInterceptor.Chain
import leakcanary.HeapAnalysisJob.Result
import shark.AndroidResourceIdNames

/**
 * Interceptor that saves the names of R.id.* entries and their associated int values to a static
 * field that can then be read from the heap dump.
 */
class SaveResourceIdsInterceptor(private val resources: Resources) : HeapAnalysisInterceptor {
  override fun intercept(chain: Chain): Result {
    saveResourceIdNamesToMemory()
    return chain.proceed()
  }

  private fun saveResourceIdNamesToMemory() {
    AndroidResourceIdNames.saveToMemory(
      getResourceTypeName = { id ->
        try {
          resources.getResourceTypeName(id)
        } catch (e: NotFoundException) {
          null
        }
      },
      getResourceEntryName = { id ->
        try {
          resources.getResourceEntryName(id)
        } catch (e: NotFoundException) {
          null
        }
      })
  }
}

package leakcanary

import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_SCREEN_OFF
import android.content.Intent.ACTION_SCREEN_ON
import android.content.IntentFilter
import leakcanary.internal.friendly.checkMainThread
import shark.SharkLog
import java.util.concurrent.Executor

class ScreenOffTrigger(
  private val application: Application,
  private val analysisClient: HeapAnalysisClient,
  /**
   * The executor on which the analysis is performed and on which [analysisCallback] is called.
   * This should likely be a single thread executor with a background thread priority.
   */
  private val analysisExecutor: Executor,

  /**
   * Called back with a [HeapAnalysisJob.Result] after the screen went off and a
   * heap analysis was attempted. This is called on the same thread that the analysis was
   * performed on.
   *
   * Defaults to logging to [SharkLog] (don't forget to set [SharkLog.logger] if you do want to see
   * logs).
   */
  private val analysisCallback: (HeapAnalysisJob.Result) -> Unit = { result ->
    SharkLog.d { "$result" }
  },
) {

  @Volatile
  private var currentJob: HeapAnalysisJob? = null

  private val screenReceiver = object : BroadcastReceiver() {
    override fun onReceive(
      context: Context,
      intent: Intent
    ) {
      if (intent.action == ACTION_SCREEN_OFF) {
        if (currentJob == null) {
          val job =
            analysisClient.newJob(JobContext(ScreenOffTrigger::class))
          currentJob = job
          analysisExecutor.execute {
            val result = job.execute()
            currentJob = null
            analysisCallback(result)
          }
        }
      } else {
        currentJob?.cancel("screen on again")
        currentJob = null
      }
    }
  }

  fun start() {
    checkMainThread()
    application.registerReceiver(screenReceiver, IntentFilter().apply {
      addAction(ACTION_SCREEN_ON)
      addAction(ACTION_SCREEN_OFF)
    })
  }

  fun stop() {
    checkMainThread()
    application.unregisterReceiver(screenReceiver)
  }
}

package leakcanary

import android.util.Log
import shark.SharkLog
import shark.SharkLog.Logger

class LogcatSharkLog : Logger {

  override fun d(message: String) {
    if (message.length < 4000) {
      Log.d("LeakCanary", message)
    } else {
      message.lines().forEach { line ->
        Log.d("LeakCanary", line)
      }
    }
  }

  override fun d(
    throwable: Throwable,
    message: String
  ) {
    d("$message\n${Log.getStackTraceString(throwable)}")
  }

  companion object {
    fun install() {
      SharkLog.logger = LogcatSharkLog()
    }
  }
}

package leakcanary

import android.app.Activity
import android.app.Application
import leakcanary.internal.friendly.noOpDelegate

/**
 * Expects activities to become weakly reachable soon after they receive the [Activity.onDestroy]
 * callback.
 */
class ActivityWatcher(
  private val application: Application,
  private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        reachabilityWatcher.expectWeaklyReachable(
          activity, "${activity::class.java.name} received Activity#onDestroy() callback"
        )
      }
    }

  override fun install() {
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
  }

  override fun uninstall() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
  }
}

package leakcanary

import android.app.Application
import android.os.SystemClock
import leakcanary.AppWatcher.objectWatcher
import leakcanary.internal.LeakCanaryDelegate
import leakcanary.internal.friendly.checkMainThread
import leakcanary.internal.friendly.mainHandler
import leakcanary.internal.isDebuggableBuild
import shark.SharkLog
import java.lang.RuntimeException
import java.util.concurrent.TimeUnit

/**
 * The entry point API for using [ObjectWatcher] in an Android app. [AppWatcher.objectWatcher] is
 * in charge of detecting retained objects, and [AppWatcher] is auto configured on app start to
 * pass it activity and fragment instances. Call [ObjectWatcher.watch] on [objectWatcher] to
 * watch any other object that you expect to be unreachable.
 */
object AppWatcher {

  private const val RETAINED_DELAY_NOT_SET = -1L

  @Volatile
  private var retainedDelayMillis = RETAINED_DELAY_NOT_SET

  private var installCause: Exception? = null

  /**
   * The [ObjectWatcher] used by AppWatcher to detect retained objects.
   * Only set when [isInstalled] is true.
   */
  val objectWatcher = ObjectWatcher(
    clock = { SystemClock.uptimeMillis() },
    checkRetainedExecutor = {
      check(isInstalled) {
        "AppWatcher not installed"
      }
      mainHandler.postDelayed(it, retainedDelayMillis)
    },
    isEnabled = { true }
  )

  /** @see [manualInstall] */
  val isInstalled: Boolean
    get() = installCause != null

  /**
   * Enables usage of [AppWatcher.objectWatcher] which will expect passed in objects to become
   * weakly reachable within [retainedDelayMillis] ms and if not will trigger LeakCanary (if
   * LeakCanary is in the classpath).
   *
   * In the main process, this method is automatically called with default parameter values  on app
   * startup. You can call this method directly to customize the installation, however you must
   * first disable the automatic call by overriding the `leak_canary_watcher_auto_install` boolean
   * resource:
   *
   * ```
   * <?xml version="1.0" encoding="utf-8"?>
   * <resources>
   *   <bool name="leak_canary_watcher_auto_install">false</bool>
   * </resources>
   * ```
   *
   * [watchersToInstall] can be customized to a subset of the default app watchers:
   *
   * ```
   * val watchersToInstall = AppWatcher.appDefaultWatchers(application)
   *   .filter { it !is RootViewWatcher }
   * AppWatcher.manualInstall(
   *   application = application,
   *   watchersToInstall = watchersToInstall
   * )
   * ```
   *
   * [watchersToInstall] can also be customized to ignore specific instances (e.g. here ignoring
   * leaks of BadSdkLeakingFragment):
   *
   * ```
   * val watchersToInstall = AppWatcher.appDefaultWatchers(application, ReachabilityWatcher { watchedObject, description ->
   *   if (watchedObject !is BadSdkLeakingFragment) {
   *     AppWatcher.objectWatcher.expectWeaklyReachable(watchedObject, description)
   *   }
   * })
   * AppWatcher.manualInstall(
   *   application = application,
   *   watchersToInstall = watchersToInstall
   * )
   * ```
   */
  @JvmOverloads
  fun manualInstall(
    application: Application,
    retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
    watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
  ) {
    checkMainThread()
    if (isInstalled) {
      throw IllegalStateException(
        "AppWatcher already installed, see exception cause for prior install call", installCause
      )
    }
    check(retainedDelayMillis >= 0) {
      "retainedDelayMillis $retainedDelayMillis must be at least 0 ms"
    }
    installCause = RuntimeException("manualInstall() first called here")
    this.retainedDelayMillis = retainedDelayMillis
    if (application.isDebuggableBuild) {
      LogcatSharkLog.install()
    }
    // Requires AppWatcher.objectWatcher to be set
    LeakCanaryDelegate.loadLeakCanary(application)

    watchersToInstall.forEach {
      it.install()
    }
  }

  /**
   * Creates a new list of default app [InstallableWatcher], created with the passed in
   * [reachabilityWatcher] (which defaults to [objectWatcher]). Once installed,
   * these watchers will pass in to [reachabilityWatcher] objects that they expect to become
   * weakly reachable.
   *
   * The passed in [reachabilityWatcher] should probably delegate to [objectWatcher] but can
   * be used to filter out specific instances.
   */
  fun appDefaultWatchers(
    application: Application,
    reachabilityWatcher: ReachabilityWatcher = objectWatcher
  ): List<InstallableWatcher> {
    return listOf(
      ActivityWatcher(application, reachabilityWatcher),
      FragmentAndViewModelWatcher(application, reachabilityWatcher),
      RootViewWatcher(reachabilityWatcher),
      ServiceWatcher(reachabilityWatcher)
    )
  }

  @Deprecated("Call AppWatcher.manualInstall() ")
  data class Config(
    @Deprecated("Call AppWatcher.manualInstall() with a custom watcher list")
    val watchActivities: Boolean = true,

    @Deprecated("Call AppWatcher.manualInstall() with a custom watcher list")
    val watchFragments: Boolean = true,

    @Deprecated("Call AppWatcher.manualInstall() with a custom watcher list")
    val watchFragmentViews: Boolean = true,

    @Deprecated("Call AppWatcher.manualInstall() with a custom watcher list")
    val watchViewModels: Boolean = true,

    @Deprecated("Call AppWatcher.manualInstall() with a custom retainedDelayMillis value")
    val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5),

    @Deprecated("Call AppWatcher.appDefaultWatchers() with a custom ReachabilityWatcher")
    val enabled: Boolean = true
  ) {

    @Deprecated("Configuration moved to AppWatcher.manualInstall()", replaceWith = ReplaceWith(""))
    @Suppress("NEWER_VERSION_IN_SINCE_KOTLIN")
    @SinceKotlin("999.9") // Hide from Kotlin code, this method is only for Java code
    fun newBuilder(): Builder = Builder(this)

    @Deprecated("Configuration moved to XML resources")
    class Builder internal constructor(config: Config) {
      private var watchActivities = config.watchActivities
      private var watchFragments = config.watchFragments
      private var watchFragmentViews = config.watchFragmentViews
      private var watchViewModels = config.watchViewModels
      private var watchDurationMillis = config.watchDurationMillis

      /** Deprecated. @see [Config.enabled] */
      @Deprecated("see [Config.enabled]", replaceWith = ReplaceWith(""))
      fun enabled(enabled: Boolean) = this

      /** @see [Config.watchActivities] */
      @Deprecated("see [Config.watchActivities]", replaceWith = ReplaceWith(""))
      fun watchActivities(watchActivities: Boolean) =
        apply { this.watchActivities = watchActivities }

      @Deprecated("see [Config.watchFragments]", replaceWith = ReplaceWith(""))
        /** @see [Config.watchFragments] */
      fun watchFragments(watchFragments: Boolean) =
        apply { this.watchFragments = watchFragments }

      @Deprecated("see [Config.watchFragmentViews]", replaceWith = ReplaceWith(""))
        /** @see [Config.watchFragmentViews] */
      fun watchFragmentViews(watchFragmentViews: Boolean) =
        apply { this.watchFragmentViews = watchFragmentViews }

      @Deprecated("see [Config.watchViewModels]", replaceWith = ReplaceWith(""))
        /** @see [Config.watchViewModels] */
      fun watchViewModels(watchViewModels: Boolean) =
        apply { this.watchViewModels = watchViewModels }

      @Deprecated("see [Config.watchDurationMillis]", replaceWith = ReplaceWith(""))
        /** @see [Config.watchDurationMillis] */
      fun watchDurationMillis(watchDurationMillis: Long) =
        apply { this.watchDurationMillis = watchDurationMillis }

      @Deprecated("Configuration moved to AppWatcher.manualInstall()")
      fun build() = config.copy(
        watchActivities = watchActivities,
        watchFragments = watchFragments,
        watchFragmentViews = watchFragmentViews,
        watchViewModels = watchViewModels,
        watchDurationMillis = watchDurationMillis
      )
    }
  }

  @Deprecated("Configuration moved to AppWatcher.manualInstall()")
  @JvmStatic @Volatile
  var config: Config = Config()

}

package leakcanary

import android.app.Activity
import android.app.Application
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O
import android.os.Bundle
import leakcanary.internal.AndroidOFragmentDestroyWatcher
import leakcanary.internal.friendly.noOpDelegate

/**
 * Expects:
 * - Fragments (Support Library, Android X and AOSP) to become weakly reachable soon after they
 * receive the Fragment#onDestroy() callback.
 * - Fragment views (Support Library, Android X and AOSP) to become weakly reachable soon after
 * fragments receive the Fragment#onDestroyView() callback.
 * - Android X view models (both activity and fragment view models) to become weakly reachable soon
 * after they received the ViewModel#onCleared() callback.
 */
class FragmentAndViewModelWatcher(
  private val application: Application,
  private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

  private val fragmentDestroyWatchers: List<(Activity) -> Unit> = run {
    val fragmentDestroyWatchers = mutableListOf<(Activity) -> Unit>()

    if (SDK_INT >= O) {
      fragmentDestroyWatchers.add(
        AndroidOFragmentDestroyWatcher(reachabilityWatcher)
      )
    }

    getWatcherIfAvailable(
      ANDROIDX_FRAGMENT_CLASS_NAME,
      ANDROIDX_FRAGMENT_DESTROY_WATCHER_CLASS_NAME,
      reachabilityWatcher
    )?.let {
      fragmentDestroyWatchers.add(it)
    }

    getWatcherIfAvailable(
      ANDROID_SUPPORT_FRAGMENT_CLASS_NAME,
      ANDROID_SUPPORT_FRAGMENT_DESTROY_WATCHER_CLASS_NAME,
      reachabilityWatcher
    )?.let {
      fragmentDestroyWatchers.add(it)
    }
    fragmentDestroyWatchers
  }

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
      ) {
        for (watcher in fragmentDestroyWatchers) {
          watcher(activity)
        }
      }
    }

  override fun install() {
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
  }

  override fun uninstall() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
  }

  private fun getWatcherIfAvailable(
    fragmentClassName: String,
    watcherClassName: String,
    reachabilityWatcher: ReachabilityWatcher
  ): ((Activity) -> Unit)? {

    return if (classAvailable(fragmentClassName) &&
      classAvailable(watcherClassName)
    ) {
      val watcherConstructor =
        Class.forName(watcherClassName).getDeclaredConstructor(ReachabilityWatcher::class.java)
      @Suppress("UNCHECKED_CAST")
      watcherConstructor.newInstance(reachabilityWatcher) as (Activity) -> Unit
    } else {
      null
    }
  }

  private fun classAvailable(className: String): Boolean {
    return try {
      Class.forName(className)
      true
    } catch (e: Throwable) {
      // e is typically expected to be a ClassNotFoundException
      // Unfortunately, prior to version 25.0.2 of the support library the
      // FragmentManager.FragmentLifecycleCallbacks class was a non static inner class.
      // Our AndroidSupportFragmentDestroyWatcher class is compiled against the static version of
      // the FragmentManager.FragmentLifecycleCallbacks class, leading to the
      // AndroidSupportFragmentDestroyWatcher class being rejected and a NoClassDefFoundError being
      // thrown here. So we're just covering our butts here and catching everything, and assuming
      // any throwable means "can't use this". See https://github.com/square/leakcanary/issues/1662
      false
    }
  }

  companion object {
    private const val ANDROIDX_FRAGMENT_CLASS_NAME = "androidx.fragment.app.Fragment"
    private const val ANDROIDX_FRAGMENT_DESTROY_WATCHER_CLASS_NAME =
      "leakcanary.internal.AndroidXFragmentDestroyWatcher"

    // Using a string builder to prevent Jetifier from changing this string to Android X Fragment
    @Suppress("VariableNaming", "PropertyName")
    private val ANDROID_SUPPORT_FRAGMENT_CLASS_NAME =
      StringBuilder("android.").append("support.v4.app.Fragment")
        .toString()
    private const val ANDROID_SUPPORT_FRAGMENT_DESTROY_WATCHER_CLASS_NAME =
      "leakcanary.internal.AndroidSupportFragmentDestroyWatcher"
  }
}

package leakcanary

interface InstallableWatcher {

  fun install()

  fun uninstall()
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary

import android.app.Activity
import android.app.Dialog
import android.view.View
import android.view.View.OnAttachStateChangeListener
import com.squareup.leakcanary.objectwatcher.R
import curtains.Curtains
import curtains.OnRootViewAddedListener
import curtains.WindowType.PHONE_WINDOW
import curtains.WindowType.POPUP_WINDOW
import curtains.WindowType.TOAST
import curtains.WindowType.TOOLTIP
import curtains.WindowType.UNKNOWN
import curtains.phoneWindow
import curtains.windowType
import curtains.wrappedCallback
import leakcanary.internal.friendly.mainHandler

/**
 * Expects root views to become weakly reachable soon after they are removed from the window
 * manager.
 */
class RootViewWatcher(
  private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

  private val listener = OnRootViewAddedListener { rootView ->
    val trackDetached = when(rootView.windowType) {
      PHONE_WINDOW -> {
        when (rootView.phoneWindow?.callback?.wrappedCallback) {
          // Activities are already tracked by ActivityWatcher
          is Activity -> false
          is Dialog -> rootView.resources.getBoolean(R.bool.leak_canary_watcher_watch_dismissed_dialogs)
          // Probably a DreamService
          else -> true
        }
      }
      // Android widgets keep detached popup window instances around.
      POPUP_WINDOW -> false
      TOOLTIP, TOAST, UNKNOWN -> true
    }
    if (trackDetached) {
      rootView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {

        val watchDetachedView = Runnable {
          reachabilityWatcher.expectWeaklyReachable(
            rootView, "${rootView::class.java.name} received View#onDetachedFromWindow() callback"
          )
        }

        override fun onViewAttachedToWindow(v: View) {
          mainHandler.removeCallbacks(watchDetachedView)
        }

        override fun onViewDetachedFromWindow(v: View) {
          mainHandler.post(watchDetachedView)
        }
      })
    }
  }

  override fun install() {
    Curtains.onRootViewsChangedListeners += listener
  }

  override fun uninstall() {
    Curtains.onRootViewsChangedListeners -= listener
  }
}

package leakcanary

import android.annotation.SuppressLint
import android.app.Service
import android.os.Build
import android.os.Handler
import android.os.IBinder
import leakcanary.internal.friendly.checkMainThread
import shark.SharkLog
import java.lang.ref.WeakReference
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Proxy
import java.util.WeakHashMap

/**
 * Expects services to become weakly reachable soon after they receive the [Service.onDestroy]
 * callback.
 */
@SuppressLint("PrivateApi")
class ServiceWatcher(private val reachabilityWatcher: ReachabilityWatcher) : InstallableWatcher {

  private val servicesToBeDestroyed = WeakHashMap<IBinder, WeakReference<Service>>()

  private val activityThreadClass by lazy { Class.forName("android.app.ActivityThread") }

  private val activityThreadInstance by lazy {
    activityThreadClass.getDeclaredMethod("currentActivityThread").invoke(null)!!
  }

  private val activityThreadServices by lazy {
    val mServicesField =
      activityThreadClass.getDeclaredField("mServices").apply { isAccessible = true }

    @Suppress("UNCHECKED_CAST")
    mServicesField[activityThreadInstance] as Map<IBinder, Service>
  }

  private var uninstallActivityThreadHandlerCallback: (() -> Unit)? = null
  private var uninstallActivityManager: (() -> Unit)? = null

  override fun install() {
    checkMainThread()
    check(uninstallActivityThreadHandlerCallback == null) {
      "ServiceWatcher already installed"
    }
    check(uninstallActivityManager == null) {
      "ServiceWatcher already installed"
    }
    try {
      swapActivityThreadHandlerCallback { mCallback ->
        uninstallActivityThreadHandlerCallback = {
          swapActivityThreadHandlerCallback {
            mCallback
          }
        }
        Handler.Callback { msg ->
          if (msg.what == STOP_SERVICE) {
            val key = msg.obj as IBinder
            activityThreadServices[key]?.let {
              onServicePreDestroy(key, it)
            }
          }
          mCallback?.handleMessage(msg) ?: false
        }
      }
      swapActivityManager { activityManagerInterface, activityManagerInstance ->
        uninstallActivityManager = {
          swapActivityManager { _, _ ->
            activityManagerInstance
          }
        }
        Proxy.newProxyInstance(
          activityManagerInterface.classLoader, arrayOf(activityManagerInterface)
        ) { _, method, args ->
          if (METHOD_SERVICE_DONE_EXECUTING == method.name) {
            val token = args!![0] as IBinder
            if (servicesToBeDestroyed.containsKey(token)) {
              onServiceDestroyed(token)
            }
          }
          try {
            if (args == null) {
              method.invoke(activityManagerInstance)
            } else {
              method.invoke(activityManagerInstance, *args)
            }
          } catch (invocationException: InvocationTargetException) {
            throw invocationException.targetException
          }
        }
      }
    } catch (ignored: Throwable) {
      SharkLog.d(ignored) { "Could not watch destroyed services" }
    }
  }

  override fun uninstall() {
    checkMainThread()
    uninstallActivityManager?.invoke()
    uninstallActivityThreadHandlerCallback?.invoke()
    uninstallActivityManager = null
    uninstallActivityThreadHandlerCallback = null
  }

  private fun onServicePreDestroy(
    token: IBinder,
    service: Service
  ) {
    servicesToBeDestroyed[token] = WeakReference(service)
  }

  private fun onServiceDestroyed(token: IBinder) {
    servicesToBeDestroyed.remove(token)?.also { serviceWeakReference ->
      serviceWeakReference.get()?.let { service ->
        reachabilityWatcher.expectWeaklyReachable(
          service, "${service::class.java.name} received Service#onDestroy() callback"
        )
      }
    }
  }

  private fun swapActivityThreadHandlerCallback(swap: (Handler.Callback?) -> Handler.Callback?) {
    val mHField =
      activityThreadClass.getDeclaredField("mH").apply { isAccessible = true }
    val mH = mHField[activityThreadInstance] as Handler

    val mCallbackField =
      Handler::class.java.getDeclaredField("mCallback").apply { isAccessible = true }
    val mCallback = mCallbackField[mH] as Handler.Callback?
    mCallbackField[mH] = swap(mCallback)
  }

  @SuppressLint("PrivateApi")
  private fun swapActivityManager(swap: (Class<*>, Any) -> Any) {
    val singletonClass = Class.forName("android.util.Singleton")
    val mInstanceField =
      singletonClass.getDeclaredField("mInstance").apply { isAccessible = true }

    val singletonGetMethod = singletonClass.getDeclaredMethod("get")

    val (className, fieldName) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      "android.app.ActivityManager" to "IActivityManagerSingleton"
    } else {
      "android.app.ActivityManagerNative" to "gDefault"
    }

    val activityManagerClass = Class.forName(className)
    val activityManagerSingletonField =
      activityManagerClass.getDeclaredField(fieldName).apply { isAccessible = true }
    val activityManagerSingletonInstance = activityManagerSingletonField[activityManagerClass]

    // Calling get() instead of reading from the field directly to ensure the singleton is
    // created.
    val activityManagerInstance = singletonGetMethod.invoke(activityManagerSingletonInstance)

    val iActivityManagerInterface = Class.forName("android.app.IActivityManager")
    mInstanceField[activityManagerSingletonInstance] =
      swap(iActivityManagerInterface, activityManagerInstance!!)
  }

  companion object {
    private const val STOP_SERVICE = 116

    private const val METHOD_SERVICE_DONE_EXECUTING = "serviceDoneExecuting"
  }
}

package leakcanary

/**
 * An interface to abstract the SystemClock.uptimeMillis() Android API in non Android artifacts.
 *
 * This is a functional interface with which you can create a [Clock] from a lambda.
 */
fun interface Clock {
  /**
   * On Android VMs, this should return android.os.SystemClock.uptimeMillis().
   */
  fun uptimeMillis(): Long

  companion object {
    /**
     * Utility function to create a [Clock] from the passed in [block] lambda
     * instead of using the anonymous `object : Clock` syntax.
     *
     * Usage:
     *
     * ```kotlin
     * val clock = Clock {
     *
     * }
     * ```
     */
    inline operator fun invoke(crossinline block: () -> Long): Clock =
      object : Clock {
        override fun uptimeMillis(): Long = block()
      }
  }
}

/*
 * Copyright (C) 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary

/**
 * [GcTrigger] is used to try triggering garbage collection and enqueuing [KeyedWeakReference] into
 * the associated [java.lang.ref.ReferenceQueue]. The default implementation [Default] comes from
 * AOSP.
 */
interface GcTrigger {

  /**
   * Attempts to run garbage collection.
   */
  fun runGc()

  /**
   * Default implementation of [GcTrigger].
   */
  object Default : GcTrigger {
    override fun runGc() {
      // Code taken from AOSP FinalizationTest:
      // https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
      // java/lang/ref/FinalizationTester.java
      // System.gc() does not garbage collect every time. Runtime.gc() is
      // more likely to perform a gc.
      Runtime.getRuntime()
        .gc()
      enqueueReferences()
      System.runFinalization()
    }

    private fun enqueueReferences() {
      // Hack. We don't have a programmatic way to wait for the reference queue daemon to move
      // references to the appropriate queues.
      try {
        Thread.sleep(100)
      } catch (e: InterruptedException) {
        throw AssertionError()
      }
    }
  }
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary

import leakcanary.KeyedWeakReference.Companion.heapDumpUptimeMillis
import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference

/**
 * A weak reference used by [ObjectWatcher] to determine which objects become weakly reachable
 * and which don't. [ObjectWatcher] uses [key] to keep track of [KeyedWeakReference] instances that
 * haven't made it into the associated [ReferenceQueue] yet.
 *
 * [heapDumpUptimeMillis] should be set with the current time from [Clock.uptimeMillis] right
 * before dumping the heap, so that we can later determine how long an object was retained.
 */
class KeyedWeakReference(
  referent: Any,
  val key: String,
  val description: String,
  val watchUptimeMillis: Long,
  referenceQueue: ReferenceQueue<Any>
) : WeakReference<Any>(
  referent, referenceQueue
) {
  /**
   * Time at which the associated object ([referent]) was considered retained, or -1 if it hasn't
   * been yet.
   */
  @Volatile
  var retainedUptimeMillis = -1L

  override fun clear() {
    super.clear()
    retainedUptimeMillis = -1L
  }

  companion object {
    @Volatile
    @JvmStatic var heapDumpUptimeMillis = 0L
  }
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary

import shark.SharkLog
import java.lang.ref.ReferenceQueue
import java.util.UUID
import java.util.concurrent.Executor

/**
 * [ObjectWatcher] can be passed objects to [watch]. It will create [KeyedWeakReference] instances
 * that reference watches objects, and check if those references have been cleared as expected on
 * the [checkRetainedExecutor] executor. If not, these objects are considered retained and
 * [ObjectWatcher] will then notify registered [OnObjectRetainedListener]s on that executor thread.
 *
 * [checkRetainedExecutor] is expected to run its tasks on a background thread, with a significant
 * delay to give the GC the opportunity to identify weakly reachable objects.
 *
 * [ObjectWatcher] is thread safe.
 */
// Thread safe by locking on all methods, which is reasonably efficient given how often
// these methods are accessed.
class ObjectWatcher constructor(
  private val clock: Clock,
  private val checkRetainedExecutor: Executor,
  /**
   * Calls to [watch] will be ignored when [isEnabled] returns false
   */
  private val isEnabled: () -> Boolean = { true }
) : ReachabilityWatcher {

  private val onObjectRetainedListeners = mutableSetOf<OnObjectRetainedListener>()

  /**
   * References passed to [watch].
   */
  private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()

  private val queue = ReferenceQueue<Any>()

  /**
   * Returns true if there are watched objects that aren't weakly reachable, and
   * have been watched for long enough to be considered retained.
   */
  val hasRetainedObjects: Boolean
    @Synchronized get() {
      removeWeaklyReachableObjects()
      return watchedObjects.any { it.value.retainedUptimeMillis != -1L }
    }

  /**
   * Returns the number of retained objects, ie the number of watched objects that aren't weakly
   * reachable, and have been watched for long enough to be considered retained.
   */
  val retainedObjectCount: Int
    @Synchronized get() {
      removeWeaklyReachableObjects()
      return watchedObjects.count { it.value.retainedUptimeMillis != -1L }
    }

  /**
   * Returns true if there are watched objects that aren't weakly reachable, even
   * if they haven't been watched for long enough to be considered retained.
   */
  val hasWatchedObjects: Boolean
    @Synchronized get() {
      removeWeaklyReachableObjects()
      return watchedObjects.isNotEmpty()
    }

  /**
   * Returns the objects that are currently considered retained. Useful for logging purposes.
   * Be careful with those objects and release them ASAP as you may creating longer lived leaks
   * then the one that are already there.
   */
  val retainedObjects: List<Any>
    @Synchronized get() {
      removeWeaklyReachableObjects()
      val instances = mutableListOf<Any>()
      for (weakReference in watchedObjects.values) {
        if (weakReference.retainedUptimeMillis != -1L) {
          val instance = weakReference.get()
          if (instance != null) {
            instances.add(instance)
          }
        }
      }
      return instances
    }

  @Synchronized fun addOnObjectRetainedListener(listener: OnObjectRetainedListener) {
    onObjectRetainedListeners.add(listener)
  }

  @Synchronized fun removeOnObjectRetainedListener(listener: OnObjectRetainedListener) {
    onObjectRetainedListeners.remove(listener)
  }

  /**
   * Identical to [watch] with an empty string reference name.
   */
  @Deprecated(
    "Add description parameter explaining why an object is watched to help understand leak traces.",
    replaceWith = ReplaceWith(
      "expectWeaklyReachable(watchedObject, \"Explain why this object should be garbage collected soon\")"
    )
  )
  fun watch(watchedObject: Any) {
    expectWeaklyReachable(watchedObject, "")
  }

  @Deprecated(
    "Method renamed expectWeaklyReachable() to clarify usage.",
    replaceWith = ReplaceWith(
      "expectWeaklyReachable(watchedObject, description)"
    )
  )
  fun watch(
    watchedObject: Any,
    description: String
  ) {
    expectWeaklyReachable(watchedObject, description)
  }

  @Synchronized override fun expectWeaklyReachable(
    watchedObject: Any,
    description: String
  ) {
    if (!isEnabled()) {
      return
    }
    removeWeaklyReachableObjects()
    val key = UUID.randomUUID()
      .toString()
    val watchUptimeMillis = clock.uptimeMillis()
    val reference =
      KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
    SharkLog.d {
      "Watching " +
        (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
        (if (description.isNotEmpty()) " ($description)" else "") +
        " with key $key"
    }

    watchedObjects[key] = reference
    checkRetainedExecutor.execute {
      moveToRetained(key)
    }
  }

  /**
   * Clears all [KeyedWeakReference] that were created before [heapDumpUptimeMillis] (based on
   * [clock] [Clock.uptimeMillis])
   */
  @Synchronized fun clearObjectsWatchedBefore(heapDumpUptimeMillis: Long) {
    val weakRefsToRemove =
      watchedObjects.filter { it.value.watchUptimeMillis <= heapDumpUptimeMillis }
    weakRefsToRemove.values.forEach { it.clear() }
    watchedObjects.keys.removeAll(weakRefsToRemove.keys)
  }

  /**
   * Clears all [KeyedWeakReference]
   */
  @Synchronized fun clearWatchedObjects() {
    watchedObjects.values.forEach { it.clear() }
    watchedObjects.clear()
  }

  @Synchronized private fun moveToRetained(key: String) {
    removeWeaklyReachableObjects()
    val retainedRef = watchedObjects[key]
    if (retainedRef != null) {
      retainedRef.retainedUptimeMillis = clock.uptimeMillis()
      onObjectRetainedListeners.forEach { it.onObjectRetained() }
    }
  }

  private fun removeWeaklyReachableObjects() {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    var ref: KeyedWeakReference?
    do {
      ref = queue.poll() as KeyedWeakReference?
      if (ref != null) {
        watchedObjects.remove(ref.key)
      }
    } while (ref != null)
  }
}

package leakcanary

/**
 * Listener used by [ObjectWatcher] to report retained objects.
 *
 * This is a functional interface with which you can create a [OnObjectRetainedListener] from a lambda.
 */
fun interface OnObjectRetainedListener {

  /**
   * A watched object became retained.
   */
  fun onObjectRetained()

  companion object {
    /**
     * Utility function to create a [OnObjectRetainedListener] from the passed in [block] lambda
     * instead of using the anonymous `object : OnObjectRetainedListener` syntax.
     *
     * Usage:
     *
     * ```kotlin
     * val listener = OnObjectRetainedListener {
     *
     * }
     * ```
     */
    inline operator fun invoke(crossinline block: () -> Unit): OnObjectRetainedListener =
      object : OnObjectRetainedListener {
        override fun onObjectRetained() {
          block()
        }
      }
  }
}

package leakcanary

fun interface ReachabilityWatcher {

  /**
   * Expects the provided [watchedObject] to become weakly reachable soon. If not,
   * [watchedObject] will be considered retained.
   */
  fun expectWeaklyReachable(
    watchedObject: Any,
    description: String
  )
}

package leakcanary

import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.Context.INPUT_METHOD_SERVICE
import android.os.Build
import android.os.Build.MANUFACTURER
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.os.Process
import android.os.UserManager
import android.view.View
import android.view.Window
import android.view.accessibility.AccessibilityNodeInfo
import android.view.inputmethod.InputMethodManager
import android.view.textservice.TextServicesManager
import android.widget.TextView
import leakcanary.internal.ReferenceCleaner
import leakcanary.internal.friendly.checkMainThread
import leakcanary.internal.friendly.noOpDelegate
import shark.SharkLog
import java.lang.reflect.Array
import java.lang.reflect.Field
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.lang.reflect.Proxy
import java.util.EnumSet
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.SECONDS

/**
 * A collection of hacks to fix leaks in the Android Framework and other Google Android libraries.
 */
@SuppressLint("NewApi")
enum class AndroidLeakFixes {

  /**
   * MediaSessionLegacyHelper is a static singleton and did not use the application context.
   * Introduced in android-5.0.1_r1, fixed in Android 5.1.0_r1.
   * https://github.com/android/platform_frameworks_base/commit/
   * 9b5257c9c99c4cb541d8e8e78fb04f008b1a9091
   *
   * We fix this leak by invoking MediaSessionLegacyHelper.getHelper() early in the app lifecycle.
   */
  MEDIA_SESSION_LEGACY_HELPER {
    override fun apply(application: Application) {
      if (SDK_INT != 21) {
        return
      }
      backgroundExecutor.execute {
        try {
          val clazz = Class.forName("android.media.session.MediaSessionLegacyHelper")
          val getHelperMethod = clazz.getDeclaredMethod("getHelper", Context::class.java)
          getHelperMethod.invoke(null, application)
        } catch (ignored: Exception) {
          SharkLog.d(ignored) { "Could not fix the $name leak" }
        }
      }
    }
  },

  /**
   * This flushes the TextLine pool when an activity is destroyed, to prevent memory leaks.
   *
   * The first memory leak has been fixed in android-5.1.0_r1
   * https://github.com/android/platform_frameworks_base/commit/
   * 893d6fe48d37f71e683f722457bea646994a10bf
   *
   * Second memory leak: https://github.com/android/platform_frameworks_base/commit/
   * b3a9bc038d3a218b1dbdf7b5668e3d6c12be5ee4
   */
  TEXT_LINE_POOL {
    override fun apply(application: Application) {
      // Can't use reflection starting in SDK 28
      if (SDK_INT >= 28) {
        return
      }
      backgroundExecutor.execute {
        // Pool of TextLine instances.
        val sCached: Any?
        try {
          val textLineClass = Class.forName("android.text.TextLine")
          val sCachedField = textLineClass.getDeclaredField("sCached")
          sCachedField.isAccessible = true
          sCached = sCachedField.get(null)
          // Can't happen in current Android source, but hidden APIs can change.
          if (sCached == null || !sCached.javaClass.isArray) {
            SharkLog.d { "Could not fix the $name leak, sCached=$sCached" }
            return@execute
          }
        } catch (ignored: Exception) {
          SharkLog.d(ignored) { "Could not fix the $name leak" }
          return@execute
        }

        application.onActivityDestroyed {
          // TextLine locks on sCached. We take that lock and clear the whole array at once.
          synchronized(sCached) {
            val length = Array.getLength(sCached)
            for (i in 0 until length) {
              Array.set(sCached, i, null)
            }
          }
        }
      }
    }
  },

  /**
   * Obtaining the UserManager service ends up calling the hidden UserManager.get() method which
   * stores the context in a singleton UserManager instance and then stores that instance in a
   * static field.
   *
   * We obtain the user manager from an activity context, so if it hasn't been created yet it will
   * leak that activity forever.
   *
   * This fix makes sure the UserManager is created and holds on to the Application context.
   *
   * Issue: https://code.google.com/p/android/issues/detail?id=173789
   *
   * Fixed in https://android.googlesource.com/platform/frameworks/base/+/
   * 5200e1cb07190a1f6874d72a4561064cad3ee3e0%5E%21/#F0 (Android O)
   */
  USER_MANAGER {
    @SuppressLint("NewApi")
    override fun apply(application: Application) {
      if (SDK_INT !in 17..25) {
        return
      }
      try {
        val getMethod = UserManager::class.java.getDeclaredMethod("get", Context::class.java)
        getMethod.invoke(null, application)
      } catch (ignored: Exception) {
        SharkLog.d(ignored) { "Could not fix the $name leak" }
      }
    }
  },

  /**
   * HandlerThread instances keep local reference to their last handled message after recycling it.
   * That message is obtained by a dialog which sets on an OnClickListener on it and then never
   * recycles it, expecting it to be garbage collected but it ends up being held by the
   * HandlerThread.
   */
  FLUSH_HANDLER_THREADS {
    override fun apply(application: Application) {
      val flushedThreadIds = mutableSetOf<Int>()
      // Wait 2 seconds then look for handler threads every 3 seconds.
      backgroundExecutor.scheduleWithFixedDelay({
        val newHandlerThreadsById = findAllHandlerThreads()
          .mapNotNull { thread ->
            val threadId = thread.threadId
            if (threadId == -1 || threadId in flushedThreadIds) {
              null
            } else {
              threadId to thread
            }
          }
        flushedThreadIds += newHandlerThreadsById.map { it.first }
        newHandlerThreadsById
          .map { it.second }
          .forEach { handlerThread ->
            SharkLog.d { "Setting up flushing for $handlerThread" }
            var scheduleFlush = true
            val flushHandler = Handler(handlerThread.looper)
            flushHandler.onEachIdle {
              if (handlerThread.isAlive && scheduleFlush) {
                scheduleFlush = false
                // When the Handler thread becomes idle, we post a message to force it to move.
                // Source: https://developer.squareup.com/blog/a-small-leak-will-sink-a-great-ship/
                try {
                  val posted = flushHandler.postDelayed({
                    // Right after this postDelayed executes, the idle handler will likely be called
                    // again (if the queue is otherwise empty), so we'll need to schedule a flush
                    // again.
                    scheduleFlush = true
                  }, 1000)
                  if (!posted) {
                    SharkLog.d { "Failed to post to ${handlerThread.name}" }
                  }
                } catch (ignored: RuntimeException) {
                  // If the thread is quitting, posting to it may throw. There is no safe and atomic way
                  // to check if a thread is quitting first then post it it.
                  SharkLog.d(ignored) { "Failed to post to ${handlerThread.name}" }
                }
              }
            }
          }
      }, 2, 3, TimeUnit.SECONDS)
    }
  },

  /**
   * Until API 28, AccessibilityNodeInfo has a mOriginalText field that was not properly cleared
   * when instance were put back in the pool.
   * Leak introduced here: https://android.googlesource.com/platform/frameworks/base/+
   * /193520e3dff5248ddcf8435203bf99d2ba667219%5E%21/core/java/android/view/accessibility
   * /AccessibilityNodeInfo.java
   *
   * Fixed here: https://android.googlesource.com/platform/frameworks/base/+
   * /6f8ec1fd8c159b09d617ed6d9132658051443c0c
   */
  ACCESSIBILITY_NODE_INFO {
    override fun apply(application: Application) {
      if (SDK_INT >= 28) {
        return
      }
      // Starve the pool every 5 seconds.
      backgroundExecutor.scheduleAtFixedRate({
        val maxPoolSize = 50
        for (i in 0 until maxPoolSize) {
          AccessibilityNodeInfo.obtain()
        }
      }, 5, 5, SECONDS)
    }
  },

  /**
   * ConnectivityManager has a sInstance field that is set when the first ConnectivityManager instance is created.
   * ConnectivityManager has a mContext field.
   * When calling activity.getSystemService(Context.CONNECTIVITY_SERVICE) , the first ConnectivityManager instance
   * is created with the activity context and stored in sInstance.
   * That activity context then leaks forever.
   *
   * This fix makes sure the connectivity manager is created with the application context.
   *
   * Tracked here: https://code.google.com/p/android/issues/detail?id=198852
   * Introduced here: https://github.com/android/platform_frameworks_base/commit/e0bef71662d81caaaa0d7214fb0bef5d39996a69
   */
  CONNECTIVITY_MANAGER {
    override fun apply(application: Application) {
      if (SDK_INT > 23) {
        return
      }

      try {
        application.getSystemService(Context.CONNECTIVITY_SERVICE)
      } catch (ignored: Exception) {
        SharkLog.d(ignored) { "Could not fix the $name leak" }
      }
    }
  },

  /**
   * ClipboardUIManager is a static singleton that leaks an activity context.
   * This fix makes sure the manager is called with an application context.
   */
  SAMSUNG_CLIPBOARD_MANAGER {
    override fun apply(application: Application) {
      if (MANUFACTURER != SAMSUNG || SDK_INT !in 19..21) {
        return
      }

      try {
        val managerClass = Class.forName("android.sec.clipboard.ClipboardUIManager")
        val instanceMethod = managerClass.getDeclaredMethod("getInstance", Context::class.java)
        instanceMethod.isAccessible = true
        instanceMethod.invoke(null, application)
      } catch (ignored: Exception) {
        SharkLog.d(ignored) { "Could not fix the $name leak" }
      }
    }
  },

  /**
   * A static helper for EditText bubble popups leaks a reference to the latest focused view.
   *
   * This fix clears it when the activity is destroyed.
   */
  BUBBLE_POPUP {
    override fun apply(application: Application) {
      if (MANUFACTURER != LG || SDK_INT !in 19..21) {
        return
      }

      backgroundExecutor.execute {
        val helperField: Field
        try {
          val helperClass = Class.forName("android.widget.BubblePopupHelper")
          helperField = helperClass.getDeclaredField("sHelper")
          helperField.isAccessible = true
        } catch (ignored: Exception) {
          SharkLog.d(ignored) { "Could not fix the $name leak" }
          return@execute
        }

        application.onActivityDestroyed {
          try {
            helperField.set(null, null)
          } catch (ignored: Exception) {
            SharkLog.d(ignored) { "Could not fix the $name leak" }
          }
        }
      }
    }
  },

  /**
   * mLastHoveredView is a static field in TextView that leaks the last hovered view.
   *
   * This fix clears it when the activity is destroyed.
   */
  LAST_HOVERED_VIEW {
    override fun apply(application: Application) {
      if (MANUFACTURER != SAMSUNG || SDK_INT !in 19..21) {
        return
      }

      backgroundExecutor.execute {
        val field: Field
        try {
          field = TextView::class.java.getDeclaredField("mLastHoveredView")
          field.isAccessible = true
        } catch (ignored: Exception) {
          SharkLog.d(ignored) { "Could not fix the $name leak" }
          return@execute
        }

        application.onActivityDestroyed {
          try {
            field.set(null, null)
          } catch (ignored: Exception) {
            SharkLog.d(ignored) { "Could not fix the $name leak" }
          }
        }
      }
    }
  },

  /**
   * Samsung added a static mContext field to ActivityManager, holding a reference to the activity.
   *
   * This fix clears the field when an activity is destroyed if it refers to this specific activity.
   *
   * Observed here: https://github.com/square/leakcanary/issues/177
   */
  ACTIVITY_MANAGER {
    override fun apply(application: Application) {
      if (MANUFACTURER != SAMSUNG || SDK_INT != 22) {
        return
      }

      backgroundExecutor.execute {
        val contextField: Field
        try {
          contextField = application
            .getSystemService(Context.ACTIVITY_SERVICE)
            .javaClass
            .getDeclaredField("mContext")
          contextField.isAccessible = true
          if ((contextField.modifiers or Modifier.STATIC) != contextField.modifiers) {
            SharkLog.d { "Could not fix the $name leak, contextField=$contextField" }
            return@execute
          }
        } catch (ignored: Exception) {
          SharkLog.d(ignored) { "Could not fix the $name leak" }
          return@execute
        }

        application.onActivityDestroyed { activity ->
          try {
            if (contextField.get(null) == activity) {
              contextField.set(null, null)
            }
          } catch (ignored: Exception) {
            SharkLog.d(ignored) { "Could not fix the $name leak" }
          }
        }
      }
    }
  },

  /**
   * In Android P, ViewLocationHolder has an mRoot field that is not cleared in its clear() method.
   * Introduced in https://github.com/aosp-mirror/platform_frameworks_base/commit
   * /86b326012813f09d8f1de7d6d26c986a909d
   *
   * This leaks triggers very often when accessibility is on. To fix this leak we need to clear
   * the ViewGroup.ViewLocationHolder.sPool pool. Unfortunately Android P prevents accessing that
   * field through reflection. So instead, we call [ViewGroup#addChildrenForAccessibility] with
   * a view group that has 32 children (32 being the pool size), which as result fills in the pool
   * with 32 dumb views that reference a dummy context instead of an activity context.
   *
   * This fix empties the pool on every activity destroy and every AndroidX fragment view destroy.
   * You can support other cases where views get detached by calling directly
   * [ViewLocationHolderLeakFix.clearStaticPool].
   */
  VIEW_LOCATION_HOLDER {
    override fun apply(application: Application) {
      ViewLocationHolderLeakFix.applyFix(application)
    }
  },

  /**
   * Fix for https://code.google.com/p/android/issues/detail?id=171190 .
   *
   * When a view that has focus gets detached, we wait for the main thread to be idle and then
   * check if the InputMethodManager is leaking a view. If yes, we tell it that the decor view got
   * focus, which is what happens if you press home and come back from recent apps. This replaces
   * the reference to the detached view with a reference to the decor view.
   */
  IMM_FOCUSED_VIEW {
    // mServedView should not be accessed on API 29+. Make this clear to Lint with the
    // TargetApi annotation.
    @TargetApi(23)
    @SuppressLint("PrivateApi")
    override fun apply(application: Application) {
      // Fixed in API 24.
      if (SDK_INT > 23) {
        return
      }
      val inputMethodManager =
        application.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
      val mServedViewField: Field
      val mHField: Field
      val finishInputLockedMethod: Method
      val focusInMethod: Method
      try {
        mServedViewField =
          InputMethodManager::class.java.getDeclaredField("mServedView")
        mServedViewField.isAccessible = true
        mHField = InputMethodManager::class.java.getDeclaredField("mH")
        mHField.isAccessible = true
        finishInputLockedMethod =
          InputMethodManager::class.java.getDeclaredMethod("finishInputLocked")
        finishInputLockedMethod.isAccessible = true
        focusInMethod = InputMethodManager::class.java.getDeclaredMethod(
          "focusIn", View::class.java
        )
        focusInMethod.isAccessible = true
      } catch (ignored: Exception) {
        SharkLog.d(ignored) { "Could not fix the $name leak" }
        return
      }
      application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks
      by noOpDelegate() {
        override fun onActivityCreated(
          activity: Activity,
          savedInstanceState: Bundle?
        ) {
          activity.window.onDecorViewReady {
            val cleaner = ReferenceCleaner(
              inputMethodManager,
              mHField,
              mServedViewField,
              finishInputLockedMethod
            )
            val rootView = activity.window.decorView.rootView
            val viewTreeObserver = rootView.viewTreeObserver
            viewTreeObserver.addOnGlobalFocusChangeListener(cleaner)
          }
        }
      })
    }
  },

  /**
   * When an activity is destroyed, the corresponding ViewRootImpl instance is released and ready to
   * be garbage collected.
   * Some time after that, ViewRootImpl#W receives a windowfocusChanged() callback, which it
   * normally delegates to ViewRootImpl which in turn calls
   * InputMethodManager#onPreWindowFocus which clears InputMethodManager#mCurRootView.
   *
   * Unfortunately, since the ViewRootImpl instance is garbage collectable it may be garbage
   * collected before that happens.
   * ViewRootImpl#W has a weak reference on ViewRootImpl, so that weak reference will then return
   * null and the windowfocusChanged() callback will be ignored, leading to
   * InputMethodManager#mCurRootView not being cleared.
   *
   * Filed here: https://issuetracker.google.com/u/0/issues/116078227
   * Fixed here: https://android.googlesource.com/platform/frameworks/base/+/dff365ef4dc61239fac70953b631e92972a9f41f%5E%21/#F0
   * InputMethodManager.mCurRootView is part of the unrestricted grey list on Android 9:
   * https://android.googlesource.com/platform/frameworks/base/+/pie-release/config/hiddenapi-light-greylist.txt#6057
   */
  IMM_CUR_ROOT_VIEW {
    override fun apply(application: Application) {
      if (Build.VERSION.SDK_INT >= 29) {
        return
      }
      val inputMethodManager =
        application.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
      val mCurRootViewField: Field
      try {
        mCurRootViewField =
          InputMethodManager::class.java.getDeclaredField("mCurRootView")
        mCurRootViewField.isAccessible = true
      } catch (ignored: Exception) {
        SharkLog.d(ignored) { "Could not read InputMethodManager.mCurRootView field" }
        return
      }
      application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks
      by noOpDelegate() {
        override fun onActivityDestroyed(activity: Activity) {
          try {
            val rootView = mCurRootViewField[inputMethodManager] as View?
            if (rootView != null && activity.window != null && activity.window.decorView === rootView
            ) {
              mCurRootViewField[inputMethodManager] = null
            }
          } catch (ignored: Exception) {
            SharkLog.d(ignored) { "Could not update InputMethodManager.mCurRootView field" }
          }
        }
      })
    }
  },

  /**
   * Every editable TextView has an Editor instance which has a SpellChecker instance. SpellChecker
   * is in charge of displaying the little squiggle spans that show typos. SpellChecker starts a
   * SpellCheckerSession as needed and then closes it when the TextView is detached from the window.
   * A SpellCheckerSession is in charge of communicating with the spell checker service (which lives
   * in another process) through TextServicesManager.
   *
   * The SpellChecker sends the TextView content to the spell checker service every 400ms, ie every
   * time the service calls back with a result the SpellChecker schedules another check for 400ms
   * later.
   *
   * When the TextView is detached from the window, the spell checker closes the session. In practice,
   * SpellCheckerSessionListenerImpl.mHandler is set to null and when the service calls
   * SpellCheckerSessionListenerImpl.onGetSuggestions or
   * SpellCheckerSessionListenerImpl.onGetSentenceSuggestions back from another process, there's a
   * null check for SpellCheckerSessionListenerImpl.mHandler and the callback is dropped.
   *
   * Unfortunately, on Android M there's a race condition in how that's done. When the service calls
   * back into our app process, the IPC call is received on a binder thread. That's when the null
   * check happens. If the session is not closed at this point (mHandler not null), the callback is
   * then posted to the main thread. If on the main thread the session is closed after that post but
   * prior to that post being handled, then the post will still be processed, after the session has
   * been closed.
   *
   * When the post is processed, SpellCheckerSession calls back into SpellChecker which in turns
   * schedules a new spell check to be ran in 400ms. The check is an anonymous inner class
   * (SpellChecker$1) stored as SpellChecker.mSpellRunnable and implementing Runnable. It is scheduled
   * by calling [View.postDelayed]. As we've seen, at this point the session may be closed which means
   * that the view has been detached. [View.postDelayed] behaves differently when a view is detached:
   * instead of posting to the single [Handler] used by the view hierarchy, it enqueues the Runnable
   * into ViewRootImpl.RunQueue, a static queue that holds on to "actions" to be executed. As soon as
   * a view hierarchy is attached, the ViewRootImpl.RunQueue is processed and emptied.
   *
   * Unfortunately, that means that as long as no view hierarchy is attached, ie as long as there
   * are no activities alive, the actions stay in ViewRootImpl.RunQueue. That means SpellChecker$1
   * ends up being kept in memory. It holds on to SpellChecker which in turns holds on
   * to the detached TextView and corresponding destroyed activity & view hierarchy.
   *
   * We have a fix for this! When the spell check session is closed, we replace
   * SpellCheckerSession.mSpellCheckerSessionListener (which normally is the SpellChecker) with a
   * no-op implementation. So even if callbacks are enqueued to the main thread handler, these
   * callbacks will call the no-op implementation and SpellChecker will not be scheduling a spell
   * check.
   *
   * Sources to corroborate:
   *
   * https://android.googlesource.com/platform/frameworks/base/+/marshmallow-release
   * /core/java/android/view/textservice/SpellCheckerSession.java
   * /core/java/android/view/textservice/TextServicesManager.java
   * /core/java/android/widget/SpellChecker.java
   * /core/java/android/view/ViewRootImpl.java
   */
  SPELL_CHECKER {
    @TargetApi(23)
    @SuppressLint("PrivateApi")
    override fun apply(application: Application) {
      if (SDK_INT != 23) {
        return
      }

      try {
        val textServiceClass = TextServicesManager::class.java
        val getInstanceMethod = textServiceClass.getDeclaredMethod("getInstance")

        val sServiceField = textServiceClass.getDeclaredField("sService")
        sServiceField.isAccessible = true

        val serviceStubInterface =
          Class.forName("com.android.internal.textservice.ITextServicesManager")

        val spellCheckSessionClass = Class.forName("android.view.textservice.SpellCheckerSession")
        val mSpellCheckerSessionListenerField =
          spellCheckSessionClass.getDeclaredField("mSpellCheckerSessionListener")
        mSpellCheckerSessionListenerField.isAccessible = true

        val spellCheckerSessionListenerImplClass =
          Class.forName(
            "android.view.textservice.SpellCheckerSession\$SpellCheckerSessionListenerImpl"
          )
        val listenerImplHandlerField =
          spellCheckerSessionListenerImplClass.getDeclaredField("mHandler")
        listenerImplHandlerField.isAccessible = true

        val spellCheckSessionHandlerClass =
          Class.forName("android.view.textservice.SpellCheckerSession\$1")
        val outerInstanceField = spellCheckSessionHandlerClass.getDeclaredField("this$0")
        outerInstanceField.isAccessible = true

        val listenerInterface =
          Class.forName("android.view.textservice.SpellCheckerSession\$SpellCheckerSessionListener")
        val noOpListener = Proxy.newProxyInstance(
          listenerInterface.classLoader, arrayOf(listenerInterface)
        ) { _: Any, _: Method, _: kotlin.Array<Any>? ->
          SharkLog.d { "Received call to no-op SpellCheckerSessionListener after session closed" }
        }

        // Ensure a TextServicesManager instance is created and TextServicesManager.sService set.
        getInstanceMethod
          .invoke(null)
        val realService = sServiceField[null]!!

        val spellCheckerListenerToSession = mutableMapOf<Any, Any>()

        val proxyService = Proxy.newProxyInstance(
          serviceStubInterface.classLoader, arrayOf(serviceStubInterface)
        ) { _: Any, method: Method, args: kotlin.Array<Any>? ->
          try {
            if (method.name == "getSpellCheckerService") {
              // getSpellCheckerService is called when the session is opened, which allows us to
              // capture the corresponding SpellCheckerSession instance via
              // SpellCheckerSessionListenerImpl.mHandler.this$0
              val spellCheckerSessionListener = args!![3]
              val handler = listenerImplHandlerField[spellCheckerSessionListener]!!
              val spellCheckerSession = outerInstanceField[handler]!!
              // We add to a map of SpellCheckerSessionListenerImpl to SpellCheckerSession
              spellCheckerListenerToSession[spellCheckerSessionListener] = spellCheckerSession
            } else if (method.name == "finishSpellCheckerService") {
              // finishSpellCheckerService is called when the session is open. After the session has been
              // closed, any pending work posted to SpellCheckerSession.mHandler should be ignored. We do
              // so by replacing mSpellCheckerSessionListener with a no-op implementation.
              val spellCheckerSessionListener = args!![0]
              val spellCheckerSession =
                spellCheckerListenerToSession.remove(spellCheckerSessionListener)!!
              // We use the SpellCheckerSessionListenerImpl to find the corresponding SpellCheckerSession
              // At this point in time the session was just closed to
              // SpellCheckerSessionListenerImpl.mHandler is null, which is why we had to capture
              // the SpellCheckerSession during the getSpellCheckerService call.
              mSpellCheckerSessionListenerField[spellCheckerSession] = noOpListener
            }
          } catch (ignored: Exception) {
            SharkLog.d(ignored) { "Unable to fix SpellChecker leak" }
          }
          // Standard delegation
          try {
            return@newProxyInstance if (args != null) {
              method.invoke(realService, *args)
            } else {
              method.invoke(realService)
            }
          } catch (invocationException: InvocationTargetException) {
            throw invocationException.targetException
          }
        }
        sServiceField[null] = proxyService
      } catch (ignored: Exception) {
        SharkLog.d(ignored) { "Unable to fix SpellChecker leak" }
      }
    }
  }

  ;

  protected abstract fun apply(application: Application)

  private var applied = false

  companion object {

    private const val SAMSUNG = "samsung"
    private const val LG = "LGE"

    fun applyFixes(
      application: Application,
      fixes: Set<AndroidLeakFixes> = EnumSet.allOf(AndroidLeakFixes::class.java)
    ) {
      checkMainThread()
      fixes.forEach { fix ->
        if (!fix.applied) {
          fix.apply(application)
          fix.applied = true
        } else {
          SharkLog.d { "${fix.name} leak fix already applied." }
        }
      }
    }

    private val backgroundExecutor =
      // Single thread => avoid dealing with concurrency (aside from background vs main thread)
      Executors.newSingleThreadScheduledExecutor { runnable ->
        val thread = object : Thread() {
          override fun run() {
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
            runnable.run()
          }
        }
        thread.name = "plumber-android-leaks"
        thread
      }

    private fun Handler.onEachIdle(onIdle: () -> Unit) {
      try {
        // Unfortunately Looper.getQueue() is API 23. Looper.myQueue() is API 1.
        // So we have to post to the handler thread to be able to obtain the queue for that
        // thread from within that thread.
        post {
          Looper
            .myQueue()
            .addIdleHandler {
              onIdle()
              true
            }
        }
      } catch (ignored: RuntimeException) {
        // If the thread is quitting, posting to it will throw. There is no safe and atomic way
        // to check if a thread is quitting first then post it it.
      }
    }

    private fun findAllHandlerThreads(): List<HandlerThread> {
      // Based on https://stackoverflow.com/a/1323480
      var rootGroup = Thread.currentThread().threadGroup!!
      while (rootGroup.parent != null) rootGroup = rootGroup.parent
      var threads = arrayOfNulls<Thread>(rootGroup.activeCount())
      while (rootGroup.enumerate(threads, true) == threads.size) {
        threads = arrayOfNulls(threads.size * 2)
      }
      return threads.mapNotNull { if (it is HandlerThread) it else null }
    }

    internal fun Application.onActivityDestroyed(block: (Activity) -> Unit) {
      registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks
      by noOpDelegate() {
        override fun onActivityDestroyed(activity: Activity) {
          block(activity)
        }
      })
    }

    private fun Window.onDecorViewReady(callback: () -> Unit) {
      if (peekDecorView() == null) {
        onContentChanged {
          callback()
          return@onContentChanged false
        }
      } else {
        callback()
      }
    }

    private fun Window.onContentChanged(block: () -> Boolean) {
      val callback = wrapCallback()
      callback.onContentChangedCallbacks += block
    }

    private fun Window.wrapCallback(): WindowDelegateCallback {
      val currentCallback = callback
      return if (currentCallback is WindowDelegateCallback) {
        currentCallback
      } else {
        val newCallback = WindowDelegateCallback(currentCallback)
        callback = newCallback
        newCallback
      }
    }

    private class WindowDelegateCallback constructor(
      private val delegate: Window.Callback
    ) : Window.Callback by delegate {

      val onContentChangedCallbacks = mutableListOf<() -> Boolean>()

      override fun onContentChanged() {
        onContentChangedCallbacks.removeAll { callback ->
          !callback()
        }
        delegate.onContentChanged()
      }
    }
  }
}

package leakcanary

import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.os.Build.VERSION
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import leakcanary.internal.friendly.checkMainThread
import leakcanary.internal.onAndroidXFragmentViewDestroyed
import shark.SharkLog
import leakcanary.internal.friendly.noOpDelegate

/**
 * @see [AndroidLeakFixes.VIEW_LOCATION_HOLDER].
 */
@SuppressLint("NewApi")
object ViewLocationHolderLeakFix {

  private var groupAndOutChildren: Pair<ViewGroup, ArrayList<View>>? = null
  private var failedClearing = false

  internal fun applyFix(application: Application) {
    if (VERSION.SDK_INT != 28) {
      return
    }
    application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks
    by noOpDelegate() {

      override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
      ) {
        activity.onAndroidXFragmentViewDestroyed {
          uncheckedClearStaticPool(application)
        }
      }

      override fun onActivityDestroyed(activity: Activity) {
        uncheckedClearStaticPool(application)
      }
    })
  }

  /**
   * Clears the ViewGroup.ViewLocationHolder.sPool static pool.
   */
  fun clearStaticPool(application: Application) {
    checkMainThread()
    if (VERSION.SDK_INT != 28) {
      return
    }
    uncheckedClearStaticPool(application)
  }

  private fun uncheckedClearStaticPool(application: Application) {
    if (failedClearing) {
      return
    }
    try {
      if (groupAndOutChildren == null) {
        val viewGroup = FrameLayout(application)
        // ViewLocationHolder.MAX_POOL_SIZE = 32
        for (i in 0 until 32) {
          val childView = View(application)
          viewGroup.addView(childView)
        }
        groupAndOutChildren = viewGroup to ArrayList()
      }
      val (group, outChildren) = groupAndOutChildren!!
      group.addChildrenForAccessibility(outChildren)
    } catch (ignored: Throwable) {
      SharkLog.d(ignored) {
        "Failed to clear ViewLocationHolder leak, will not try again."
      }
      failedClearing = true
    }
  }
}

package shark

/**
 * Caches values from the android.os.Build class in the heap dump.
 * Retrieve a cached instances via [fromHeapGraph].
 */
class AndroidBuildMirror(
  /**
   * Value of android.os.Build.MANUFACTURER
   */
  val manufacturer: String,
  /**
   * Value of android.os.Build.VERSION.SDK_INT
   */
  val sdkInt: Int
) {
  companion object {
    /**
     * @see AndroidBuildMirror
     */
    fun fromHeapGraph(graph: HeapGraph): AndroidBuildMirror {
      return graph.context.getOrPut(AndroidBuildMirror::class.java.name) {
        val buildClass = graph.findClassByName("android.os.Build")!!
        val versionClass = graph.findClassByName("android.os.Build\$VERSION")!!
        val manufacturer = buildClass["MANUFACTURER"]!!.value.readAsJavaString()!!
        val sdkInt = versionClass["SDK_INT"]!!.value.asInt!!
        AndroidBuildMirror(manufacturer, sdkInt)
      }
    }
  }
}

package shark

object AndroidMetadataExtractor : MetadataExtractor {
  override fun extractMetadata(graph: HeapGraph): Map<String, String> {
    val build = AndroidBuildMirror.fromHeapGraph(graph)

    val leakCanaryVersion = readLeakCanaryVersion(graph)
    val processName = readProcessName(graph)

    return mapOf(
      "Build.VERSION.SDK_INT" to build.sdkInt.toString(),
      "Build.MANUFACTURER" to build.manufacturer,
      "LeakCanary version" to leakCanaryVersion,
      "App process name" to processName
    )
  }

  private fun readLeakCanaryVersion(graph: HeapGraph): String {
    val versionHolderClass = graph.findClassByName("leakcanary.internal.InternalLeakCanary")
    return versionHolderClass?.get("version")?.value?.readAsJavaString() ?: "Unknown"
  }

  private fun readProcessName(graph: HeapGraph): String {
    val activityThread = graph.findClassByName("android.app.ActivityThread")
      ?.get("sCurrentActivityThread")
      ?.valueAsInstance
    val appBindData = activityThread?.get("android.app.ActivityThread", "mBoundApplication")
      ?.valueAsInstance
    val appInfo = appBindData?.get("android.app.ActivityThread\$AppBindData", "appInfo")
      ?.valueAsInstance

    return appInfo?.get(
      "android.content.pm.ApplicationInfo", "processName"
    )?.valueAsInstance?.readAsJavaString() ?: "Unknown"
  }
}

/*
 * Copyright (C) 2018 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package shark

import shark.AndroidServices.aliveAndroidServiceObjectIds
import shark.AndroidObjectInspectors.Companion.appDefaults
import shark.FilteringLeakingObjectFinder.LeakingObjectFilter
import shark.HeapObject.HeapInstance
import java.util.EnumSet
import kotlin.math.absoluteValue

/**
 * A set of default [ObjectInspector]s that knows about common AOSP and library
 * classes.
 *
 * These are heuristics based on our experience and knowledge of AOSP and various library
 * internals. We only make a decision if we're reasonably sure the state of an object is
 * unlikely to be the result of a programmer mistake.
 *
 * For example, no matter how many mistakes we make in our code, the value of Activity.mDestroy
 * will not be influenced by those mistakes.
 *
 * Most developers should use the entire set of default [ObjectInspector] by calling [appDefaults],
 * unless there's a bug and you temporarily want to remove an inspector.
 */
enum class AndroidObjectInspectors : ObjectInspector {

  VIEW {
    override val leakingObjectFilter = { heapObject: HeapObject ->
      if (heapObject is HeapInstance && heapObject instanceOf "android.view.View") {
        // Leaking if null parent or non view parent.
        val viewParent = heapObject["android.view.View", "mParent"]!!.valueAsInstance
        val isParentlessView = viewParent == null
        val isChildOfViewRootImpl =
          viewParent != null && !(viewParent instanceOf "android.view.View")
        val isRootView = isParentlessView || isChildOfViewRootImpl

        // This filter only cares for root view because we only need one view in a view hierarchy.
        if (isRootView) {
          val mContext = heapObject["android.view.View", "mContext"]!!.value.asObject!!.asInstance!!
          val activityContext = mContext.unwrapActivityContext()
          val mContextIsDestroyedActivity = (activityContext != null &&
            activityContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true)
          if (mContextIsDestroyedActivity) {
            // Root view with unwrapped mContext a destroyed activity.
            true
          } else {
            val viewDetached =
              heapObject["android.view.View", "mAttachInfo"]!!.value.isNullReference
            if (viewDetached) {
              val mWindowAttachCount =
                heapObject["android.view.View", "mWindowAttachCount"]?.value!!.asInt!!
              if (mWindowAttachCount > 0) {
                when {
                  isChildOfViewRootImpl -> {
                    // Child of ViewRootImpl that was once attached and is now detached.
                    // Unwrapped mContext not a destroyed activity. This could be a dialog root.
                    true
                  }
                  heapObject.instanceClassName == "com.android.internal.policy.DecorView" -> {
                    // DecorView with null parent, once attached now detached.
                    // Unwrapped mContext not a destroyed activity. This could be a dialog root.
                    // Unlikely to be a reusable cached view => leak.
                    true
                  }
                  else -> {
                    // View with null parent, once attached now detached.
                    // Unwrapped mContext not a destroyed activity. This could be a dialog root.
                    // Could be a leak or could be a reusable cached view.
                    false
                  }
                }
              } else {
                // Root view, detached but was never attached.
                // This could be a cached instance.
                false
              }
            } else {
              // Root view that is attached.
              false
            }
          }
        } else {
          // Not a root view.
          false
        }
      } else {
        // Not a view
        false
      }
    }

    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("android.view.View") { instance ->
        // This skips edge cases like Toast$TN.mNextView holding on to an unattached and unparented
        // next toast view
        var rootParent = instance["android.view.View", "mParent"]!!.valueAsInstance
        var rootView: HeapInstance? = null
        while (rootParent != null && rootParent instanceOf "android.view.View") {
          rootView = rootParent
          rootParent = rootParent["android.view.View", "mParent"]!!.valueAsInstance
        }

        val partOfWindowHierarchy = rootParent != null || (rootView != null &&
          rootView.instanceClassName == "com.android.internal.policy.DecorView")

        val mWindowAttachCount =
          instance["android.view.View", "mWindowAttachCount"]?.value!!.asInt!!
        val viewDetached = instance["android.view.View", "mAttachInfo"]!!.value.isNullReference
        val mContext = instance["android.view.View", "mContext"]!!.value.asObject!!.asInstance!!

        val activityContext = mContext.unwrapActivityContext()
        if (activityContext != null && activityContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true) {
          leakingReasons += "View.mContext references a destroyed activity"
        } else {
          if (partOfWindowHierarchy && mWindowAttachCount > 0) {
            if (viewDetached) {
              leakingReasons += "View detached yet still part of window view hierarchy"
            } else {
              if (rootView != null && rootView["android.view.View", "mAttachInfo"]!!.value.isNullReference) {
                leakingReasons += "View attached but root view ${rootView.instanceClassName} detached (attach disorder)"
              } else {
                notLeakingReasons += "View attached"
              }
            }
          }
        }

        labels += if (partOfWindowHierarchy) {
          "View is part of a window view hierarchy"
        } else {
          "View not part of a window view hierarchy"
        }

        labels += if (viewDetached) {
          "View.mAttachInfo is null (view detached)"
        } else {
          "View.mAttachInfo is not null (view attached)"
        }

        AndroidResourceIdNames.readFromHeap(instance.graph)
          ?.let { resIds ->
            val mID = instance["android.view.View", "mID"]!!.value.asInt!!
            val noViewId = -1
            if (mID != noViewId) {
              val resourceName = resIds[mID]
              labels += "View.mID = R.id.$resourceName"
            }
          }
        labels += "View.mWindowAttachCount = $mWindowAttachCount"
      }
    }
  },

  EDITOR {
    override val leakingObjectFilter = { heapObject: HeapObject ->
      heapObject is HeapInstance &&
        heapObject instanceOf "android.widget.Editor" &&
        heapObject["android.widget.Editor", "mTextView"]?.value?.asObject?.let { textView ->
          VIEW.leakingObjectFilter!!(textView)
        } ?: false
    }

    override fun inspect(reporter: ObjectReporter) {
      reporter.whenInstanceOf("android.widget.Editor") { instance ->
        applyFromField(VIEW, instance["android.widget.Editor", "mTextView"])
      }
    }
  },

  ACTIVITY {
    override val leakingObjectFilter = { heapObject: HeapObject ->
      heapObject is HeapInstance &&
        heapObject instanceOf "android.app.Activity" &&
        heapObject["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true
    }

    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("android.app.Activity") { instance ->
        // Activity.mDestroyed was introduced in 17.
        // https://android.googlesource.com/platform/frameworks/base/+
        // /6d9dcbccec126d9b87ab6587e686e28b87e5a04d
        val field = instance["android.app.Activity", "mDestroyed"]

        if (field != null) {
          if (field.value.asBoolean!!) {
            leakingReasons += field describedWithValue "true"
          } else {
            notLeakingReasons += field describedWithValue "false"
          }
        }
      }
    }
  },

  SERVICE {
    override val leakingObjectFilter = { heapObject: HeapObject ->
      heapObject is HeapInstance &&
        heapObject instanceOf "android.app.Service" &&
        heapObject.objectId !in heapObject.graph.aliveAndroidServiceObjectIds
    }

    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("android.app.Service") { instance ->
        if (instance.objectId in instance.graph.aliveAndroidServiceObjectIds) {
          notLeakingReasons += "Service held by ActivityThread"
        } else {
          leakingReasons +=  "Service not held by ActivityThread"
        }
      }
    }
  },

  CONTEXT_FIELD {
    override fun inspect(reporter: ObjectReporter) {
      val instance = reporter.heapObject
      if (instance !is HeapInstance) {
        return
      }
      instance.readFields().forEach { field ->
        val fieldInstance = field.valueAsInstance
        if (fieldInstance != null && fieldInstance instanceOf "android.content.Context") {
          reporter.run {
            val componentContext = fieldInstance.unwrapComponentContext()
            labels += if (componentContext == null) {
              "${field.name} instance of ${fieldInstance.instanceClassName}"
            } else if (componentContext instanceOf "android.app.Activity") {
              val activityDescription =
                "with mDestroyed = " + (componentContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean?.toString()
                  ?: "UNKNOWN")
              if (componentContext == fieldInstance) {
                "${field.name} instance of ${fieldInstance.instanceClassName} $activityDescription"
              } else {
                "${field.name} instance of ${fieldInstance.instanceClassName}, " +
                  "wrapping activity ${componentContext.instanceClassName} $activityDescription"
              }
            } else if (componentContext == fieldInstance) {
              // No need to add "instance of Application / Service", devs know their own classes.
              "${field.name} instance of ${fieldInstance.instanceClassName}"
            } else {
              "${field.name} instance of ${fieldInstance.instanceClassName}, wrapping ${componentContext.instanceClassName}"
            }
          }
        }
      }
    }
  },

  CONTEXT_WRAPPER {

    override val leakingObjectFilter = { heapObject: HeapObject ->
      heapObject is HeapInstance &&
        heapObject.unwrapActivityContext()
          ?.get("android.app.Activity", "mDestroyed")?.value?.asBoolean == true
    }

    override fun inspect(
      reporter: ObjectReporter
    ) {
      val instance = reporter.heapObject
      if (instance !is HeapInstance) {
        return
      }

      // We're looking for ContextWrapper instances that are not Activity, Application or Service.
      // So we stop whenever we find any of those 4 classes, and then only keep ContextWrapper.
      val matchingClassName = instance.instanceClass.classHierarchy.map { it.name }
        .firstOrNull {
          when (it) {
            "android.content.ContextWrapper",
            "android.app.Activity",
            "android.app.Application",
            "android.app.Service"
            -> true
            else -> false
          }
        }

      if (matchingClassName == "android.content.ContextWrapper") {
        reporter.run {
          val componentContext = instance.unwrapComponentContext()
          if (componentContext != null) {
            if (componentContext instanceOf "android.app.Activity") {
              val mDestroyed = componentContext["android.app.Activity", "mDestroyed"]
              if (mDestroyed != null) {
                if (mDestroyed.value.asBoolean!!) {
                  leakingReasons += "${instance.instanceClassSimpleName} wraps an Activity with Activity.mDestroyed true"
                } else {
                  // We can't assume it's not leaking, because this context might have a shorter lifecycle
                  // than the activity. So we'll just add a label.
                  labels += "${instance.instanceClassSimpleName} wraps an Activity with Activity.mDestroyed false"
                }
              }
            } else if (componentContext instanceOf "android.app.Application") {
              labels += "${instance.instanceClassSimpleName} wraps an Application context"
            } else {
              labels += "${instance.instanceClassSimpleName} wraps a Service context"
            }
          } else {
            labels += "${instance.instanceClassSimpleName} does not wrap a known Android context"
          }
        }
      }
    }
  },

  APPLICATION_PACKAGE_MANAGER {
    override val leakingObjectFilter = { heapObject: HeapObject ->
      heapObject is HeapInstance &&
        heapObject instanceOf "android.app.ApplicationContextManager" &&
        heapObject["android.app.ApplicationContextManager", "mContext"]!!
          .valueAsInstance!!.outerContextIsLeaking()
    }

    override fun inspect(reporter: ObjectReporter) {
      reporter.whenInstanceOf("android.app.ApplicationContextManager") { instance ->
        val outerContext = instance["android.app.ApplicationContextManager", "mContext"]!!
          .valueAsInstance!!["android.app.ContextImpl", "mOuterContext"]!!
          .valueAsInstance!!
        inspectContextImplOuterContext(outerContext, instance, "ApplicationContextManager.mContext")
      }
    }
  },

  CONTEXT_IMPL {
    override val leakingObjectFilter = { heapObject: HeapObject ->
      heapObject is HeapInstance &&
        heapObject instanceOf "android.app.ContextImpl" &&
        heapObject.outerContextIsLeaking()
    }

    override fun inspect(reporter: ObjectReporter) {
      reporter.whenInstanceOf("android.app.ContextImpl") { instance ->
        val outerContext = instance["android.app.ContextImpl", "mOuterContext"]!!
          .valueAsInstance!!
        inspectContextImplOuterContext(outerContext, instance)
      }
    }
  },

  DIALOG {
    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("android.app.Dialog") { instance ->
        val mDecor = instance["android.app.Dialog", "mDecor"]!!
        // Can't infer leaking status: mDecor null means either never shown or dismiss.
        // mDecor non null means the dialog is showing, but sometimes dialogs stay showing
        // after activity destroyed so that's not really a non leak either.
        labels += mDecor describedWithValue if (mDecor.value.isNullReference) {
          "null"
        } else {
          "not null"
        }
      }
    }
  },

  APPLICATION {
    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("android.app.Application") {
        notLeakingReasons += "Application is a singleton"
      }
    }
  },

  INPUT_METHOD_MANAGER {
    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("android.view.inputmethod.InputMethodManager") {
        notLeakingReasons += "InputMethodManager is a singleton"
      }
    }
  },

  FRAGMENT {
    override val leakingObjectFilter = { heapObject: HeapObject ->
      heapObject is HeapInstance &&
        heapObject instanceOf "android.app.Fragment" &&
        heapObject["android.app.Fragment", "mFragmentManager"]!!.value.isNullReference
    }

    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("android.app.Fragment") { instance ->
        val fragmentManager = instance["android.app.Fragment", "mFragmentManager"]!!
        if (fragmentManager.value.isNullReference) {
          leakingReasons += fragmentManager describedWithValue "null"
        } else {
          notLeakingReasons += fragmentManager describedWithValue "not null"
        }
        val mTag = instance["android.app.Fragment", "mTag"]?.value?.readAsJavaString()
        if (!mTag.isNullOrEmpty()) {
          labels += "Fragment.mTag=$mTag"
        }
      }
    }
  },

  SUPPORT_FRAGMENT {

    override val leakingObjectFilter = { heapObject: HeapObject ->
      heapObject is HeapInstance &&
        heapObject instanceOf ANDROID_SUPPORT_FRAGMENT_CLASS_NAME &&
        heapObject.getOrThrow(
          ANDROID_SUPPORT_FRAGMENT_CLASS_NAME, "mFragmentManager"
        ).value.isNullReference
    }

    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf(ANDROID_SUPPORT_FRAGMENT_CLASS_NAME) { instance ->
        val fragmentManager =
          instance.getOrThrow(ANDROID_SUPPORT_FRAGMENT_CLASS_NAME, "mFragmentManager")
        if (fragmentManager.value.isNullReference) {
          leakingReasons += fragmentManager describedWithValue "null"
        } else {
          notLeakingReasons += fragmentManager describedWithValue "not null"
        }
        val mTag = instance[ANDROID_SUPPORT_FRAGMENT_CLASS_NAME, "mTag"]?.value?.readAsJavaString()
        if (!mTag.isNullOrEmpty()) {
          labels += "Fragment.mTag=$mTag"
        }
      }
    }
  },

  ANDROIDX_FRAGMENT {
    override val leakingObjectFilter = { heapObject: HeapObject ->
      heapObject is HeapInstance &&
        heapObject instanceOf "androidx.fragment.app.Fragment" &&
        heapObject.getOrThrow(
          "androidx.fragment.app.Fragment", "mFragmentManager"
        ).value.isNullReference
    }

    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("androidx.fragment.app.Fragment") { instance ->
        val fragmentManager =
          instance.getOrThrow("androidx.fragment.app.Fragment", "mFragmentManager")
        if (fragmentManager.value.isNullReference) {
          leakingReasons += fragmentManager describedWithValue "null"
        } else {
          notLeakingReasons += fragmentManager describedWithValue "not null"
        }
        val mTag = instance["androidx.fragment.app.Fragment", "mTag"]?.value?.readAsJavaString()
        if (!mTag.isNullOrEmpty()) {
          labels += "Fragment.mTag=$mTag"
        }
      }
    }
  },

  MESSAGE_QUEUE {
    override val leakingObjectFilter = { heapObject: HeapObject ->
      heapObject is HeapInstance &&
        heapObject instanceOf "android.os.MessageQueue" &&
        (heapObject["android.os.MessageQueue", "mQuitting"]
          ?: heapObject["android.os.MessageQueue", "mQuiting"]!!).value.asBoolean!!
    }

    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("android.os.MessageQueue") { instance ->
        // mQuiting had a typo and was renamed to mQuitting
        // https://android.googlesource.com/platform/frameworks/base/+/013cf847bcfd2828d34dced60adf2d3dd98021dc
        val mQuitting = instance["android.os.MessageQueue", "mQuitting"]
          ?: instance["android.os.MessageQueue", "mQuiting"]!!
        if (mQuitting.value.asBoolean!!) {
          leakingReasons += mQuitting describedWithValue "true"
        } else {
          notLeakingReasons += mQuitting describedWithValue "false"
        }

        val queueHead = instance["android.os.MessageQueue", "mMessages"]!!.valueAsInstance
        if (queueHead != null) {
          val targetHandler = queueHead["android.os.Message", "target"]!!.valueAsInstance
          if (targetHandler != null) {
            val looper = targetHandler["android.os.Handler", "mLooper"]!!.valueAsInstance
            if (looper != null) {
              val thread = looper["android.os.Looper", "mThread"]!!.valueAsInstance!!
              val threadName = thread[Thread::class, "name"]!!.value.readAsJavaString()
              labels += "HandlerThread: \"$threadName\""
            }
          }
        }
      }
    }
  },

  LOADED_APK {
    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("android.app.LoadedApk") { instance ->
        val receiversMap = instance["android.app.LoadedApk", "mReceivers"]!!.valueAsInstance!!
        val receiversArray = receiversMap["android.util.ArrayMap", "mArray"]!!.valueAsObjectArray!!
        val receiverContextList = receiversArray.readElements().toList()

        val allReceivers = (receiverContextList.indices step 2).map { index ->
          val context = receiverContextList[index]
          if (context.isNonNullReference) {
            val contextReceiversMap = receiverContextList[index + 1].asObject!!.asInstance!!
            val contextReceivers = contextReceiversMap["android.util.ArrayMap", "mArray"]!!
              .valueAsObjectArray!!
              .readElements()
              .toList()

            val receivers =
              (contextReceivers.indices step 2).mapNotNull { contextReceivers[it].asObject?.asInstance }
            val contextInstance = context.asObject!!.asInstance!!
            val contextString =
              "${contextInstance.instanceClassSimpleName}@${contextInstance.objectId}"
            contextString to receivers.map {  "${it.instanceClassSimpleName}@${it.objectId}" }
          } else {
            null
          }
        }.filterNotNull().toList()

        if (allReceivers.isNotEmpty()) {
          labels += "Receivers"
          allReceivers.forEach { (contextString, receiverStrings) ->
            labels += "..$contextString"
            receiverStrings.forEach { receiverString ->
              labels += "....$receiverString"
            }
          }
        }
      }
    }
  },

  MORTAR_PRESENTER {
    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("mortar.Presenter") { instance ->
        val view = instance.getOrThrow("mortar.Presenter", "view")
        labels += view describedWithValue if (view.value.isNullReference) "null" else "not null"
      }
    }
  },

  MORTAR_SCOPE {
    override val leakingObjectFilter = { heapObject: HeapObject ->
      heapObject is HeapInstance &&
        heapObject instanceOf "mortar.MortarScope" &&
        heapObject.getOrThrow("mortar.MortarScope", "dead").value.asBoolean!!
    }

    override fun inspect(reporter: ObjectReporter) {
      reporter.whenInstanceOf("mortar.MortarScope") { instance ->
        val dead = instance.getOrThrow("mortar.MortarScope", "dead").value.asBoolean!!
        val scopeName = instance.getOrThrow("mortar.MortarScope", "name").value.readAsJavaString()
        if (dead) {
          leakingReasons += "mortar.MortarScope.dead is true for scope $scopeName"
        } else {
          notLeakingReasons += "mortar.MortarScope.dead is false for scope $scopeName"
        }
      }
    }
  },

  COORDINATOR {
    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("com.squareup.coordinators.Coordinator") { instance ->
        val attached = instance.getOrThrow("com.squareup.coordinators.Coordinator", "attached")
        labels += attached describedWithValue "${attached.value.asBoolean}"
      }
    }
  },

  MAIN_THREAD {
    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf(Thread::class) { instance ->
        val threadName = instance[Thread::class, "name"]!!.value.readAsJavaString()
        if (threadName == "main") {
          notLeakingReasons += "the main thread always runs"
        }
      }
    }
  },

  VIEW_ROOT_IMPL {
    override val leakingObjectFilter = { heapObject: HeapObject ->
      if (heapObject is HeapInstance &&
        heapObject instanceOf "android.view.ViewRootImpl"
      ) {
        if (heapObject["android.view.ViewRootImpl", "mView"]!!.value.isNullReference) {
          true
        } else {
          val mContextField = heapObject["android.view.ViewRootImpl", "mContext"]
          if (mContextField != null) {
            val mContext = mContextField.valueAsInstance!!
            val activityContext = mContext.unwrapActivityContext()
            (activityContext != null && activityContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true)
          } else {
            false
          }
        }
      } else {
        false
      }
    }

    override fun inspect(reporter: ObjectReporter) {
      reporter.whenInstanceOf("android.view.ViewRootImpl") { instance ->
        val mViewField = instance["android.view.ViewRootImpl", "mView"]!!
        if (mViewField.value.isNullReference) {
          leakingReasons += mViewField describedWithValue "null"
        } else {
          // ViewRootImpl.mContext wasn't always here.
          val mContextField = instance["android.view.ViewRootImpl", "mContext"]
          if (mContextField != null) {
            val mContext = mContextField.valueAsInstance!!
            val activityContext = mContext.unwrapActivityContext()
            if (activityContext != null && activityContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true) {
              leakingReasons += "ViewRootImpl.mContext references a destroyed activity, did you forget to cancel toasts or dismiss dialogs?"
            }
          }
          labels += mViewField describedWithValue "not null"
        }
        val mWindowAttributes =
          instance["android.view.ViewRootImpl", "mWindowAttributes"]!!.valueAsInstance!!
        val mTitle =
          mWindowAttributes["android.view.WindowManager\$LayoutParams", "mTitle"]!!.valueAsInstance!!.readAsJavaString()!!
        labels += "mWindowAttributes.mTitle = \"$mTitle\""

        val type =
          mWindowAttributes["android.view.WindowManager\$LayoutParams", "type"]!!.value.asInt!!
        // android.view.WindowManager.LayoutParams.TYPE_TOAST
        val details = if (type == 2005) {
          " (Toast)"
        } else ""
        labels += "mWindowAttributes.type = $type$details"
      }
    }
  },

  WINDOW {
    override val leakingObjectFilter = { heapObject: HeapObject ->
      heapObject is HeapInstance &&
        heapObject instanceOf "android.view.Window" &&
        heapObject["android.view.Window", "mDestroyed"]!!.value.asBoolean!!
    }

    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("android.view.Window") { instance ->
        val mDestroyed = instance["android.view.Window", "mDestroyed"]!!

        if (mDestroyed.value.asBoolean!!) {
          leakingReasons += mDestroyed describedWithValue "true"
        } else {
          // A dialog window could be leaking, destroy is only set to false for activity windows.
          labels += mDestroyed describedWithValue "false"
        }
      }
    }
  },

  MESSAGE {
    override fun inspect(reporter: ObjectReporter) {
      reporter.whenInstanceOf("android.os.Message") { instance ->
        labels += "Message.what = ${instance["android.os.Message", "what"]!!.value.asInt}"

        val heapDumpUptimeMillis = KeyedWeakReferenceFinder.heapDumpUptimeMillis(instance.graph)
        val whenUptimeMillis = instance["android.os.Message", "when"]!!.value.asLong!!

        labels += if (heapDumpUptimeMillis != null) {
          val diffMs = whenUptimeMillis - heapDumpUptimeMillis
          if (diffMs > 0) {
            "Message.when = $whenUptimeMillis ($diffMs ms after heap dump)"
          } else {
            "Message.when = $whenUptimeMillis (${diffMs.absoluteValue} ms before heap dump)"
          }
        } else {
          "Message.when = $whenUptimeMillis"
        }

        labels += "Message.obj = ${instance["android.os.Message", "obj"]!!.value.asObject}"
        labels += "Message.callback = ${instance["android.os.Message", "callback"]!!.value.asObject}"
      }
    }
  },

  TOAST {
    override val leakingObjectFilter = { heapObject: HeapObject ->
      if (heapObject is HeapInstance && heapObject instanceOf "android.widget.Toast") {
        val tnInstance =
          heapObject["android.widget.Toast", "mTN"]!!.value.asObject!!.asInstance!!
        (tnInstance["android.widget.Toast\$TN", "mWM"]!!.value.isNonNullReference &&
          tnInstance["android.widget.Toast\$TN", "mView"]!!.value.isNullReference)
      } else false
    }

    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf("android.widget.Toast") { instance ->
        val tnInstance =
          instance["android.widget.Toast", "mTN"]!!.value.asObject!!.asInstance!!
        // mWM is set in android.widget.Toast.TN#handleShow and never unset, so this toast was never
        // shown, we don't know if it's leaking.
        if (tnInstance["android.widget.Toast\$TN", "mWM"]!!.value.isNonNullReference) {
          // mView is reset to null in android.widget.Toast.TN#handleHide
          if (tnInstance["android.widget.Toast\$TN", "mView"]!!.value.isNullReference) {
            leakingReasons += "This toast is done showing (Toast.mTN.mWM != null && Toast.mTN.mView == null)"
          } else {
            notLeakingReasons += "This toast is showing (Toast.mTN.mWM != null && Toast.mTN.mView != null)"
          }
        }
      }
    }
  };

  internal open val leakingObjectFilter: ((heapObject: HeapObject) -> Boolean)? = null

  companion object {
    /** @see AndroidObjectInspectors */
    val appDefaults: List<ObjectInspector>
      get() = ObjectInspectors.jdkDefaults + values()

    /**
     * Returns a list of [LeakingObjectFilter] suitable for apps.
     */
    val appLeakingObjectFilters: List<LeakingObjectFilter> =
      ObjectInspectors.jdkLeakingObjectFilters +
        createLeakingObjectFilters(EnumSet.allOf(AndroidObjectInspectors::class.java))

    /**
     * Creates a list of [LeakingObjectFilter] based on the passed in [AndroidObjectInspectors].
     */
    fun createLeakingObjectFilters(inspectors: Set<AndroidObjectInspectors>): List<LeakingObjectFilter> =
      inspectors.mapNotNull { it.leakingObjectFilter }
        .map { filter ->
          object : LeakingObjectFilter {
            override fun isLeakingObject(heapObject: HeapObject) = filter(heapObject)
          }
        }
  }

  // Using a string builder to prevent Jetifier from changing this string to Android X Fragment
  @Suppress("VariableNaming", "PropertyName")
  internal val ANDROID_SUPPORT_FRAGMENT_CLASS_NAME =
    StringBuilder("android.").append("support.v4.app.Fragment")
      .toString()
}

private fun HeapInstance.outerContextIsLeaking() =
  this["android.app.ContextImpl", "mOuterContext"]!!
    .valueAsInstance!!
    .run {
      this instanceOf "android.app.Activity" &&
        this["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true
    }

private fun ObjectReporter.inspectContextImplOuterContext(
  outerContext: HeapInstance,
  contextImpl: HeapInstance,
  prefix: String = "ContextImpl"
) {
  if (outerContext instanceOf "android.app.Activity") {
    val mDestroyed = outerContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean
    if (mDestroyed != null) {
      if (mDestroyed) {
        leakingReasons += "$prefix.mOuterContext is an instance of" +
          " ${outerContext.instanceClassName} with Activity.mDestroyed true"
      } else {
        notLeakingReasons += "$prefix.mOuterContext is an instance of " +
          "${outerContext.instanceClassName} with Activity.mDestroyed false"
      }
    } else {
      labels += "$prefix.mOuterContext is an instance of ${outerContext.instanceClassName}"
    }
  } else if (outerContext instanceOf "android.app.Application") {
    notLeakingReasons += "$prefix.mOuterContext is an instance of" +
      " ${outerContext.instanceClassName} which extends android.app.Application"
  } else if (outerContext.objectId == contextImpl.objectId) {
    labels += "$prefix.mOuterContext == ContextImpl.this: not tied to any particular lifecycle"
  } else {
    labels += "$prefix.mOuterContext is an instance of ${outerContext.instanceClassName}"
  }
}

private infix fun HeapField.describedWithValue(valueDescription: String): String {
  return "${declaringClass.simpleName}#$name is $valueDescription"
}

private fun ObjectReporter.applyFromField(
  inspector: ObjectInspector,
  field: HeapField?
) {
  if (field == null) {
    return
  }
  if (field.value.isNullReference) {
    return
  }
  val heapObject = field.value.asObject!!
  val delegateReporter = ObjectReporter(heapObject)
  inspector.inspect(delegateReporter)
  val prefix = "${field.declaringClass.simpleName}#${field.name}:"

  labels += delegateReporter.labels.map { "$prefix $it" }
  leakingReasons += delegateReporter.leakingReasons.map { "$prefix $it" }
  notLeakingReasons += delegateReporter.notLeakingReasons.map { "$prefix $it" }
}

/**
 * Recursively unwraps `this` [HeapInstance] as a ContextWrapper until an Activity is found in which case it is
 * returned. Returns null if no activity was found.
 */
internal fun HeapInstance.unwrapActivityContext(): HeapInstance? {
  return unwrapComponentContext().let { context ->
    if (context != null && context instanceOf "android.app.Activity") {
      context
    } else {
      null
    }
  }
}

/**
 * Recursively unwraps `this` [HeapInstance] as a ContextWrapper until an known Android component
 * context is found in which case it is returned. Returns null if no activity was found.
 */
@Suppress("NestedBlockDepth", "ReturnCount")
internal fun HeapInstance.unwrapComponentContext(): HeapInstance? {
  val matchingClassName = instanceClass.classHierarchy.map { it.name }
    .firstOrNull {
      when (it) {
        "android.content.ContextWrapper",
        "android.app.Activity",
        "android.app.Application",
        "android.app.Service"
        -> true
        else -> false
      }
    }
    ?: return null

  if (matchingClassName != "android.content.ContextWrapper") {
    return this
  }

  var context = this
  val visitedInstances = mutableListOf<Long>()
  var keepUnwrapping = true
  while (keepUnwrapping) {
    visitedInstances += context.objectId
    keepUnwrapping = false
    val mBase = context["android.content.ContextWrapper", "mBase"]!!.value

    if (mBase.isNonNullReference) {
      val wrapperContext = context
      context = mBase.asObject!!.asInstance!!

      val contextMatchingClassName = context.instanceClass.classHierarchy.map { it.name }
        .firstOrNull {
          when (it) {
            "android.content.ContextWrapper",
            "android.app.Activity",
            "android.app.Application",
            "android.app.Service"
            -> true
            else -> false
          }
        }

      var isContextWrapper = contextMatchingClassName == "android.content.ContextWrapper"

      if (contextMatchingClassName == "android.app.Activity") {
        return context
      } else {
        if (wrapperContext instanceOf "com.android.internal.policy.DecorContext") {
          // mBase isn't an activity, let's unwrap DecorContext.mPhoneWindow.mContext instead
          val mPhoneWindowField =
            wrapperContext["com.android.internal.policy.DecorContext", "mPhoneWindow"]
          if (mPhoneWindowField != null) {
            val phoneWindow = mPhoneWindowField.valueAsInstance!!
            context = phoneWindow["android.view.Window", "mContext"]!!.valueAsInstance!!
            if (context instanceOf "android.app.Activity") {
              return context
            }
            isContextWrapper = context instanceOf "android.content.ContextWrapper"
          }
        }
        if (contextMatchingClassName == "android.app.Service" ||
          contextMatchingClassName == "android.app.Application"
        ) {
          return context
        }
        if (isContextWrapper &&
          // Avoids infinite loops
          context.objectId !in visitedInstances
        ) {
          keepUnwrapping = true
        }
      }
    }
  }
  return null
}

/**
 * Same as [HeapInstance.readField] but throws if the field doesnt exist
 */
internal fun HeapInstance.getOrThrow(
  declaringClassName: String,
  fieldName: String
): HeapField {
  return this[declaringClassName, fieldName] ?: throw IllegalStateException(
    """
$instanceClassName is expected to have a $declaringClassName.$fieldName field which cannot be found.
This might be due to the app code being obfuscated. If that's the case, then the heap analysis
is unable to proceed without a mapping file to deobfuscate class names.
You can run LeakCanary on obfuscated builds by following the instructions at
https://square.github.io/leakcanary/recipes/#using-leakcanary-with-obfuscated-apps
      """
  )
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package shark

import shark.AndroidReferenceMatchers.Companion.appDefaults
import shark.AndroidReferenceMatchers.Companion.buildKnownReferences
import shark.ReferencePattern.InstanceFieldPattern
import shark.ReferencePattern.JavaLocalPattern
import shark.ReferencePattern.NativeGlobalVariablePattern
import shark.ReferencePattern.StaticFieldPattern
import java.lang.ref.PhantomReference
import java.lang.ref.SoftReference
import java.lang.ref.WeakReference
import java.util.EnumSet

/**
 * [AndroidReferenceMatchers] values add [ReferenceMatcher] instances to a global list via their
 * [add] method. A [ReferenceMatcher] is either a [IgnoredReferenceMatcher] or
 * a [LibraryLeakReferenceMatcher].
 *
 * [AndroidReferenceMatchers] is used to build the list of known references that cannot ever create
 * leaks (via [IgnoredReferenceMatcher]) as well as the list of known leaks in the Android Framework
 * and in manufacturer specific Android implementations.
 *
 * This class is a work in progress. You can help by reporting leak traces that seem to be caused
 * by the Android SDK, here: https://github.com/square/leakcanary/issues/new
 *
 * We filter on SDK versions and Manufacturers because many of those leaks are specific to a given
 * manufacturer implementation, they usually share their builds across multiple models, and the
 * leaks eventually get fixed in newer versions.
 *
 * Most app developers should use [appDefaults]. However, you can also use a subset of
 * [AndroidReferenceMatchers] by creating an [EnumSet] that matches your needs and calling
 * [buildKnownReferences].
 */
enum class AndroidReferenceMatchers {

  // ######## Android Framework known leaks ########

  IREQUEST_FINISH_CALLBACK {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.app.Activity\$1", "this\$0",
        description = "Android Q added a new android.app.IRequestFinishCallback\$Stub " +
          "class. android.app.Activity creates an implementation of that interface as an " +
          "anonymous subclass. That anonymous subclass has a reference to the activity. " +
          "Another process is keeping the android.app.IRequestFinishCallback\$Stub " +
          "reference alive long after Activity.onDestroyed() has been called, " +
          "causing the activity to leak. " +
          "Fix: You can \"fix\" this leak by overriding Activity.onBackPressed() and calling " +
          "Activity.finishAfterTransition(); instead of super if the activity is task root and the " +
          "fragment stack is empty. " +
          "Tracked here: https://issuetracker.google.com/issues/139738913"
      ) {
        sdkInt == 29
      }
    }
  },

  ACTIVITY_CLIENT_RECORD__NEXT_IDLE {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.app.ActivityThread\$ActivityClientRecord", "nextIdle",
        description = "Android AOSP sometimes keeps a reference to a destroyed activity as a"
          + " nextIdle client record in the android.app.ActivityThread.mActivities map."
          + " Not sure what's going on there, input welcome."
      ) {
        sdkInt in 19..27
      }
    }
  },

  SPAN_CONTROLLER {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      val description =
        ("Editor inserts a special span, which has a reference to the EditText. That span is a"
          + " NoCopySpan, which makes sure it gets dropped when creating a new"
          + " SpannableStringBuilder from a given CharSequence."
          + " TextView.onSaveInstanceState() does a copy of its mText before saving it in the"
          + " bundle. Prior to KitKat, that copy was done using the SpannableString"
          + " constructor, instead of SpannableStringBuilder. The SpannableString constructor"
          + " does not drop NoCopySpan spans. So we end up with a saved state that holds a"
          + " reference to the textview and therefore the entire view hierarchy & activity"
          + " context. Fix: https://github.com/android/platform_frameworks_base/commit"
          + "/af7dcdf35a37d7a7dbaad7d9869c1c91bce2272b ."
          + " To fix this, you could override TextView.onSaveInstanceState(), and then use"
          + " reflection to access TextView.SavedState.mText and clear the NoCopySpan spans.")

      references += instanceFieldLeak(
        "android.widget.Editor\$SpanController", "this$0", description
      ) {
        sdkInt <= 19
      }

      references += instanceFieldLeak(
        "android.widget.Editor\$EasyEditSpanController", "this$0", description
      ) {
        sdkInt <= 19
      }
    }
  },

  MEDIA_SESSION_LEGACY_HELPER__SINSTANCE {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references +=
        staticFieldLeak(
          "android.media.session.MediaSessionLegacyHelper", "sInstance",
          description = "MediaSessionLegacyHelper is a static singleton that is lazily instantiated and"
            + " keeps a reference to the context it's given the first time"
            + " MediaSessionLegacyHelper.getHelper() is called."
            + " This leak was introduced in android-5.0.1_r1 and fixed in Android 5.1.0_r1 by"
            + " calling context.getApplicationContext()."
            + " Fix: https://github.com/android/platform_frameworks_base/commit"
            + "/9b5257c9c99c4cb541d8e8e78fb04f008b1a9091"
            + " To fix this, you could call MediaSessionLegacyHelper.getHelper() early"
            + " in Application.onCreate() and pass it the application context."
        ) {
          sdkInt == 21
        }
    }
  },

  TEXT_LINE__SCACHED {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += staticFieldLeak(
        "android.text.TextLine", "sCached",
        description = "TextLine.sCached is a pool of 3 TextLine instances. TextLine.recycle() has had"
          + " at least two bugs that created memory leaks by not correctly clearing the"
          + " recycled TextLine instances. The first was fixed in android-5.1.0_r1:"
          + " https://github.com/android/platform_frameworks_base/commit"
          + "/893d6fe48d37f71e683f722457bea646994a10"
          + " The second was fixed, not released yet:"
          + " https://github.com/android/platform_frameworks_base/commit"
          + "/b3a9bc038d3a218b1dbdf7b5668e3d6c12be5e"
          + " To fix this, you could access TextLine.sCached and clear the pool every now"
          + " and then (e.g. on activity destroy)."
      ) {
        sdkInt <= 22
      }
    }
  },

  BLOCKING_QUEUE {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      val description = ("A thread waiting on a blocking queue will leak the last"
        + " dequeued object as a stack local reference. So when a HandlerThread becomes idle, it"
        + " keeps a local reference to the last message it received. That message then gets"
        + " recycled and can be used again. As long as all messages are recycled after being"
        + " used, this won't be a problem, because these references are cleared when being"
        + " recycled. However, dialogs create template Message instances to be copied when a"
        + " message needs to be sent. These Message templates holds references to the dialog"
        + " listeners, which most likely leads to holding a reference onto the activity in some"
        + " way. Dialogs never recycle their template Message, assuming these Message instances"
        + " will get GCed when the dialog is GCed."
        + " The combination of these two things creates a high potential for memory leaks as soon"
        + " as you use dialogs. These memory leaks might be temporary, but some handler threads"
        + " sleep for a long time."
        + " To fix this, you could post empty messages to the idle handler threads from time to"
        + " time. This won't be easy because you cannot access all handler threads, but a library"
        + " that is widely used should consider doing this for its own handler threads. This leaks"
        + " has been shown to happen in both Dalvik and ART.")

      references += instanceFieldLeak("android.os.Message", "obj", description)
    }
  },

  INPUT_METHOD_MANAGER_IS_TERRIBLE {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      val description =
        ("When we detach a view that receives keyboard input, the InputMethodManager"
          + " leaks a reference to it until a new view asks for keyboard input."
          + " Tracked here: https://code.google.com/p/android/issues/detail?id=171190"
          + " Hack: https://gist.github.com/pyricau/4df64341cc978a7de414")

      references += instanceFieldLeak(
        "android.view.inputmethod.InputMethodManager", "mNextServedView", description
      ) {
        sdkInt in 15..27
      }

      references += instanceFieldLeak(
        "android.view.inputmethod.InputMethodManager", "mServedView", description
      ) {
        sdkInt in 15..27
      }

      references += instanceFieldLeak(
        "android.view.inputmethod.InputMethodManager", "mServedInputConnection", description
      ) {
        sdkInt in 15..27
      }

      references += instanceFieldLeak(
        "android.view.inputmethod.InputMethodManager", "mLastSrvView",
        description =
        "HUAWEI added a mLastSrvView field to InputMethodManager" + " that leaks a reference to the last served view."
      ) {
        manufacturer == HUAWEI && sdkInt in 23..28
      }

      references += instanceFieldLeak(
        "android.view.inputmethod.InputMethodManager", "mCurRootView",
        description = "The singleton InputMethodManager is holding a reference to mCurRootView long"
          + " after the activity has been destroyed."
          + " Observed on ICS MR1: https://github.com/square/leakcanary/issues/1"
          + "#issuecomment-100579429"
          + " Hack: https://gist.github.com/pyricau/4df64341cc978a7de414"
      ) {
        sdkInt in 15..28
      }

      references += instanceFieldLeak(
        "android.view.inputmethod.InputMethodManager", "mImeInsetsConsumer",
        description = """
              Android Q Beta has a leak where InputMethodManager.mImeInsetsConsumer isn't set to
              null when the activity is destroyed.
            """.trimIndent()
      ) {
        sdkInt == 28
      }

      references += instanceFieldLeak(
        "android.view.inputmethod.InputMethodManager", "mCurrentInputConnection",
        description = """
              In Android Q Beta InputMethodManager keeps its EditableInputConnection after the
              activity has been destroyed.
            """.trimIndent()
      ) {
        sdkInt in 28..29
      }
    }
  },

  LAYOUT_TRANSITION {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.animation.LayoutTransition$1", "val\$parent",
        description = "LayoutTransition leaks parent ViewGroup through"
          + " ViewTreeObserver.OnPreDrawListener When triggered, this leaks stays until the"
          + " window is destroyed. Tracked here:"
          + " https://code.google.com/p/android/issues/detail?id=171830"
      ) {
        sdkInt in 14..22
      }
    }
  },

  SPELL_CHECKER_SESSION {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.view.textservice.SpellCheckerSession$1", "this$0",
        description = "SpellCheckerSessionListenerImpl.mHandler is leaking destroyed Activity when the"
          + " SpellCheckerSession is closed before the service is connected."
          + " Tracked here: https://code.google.com/p/android/issues/detail?id=172542"
      ) {
        sdkInt in 16..24
      }
    }
  },

  SPELL_CHECKER {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.widget.SpellChecker$1", "this$0",
        description = "SpellChecker holds on to a detached view that points to a destroyed activity."
          + " mSpellRunnable is being enqueued, and that callback should be removed when "
          + " closeSession() is called. Maybe closeSession() wasn't called, or maybe it was "
          + " called after the view was detached."
      ) {
        sdkInt == 22
      }
    }
  },

  ACTIVITY_CHOOSE_MODEL {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      val description = ("ActivityChooserModel holds a static reference to the last set"
        + " ActivityChooserModelPolicy which can be an activity context."
        + " Tracked here: https://code.google.com/p/android/issues/detail?id=172659"
        + " Hack: https://gist.github.com/andaag/b05ab66ed0f06167d6e0")

      references += instanceFieldLeak(
        "android.support.v7.internal.widget.ActivityChooserModel",
        "mActivityChoserModelPolicy",
        description = description
      ) {
        sdkInt in 15..22
      }

      references += instanceFieldLeak(
        "android.widget.ActivityChooserModel", "mActivityChoserModelPolicy",
        description = description
      )
    }
  },

  MEDIA_PROJECTION_CALLBACK {
    override fun add(references: MutableList<ReferenceMatcher>) {
      references += instanceFieldLeak(
        "android.media.projection.MediaProjection\$MediaProjectionCallback",
        "this$0", description = """
              MediaProjectionCallback is held by another process, and holds on to MediaProjection
              which has an activity as its context.
            """.trimIndent()
      ) {
        sdkInt in 22..28
      }
    }
  },

  SPEECH_RECOGNIZER {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.speech.SpeechRecognizer\$InternalListener", "this$0",
        description = "Prior to Android 5, SpeechRecognizer.InternalListener was a non static inner"
          + " class and leaked the SpeechRecognizer which leaked an activity context."
          + " Fixed in AOSP: https://github.com/android/platform_frameworks_base/commit"
          + " /b37866db469e81aca534ff6186bdafd44352329b"
      ) {
        sdkInt < 21
      }
    }
  },

  ACCOUNT_MANAGER {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.accounts.AccountManager\$AmsTask\$Response", "this$1",
        description =
        "AccountManager\$AmsTask\$Response is a stub and is held in memory by native code,"
          + " probably because the reference to the response in the other process hasn't been"
          + " cleared."
          + " AccountManager\$AmsTask is holding on to the activity reference to use for"
          + " launching a new sub- Activity."
          + " Tracked here: https://code.google.com/p/android/issues/detail?id=173689"
          + " Fix: Pass a null activity reference to the AccountManager methods and then deal"
          + " with the returned future to to get the result and correctly start an activity"
          + " when it's available."
      ) {
        sdkInt <= 27
      }
    }
  },

  MEDIA_SCANNER_CONNECTION {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.media.MediaScannerConnection", "mContext",

        description =
        "The static method MediaScannerConnection.scanFile() takes an activity context"
          + " but the service might not disconnect after the activity has been destroyed."
          + " Tracked here: https://code.google.com/p/android/issues/detail?id=173788"
          + " Fix: Create an instance of MediaScannerConnection yourself and pass in the"
          + " application context. Call connect() and disconnect() manually."
      ) {
        sdkInt <= 22
      }
    }
  },

  USER_MANAGER__SINSTANCE {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.os.UserManager", "mContext",
        description =
        "UserManager has a static sInstance field that creates an instance and caches it"
          + " the first time UserManager.get() is called. This instance is created with the"
          + " outer context (which is an activity base context)."
          + " Tracked here: https://code.google.com/p/android/issues/detail?id=173789"
          + " Introduced by: https://github.com/android/platform_frameworks_base/commit"
          + "/27db46850b708070452c0ce49daf5f79503fbde6"
          + " Fix: trigger a call to UserManager.get() in Application.onCreate(), so that the"
          + " UserManager instance gets cached with a reference to the application context."
      ) {
        sdkInt in 18..25
      }
    }
  },

  APP_WIDGET_HOST_CALLBACKS {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.appwidget.AppWidgetHost\$Callbacks", "this$0",
        description =
        "android.appwidget.AppWidgetHost\$Callbacks is a stub and is held in memory native"
          + " code. The reference to the `mContext` was not being cleared, which caused the"
          + " Callbacks instance to retain this reference"
          + " Fixed in AOSP: https://github.com/android/platform_frameworks_base/commit"
          + "/7a96f3c917e0001ee739b65da37b2fadec7d7765"
      ) {
        sdkInt < 22
      }
    }
  },

  AUDIO_MANAGER {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.media.AudioManager$1", "this$0",
        description =
        "Prior to Android M, VideoView required audio focus from AudioManager and"
          + " never abandoned it, which leaks the Activity context through the AudioManager."
          + " The root of the problem is that AudioManager uses whichever"
          + " context it receives, which in the case of the VideoView example is an Activity,"
          + " even though it only needs the application's context. The issue is fixed in"
          + " Android M, and the AudioManager now uses the application's context."
          + " Tracked here: https://code.google.com/p/android/issues/detail?id=152173"
          + " Fix: https://gist.github.com/jankovd/891d96f476f7a9ce24e2"
      ) {
        sdkInt <= 22
      }
    }
  },

  EDITTEXT_BLINK_MESSAGEQUEUE {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.widget.Editor\$Blink", "this$0",
        description =
        "The EditText Blink of the Cursor is implemented using a callback and Messages,"
          + " which trigger the display of the Cursor. If an AlertDialog or DialogFragment that"
          + " contains a blinking cursor is detached, a message is posted with a delay after the"
          + " dialog has been closed and as a result leaks the Activity."
          + " This can be fixed manually by calling TextView.setCursorVisible(false) in the"
          + " dismiss() method of the dialog."
          + " Tracked here: https://code.google.com/p/android/issues/detail?id=188551"
          + " Fixed in AOSP: https://android.googlesource.com/platform/frameworks/base/+"
          + "/5b734f2430e9f26c769d6af8ea5645e390fcf5af%5E%21/"
      ) {
        sdkInt <= 23
      }
    }
  },

  CONNECTIVITY_MANAGER__SINSTANCE {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.net.ConnectivityManager", "sInstance",
        description =
        "ConnectivityManager has a sInstance field that is set when the first"
          + " ConnectivityManager instance is created. ConnectivityManager has a mContext field."
          + " When calling activity.getSystemService(Context.CONNECTIVITY_SERVICE) , the first"
          + " ConnectivityManager instance is created with the activity context and stored in"
          + " sInstance. That activity context then leaks forever."
          + " Until this is fixed, app developers can prevent this leak by making sure the"
          + " ConnectivityManager is first created with an App Context. E.g. in some static"
          + " init do: context.getApplicationContext()"
          + ".getSystemService(Context.CONNECTIVITY_SERVICE)"
          + " Tracked here: https://code.google.com/p/android/issues/detail?id=198852"
          + " Introduced here: https://github.com/android/platform_frameworks_base/commit/"
          + "e0bef71662d81caaaa0d7214fb0bef5d39996a69"
      ) {
        sdkInt <= 23
      }
    }
  },

  ACCESSIBILITY_NODE_INFO__MORIGINALTEXT {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.view.accessibility.AccessibilityNodeInfo", "mOriginalText",
        description =
        "AccessibilityNodeInfo has a static sPool of AccessibilityNodeInfo. When"
          + " AccessibilityNodeInfo instances are released back in the pool,"
          + " AccessibilityNodeInfo.clear() does not clear the mOriginalText field, which"
          + " causes spans to leak which in turns causes TextView.ChangeWatcher to leak and the"
          + " whole view hierarchy. Introduced here: https://android.googlesource.com/platform/"
          + "frameworks/base/+/193520e3dff5248ddcf8435203bf99d2ba667219%5E%21/core/java/"
          + "android/view/accessibility/AccessibilityNodeInfo.java"
      ) {
        sdkInt in 26..27
      }
    }
  },

  ASSIST_STRUCTURE {
    override fun add(references: MutableList<ReferenceMatcher>) {
      references += instanceFieldLeak(
        "android.app.assist.AssistStructure\$ViewNodeText", "mText",
        description = "AssistStructure (google assistant / autofill) holds on to text spannables" +
          " on the screen. TextView.ChangeWatcher and android.widget.Editor end up in spans and" +
          " typically hold on to the view hierarchy"
      ) {
        sdkInt in 24..29
      }
    }
  },

  ACCESSIBILITY_ITERATORS {
    override fun add(references: MutableList<ReferenceMatcher>) {
      references += instanceFieldLeak(
        "android.widget.AccessibilityIterators\$LineTextSegmentIterator", "mLayout",
        description = "AccessibilityIterators holds on to text layouts which can hold on to spans" +
          " TextView.ChangeWatcher and android.widget.Editor end up in spans and" +
          " typically hold on to the view hierarchy"
      ) {
        sdkInt == 27
      }
    }
  },

  BIOMETRIC_PROMPT {
    override fun add(references: MutableList<ReferenceMatcher>) {
      references += instanceFieldLeak(
        "android.hardware.biometrics.BiometricPrompt", "mFingerprintManager",
        description = "BiometricPrompt holds on to a FingerprintManager which holds on to a " +
          "destroyed activity."
      ) {
        sdkInt == 28
      }
    }
  },

  MAGNIFIER {
    override fun add(references: MutableList<ReferenceMatcher>) {
      references += instanceFieldLeak(
        "android.widget.Magnifier\$InternalPopupWindow", "mCallback",
        description = "android.widget.Magnifier.InternalPopupWindow registers a frame callback" +
          " on android.view.ThreadedRenderer.SimpleRenderer which holds it as a native" +
          " reference. android.widget.Editor\$InsertionHandleView registers an" +
          " OnOperationCompleteCallback on Magnifier.InternalPopupWindow. These references are" +
          " held after the activity has been destroyed."
      ) {
        sdkInt == 28
      }
    }
  },

  BACKDROP_FRAME_RENDERER__MDECORVIEW {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "com.android.internal.policy.BackdropFrameRenderer", "mDecorView",
        description =
        "When BackdropFrameRenderer.releaseRenderer() is called, there's an unknown case"
          + " where mRenderer becomes null but mChoreographer doesn't and the thread doesn't"
          + " stop and ends up leaking mDecorView which itself holds on to a destroyed"
          + " activity"
      ) {
        sdkInt in 24..26
      }
    }
  },

  VIEWLOCATIONHOLDER_ROOT {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.view.ViewGroup\$ViewLocationHolder",
        "mRoot",
        description = "In Android P, ViewLocationHolder has an mRoot field that is not cleared " +
          "in its clear() method. Introduced in https://github.com/aosp-mirror" +
          "/platform_frameworks_base/commit/86b326012813f09d8f1de7d6d26c986a909d Bug " +
          "report: https://issuetracker.google.com/issues/112792715"
      ) {
        sdkInt == 28
      }
    }
  },

  ACCESSIBILITY_NODE_ID_MANAGER {
    override fun add(references: MutableList<ReferenceMatcher>) {
      references += instanceFieldLeak(
        "android.view.accessibility.AccessibilityNodeIdManager", "mIdsToViews",
        description = """
              Android Q Beta added AccessibilityNodeIdManager which stores all views from their
              onAttachedToWindow() call, until detached. Unfortunately it's possible to trigger
              the view framework to call detach before attach (by having a view removing itself
              from its parent in onAttach, which then causes AccessibilityNodeIdManager to keep
              children view forever. Future releases of Q will hold weak references.
            """.trimIndent()
      ) {
        sdkInt in 28..29
      }
    }
  },

  TEXT_TO_SPEECH {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      val description =
        ("TextToSpeech.shutdown() does not release its references to context objects." +
          " Furthermore, TextToSpeech instances cannot be garbage collected due to other process" +
          " keeping the references, resulting the context objects leaked." +
          " Developers might be able to mitigate the issue by passing application context" +
          " to TextToSpeech constructor." +
          " Tracked at: https://github.com/square/leakcanary/issues/1210 and" +
          " https://issuetracker.google.com/issues/129250419")
      references += instanceFieldLeak(
        "android.speech.tts.TextToSpeech", "mContext",
        description = description
      ) {
        sdkInt == 24
      }

      references += instanceFieldLeak(
        "android.speech.tts.TtsEngines", "mContext",
        description = description
      ) {
        sdkInt == 24
      }
    }
  },

  CONTROLLED_INPUT_CONNECTION_WRAPPER {
    override fun add(references: MutableList<ReferenceMatcher>) {
      references += nativeGlobalVariableLeak(
        "android.view.inputmethod.InputMethodManager\$ControlledInputConnectionWrapper",
        description = """
        ControlledInputConnectionWrapper is held by a global variable in native code.
      """.trimIndent()
      )
    }
  },

  TOAST_TN {
    override fun add(references: MutableList<ReferenceMatcher>) {
      references += nativeGlobalVariableLeak(
        "android.widget.Toast\$TN",
        description = """
        Toast.TN is held by a global variable in native code due to an IPC call to show the toast.
      """.trimIndent()
      )
    }
  },

  APPLICATION_PACKAGE_MANAGER__HAS_SYSTEM_FEATURE_QUERY {
    override fun add(references: MutableList<ReferenceMatcher>) {
      references  += instanceFieldLeak(
        "android.app.ApplicationPackageManager\$HasSystemFeatureQuery", "this\$0",
        description = """
          In Android 11 DP 2 ApplicationPackageManager.HasSystemFeatureQuery was an inner class.
          Introduced in https://cs.android.com/android/_/android/platform/frameworks/base/+/89608118192580ffca026b5dacafa637a556d578
          Fixed in https://cs.android.com/android/_/android/platform/frameworks/base/+/1f771846c51148b7cb6283e6dc82a216ffaa5353
          Related blog: https://dev.to/pyricau/beware-packagemanager-leaks-223g
        """.trimIndent()
      ) {
        sdkInt == 29
      }
    }
  },

  // ######## Manufacturer specific known leaks ########

  // SAMSUNG

  SPEN_GESTURE_MANAGER {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += staticFieldLeak(
        "com.samsung.android.smartclip.SpenGestureManager", "mContext",
        description =
        "SpenGestureManager has a static mContext field that leaks a reference to the" + " activity. Yes, a STATIC mContext field."
      ) {
        manufacturer == SAMSUNG && sdkInt == 19
      }
    }
  },

  CLIPBOARD_UI_MANAGER__SINSTANCE {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.sec.clipboard.ClipboardUIManager", "mContext",
        description =
        "ClipboardUIManager is a static singleton that leaks an activity context."
          + " Fix: trigger a call to ClipboardUIManager.getInstance() in Application.onCreate()"
          + " , so that the ClipboardUIManager instance gets cached with a reference to the"
          + " application context. Example: https://gist.github.com/cypressious/"
          + "91c4fb1455470d803a602838dfcd5774"
      ) {
        manufacturer == SAMSUNG && sdkInt in 19..21
      }
    }
  },

  SEM_CLIPBOARD_MANAGER__MCONTEXT {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      val description = """
         SemClipboardManager inner classes are held by native references due to IPC calls
      """.trimIndent()
      references += nativeGlobalVariableLeak(
        "com.samsung.android.content.clipboard.SemClipboardManager$1", description
      ) {
        manufacturer == SAMSUNG && sdkInt in 19..28
      }
      references += nativeGlobalVariableLeak(
        "com.samsung.android.content.clipboard.SemClipboardManager$3", description
      ) {
        manufacturer == SAMSUNG && sdkInt in 19..28
      }
    }
  },

  CLIPBOARD_EX_MANAGER {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.sec.clipboard.ClipboardExManager", "mContext",
        description = "android.sec.clipboard.ClipboardExManager\$IClipboardDataPasteEventImpl\$1" +
          " is a native callback that holds IClipboardDataPasteEventImpl which holds" +
          " ClipboardExManager which has a destroyed activity as mContext"
      ) {
        manufacturer == SAMSUNG && sdkInt == 23
      }
      references += instanceFieldLeak(
        "android.sec.clipboard.ClipboardExManager", "mPersonaManager",
        description = "android.sec.clipboard.ClipboardExManager\$IClipboardDataPasteEventImpl\$1" +
          " is a native callback that holds IClipboardDataPasteEventImpl which holds" +
          " ClipboardExManager which holds PersonaManager which has a destroyed activity as" +
          " mContext"
      ) {
        manufacturer == SAMSUNG && sdkInt == 23
      }
      references += instanceFieldLeak(
        "android.widget.TextView\$IClipboardDataPasteEventImpl", "this\$0",
        description = "TextView\$IClipboardDataPasteEventImpl\$1 is held by a native ref, and" +
          " IClipboardDataPasteEventImpl ends up leaking a detached textview"
      ) {
        manufacturer == SAMSUNG && sdkInt == 22
      }
    }
  },

  SEM_EMERGENCY_MANAGER__MCONTEXT {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "com.samsung.android.emergencymode.SemEmergencyManager", "mContext",
        description =
        "SemEmergencyManager is a static singleton that leaks a DecorContext." +
          " Fix: https://gist.github.com/jankovd/a210460b814c04d500eb12025902d60d"
      ) {
        manufacturer == SAMSUNG && sdkInt in 19..24
      }
    }
  },

  SEM_PERSONA_MANAGER {
    override fun add(references: MutableList<ReferenceMatcher>) {
      references += instanceFieldLeak(
        "com.samsung.android.knox.SemPersonaManager", "mContext"
      ) {
        manufacturer == SAMSUNG && sdkInt == 24
      }
    }
  },

  SEM_APP_ICON_SOLUTION {
    override fun add(references: MutableList<ReferenceMatcher>) {
      references += instanceFieldLeak(
        "android.app.SemAppIconSolution", "mContext"
      ) {
        manufacturer == SAMSUNG && sdkInt in 28..29
      }
    }
  },

  AW_RESOURCE__SRESOURCES {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      // AwResource#setResources() is called with resources that hold a reference to the
      // activity context (instead of the application context) and doesn't clear it.
      // Not sure what's going on there, input welcome.
      references += staticFieldLeak(
        "com.android.org.chromium.android_webview.AwResource", "sResources"
      ) {
        manufacturer == SAMSUNG && sdkInt == 19
      }
    }
  },

  TEXT_VIEW__MLAST_HOVERED_VIEW {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += staticFieldLeak(
        "android.widget.TextView", "mLastHoveredView",
        description =
        "mLastHoveredView is a static field in TextView that leaks the last hovered" + " view."
      ) {
        manufacturer == SAMSUNG && sdkInt in 19..29
      }
    }
  },

  PERSONA_MANAGER {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.os.PersonaManager", "mContext",
        description =
        "android.app.LoadedApk.mResources has a reference to"
          + " android.content.res.Resources.mPersonaManager which has a reference to"
          + " android.os.PersonaManager.mContext which is an activity."
      ) {
        manufacturer == SAMSUNG && sdkInt == 19
      }
    }
  },

  RESOURCES__MCONTEXT {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.content.res.Resources", "mContext",
        description =
        "In AOSP the Resources class does not have a context."
          + " Here we have ZygoteInit.mResources (static field) holding on to a Resources"
          + " instance that has a context that is the activity."
          + " Observed here: https://github.com/square/leakcanary/issues/1#issue-74450184"
      ) {
        manufacturer == SAMSUNG && sdkInt == 19
      }
    }
  },

  VIEW_CONFIGURATION__MCONTEXT {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.view.ViewConfiguration", "mContext",
        description =
        "In AOSP the ViewConfiguration class does not have a context."
          + " Here we have ViewConfiguration.sConfigurations (static field) holding on to a"
          + " ViewConfiguration instance that has a context that is the activity."
          + " Observed here: https://github.com/square/leakcanary/issues"
          + "/1#issuecomment-100324683"
      ) {
        manufacturer == SAMSUNG && sdkInt == 19
      }
    }
  },

  AUDIO_MANAGER__MCONTEXT_STATIC {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += staticFieldLeak(
        "android.media.AudioManager", "mContext_static",
        description =
        "Samsung added a static mContext_static field to AudioManager, holds a reference"
          + " to the activity."
          + " Observed here: https://github.com/square/leakcanary/issues/32"
      ) {
        manufacturer == SAMSUNG && sdkInt == 19
      }
    }
  },

  ACTIVITY_MANAGER_MCONTEXT {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += staticFieldLeak(
        "android.app.ActivityManager", "mContext",
        description =
        "Samsung added a static mContext field to ActivityManager, holds a reference"
          + " to the activity."
          + " Observed here: https://github.com/square/leakcanary/issues/177 Fix in comment:"
          + " https://github.com/square/leakcanary/issues/177#issuecomment-222724283"
      ) {
        manufacturer == SAMSUNG && sdkInt in 22..23
      }
    }
  },

  STATIC_MTARGET_VIEW {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += staticFieldLeak(
        "android.widget.TextView", "mTargetView",
        description =
        "Samsung added a static mTargetView field to TextView which holds on to detached views."
      ) {
        manufacturer == SAMSUNG && sdkInt == 27
      }
    }
  },

  MULTI_WINDOW_DECOR_SUPPORT__MWINDOW {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "com.android.internal.policy.MultiWindowDecorSupport", "mWindow",
        description = """DecorView isn't leaking but its mDecorViewSupport field holds
            |a MultiWindowDecorSupport which has a mWindow field which holds a leaking PhoneWindow.
            |DecorView.mDecorViewSupport doesn't exist in AOSP.
            |Filed here: https://github.com/square/leakcanary/issues/1819
          """.trimMargin()
      ) {
        manufacturer == SAMSUNG && sdkInt in 26..29
      }
    }
  },

  // OTHER MANUFACTURERS

  GESTURE_BOOST_MANAGER {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += staticFieldLeak(
        "android.gestureboost.GestureBoostManager", "mContext",
        description =
        "GestureBoostManager is a static singleton that leaks an activity context." +
          " Fix: https://github.com/square/leakcanary/issues/696#issuecomment-296420756"
      ) {
        manufacturer == HUAWEI && sdkInt in 24..25
      }
    }
  },

  BUBBLE_POPUP_HELPER__SHELPER {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += staticFieldLeak(
        "android.widget.BubblePopupHelper", "sHelper",
        description =
        "A static helper for EditText bubble popups leaks a reference to the latest" + " focused view."
      ) {
        manufacturer == LG && sdkInt in 19..22
      }
    }
  },

  LGCONTEXT__MCONTEXT {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "com.lge.systemservice.core.LGContext", "mContext",
        description = "LGContext is a static singleton that leaks an activity context."
      ) {
        manufacturer == LG && sdkInt == 21
      }
    }
  },

  SMART_COVER_MANAGER {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "com.lge.systemservice.core.SmartCoverManager", "mContext",
        description = "SmartCoverManager\$CallbackRegister is a callback held by a native ref," +
          " and SmartCoverManager ends up leaking an activity context."
      ) {
        manufacturer == LG && sdkInt == 27
      }
    }
  },

  MAPPER_CLIENT {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "com.nvidia.ControllerMapper.MapperClient\$ServiceClient", "this$0",
        description =
        "Not sure exactly what ControllerMapper is about, but there is an anonymous"
          + " Handler in ControllerMapper.MapperClient.ServiceClient, which leaks"
          + " ControllerMapper.MapperClient which leaks the activity context."
      ) {
        manufacturer == NVIDIA && sdkInt == 19
      }
    }
  },

  SYSTEM_SENSOR_MANAGER__MAPPCONTEXTIMPL {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += staticFieldLeak(
        "android.hardware.SystemSensorManager", "mAppContextImpl",
        description =
        "SystemSensorManager stores a reference to context"
          + " in a static field in its constructor."
          + " Fix: use application context to get SensorManager"
      ) {
        (manufacturer == LENOVO && sdkInt == 19) || (manufacturer == VIVO && sdkInt == 22)
      }
    }
  },

  INSTRUMENTATION_RECOMMEND_ACTIVITY {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += staticFieldLeak(
        "android.app.Instrumentation", "mRecommendActivity",
        description =
        "Instrumentation would leak com.android.internal.app.RecommendActivity (in"
          + " framework.jar) in Meizu FlymeOS 4.5 and above, which is based on Android 5.0 and "
          + " above"
      ) {
        manufacturer == MEIZU && sdkInt in 21..22
      }
    }
  },

  DEVICE_POLICY_MANAGER__SETTINGS_OBSERVER {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.app.admin.DevicePolicyManager\$SettingsObserver", "this$0",
        description =
        "DevicePolicyManager keeps a reference to the context it has been created with"
          + " instead of extracting the application context. In this Motorola build,"
          + " DevicePolicyManager has an inner SettingsObserver class that is a content"
          + " observer, which is held into memory by a binder transport object."
      ) {
        manufacturer == MOTOROLA && sdkInt in 19..22
      }
    }
  },

  EXTENDED_STATUS_BAR_MANAGER {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += staticFieldLeak(
        "android.app.ExtendedStatusBarManager", "sInstance",
        description =
        """
            ExtendedStatusBarManager is held in a static sInstance field and has a mContext
            field which references a decor context which references a destroyed activity.
          """.trimIndent()
      ) {
        manufacturer == SHARP && sdkInt == 29
      }
    }
  },

  OEM_SCENE_CALL_BLOCKER {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += staticFieldLeak(
        "com.oneplus.util.OemSceneCallBlocker", "sContext",
        description =
        """
            OemSceneCallBlocker has a sContext static field which holds on to an activity instance.
          """.trimIndent()
      ) {
        manufacturer == ONE_PLUS && sdkInt == 28
      }
    }
  },

  RAZER_TEXT_KEY_LISTENER__MCONTEXT {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += instanceFieldLeak(
        "android.text.method.TextKeyListener", "mContext",
        description =
        """
            In AOSP, TextKeyListener instances are held in a TextKeyListener.sInstances static
            array. The Razer implementation added a mContext field, creating activity leaks.
          """.trimIndent()
      ) {
        manufacturer == RAZER && sdkInt == 28
      }
    }
  },

  // ######## Ignored references (not leaks) ########

  REFERENCES {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += ignoredInstanceField(WeakReference::class.java.name, "referent")
      references += ignoredInstanceField("leakcanary.KeyedWeakReference", "referent")
      references += ignoredInstanceField(SoftReference::class.java.name, "referent")
      references += ignoredInstanceField(PhantomReference::class.java.name, "referent")
      references += ignoredInstanceField("java.lang.ref.Finalizer", "prev")
      references += ignoredInstanceField("java.lang.ref.Finalizer", "element")
      references += ignoredInstanceField("java.lang.ref.Finalizer", "next")
      references += ignoredInstanceField("java.lang.ref.FinalizerReference", "prev")
      references += ignoredInstanceField("java.lang.ref.FinalizerReference", "element")
      references += ignoredInstanceField("java.lang.ref.FinalizerReference", "next")
      references += ignoredInstanceField("sun.misc.Cleaner", "prev")
      references += ignoredInstanceField("sun.misc.Cleaner", "next")
    }
  },

  FINALIZER_WATCHDOG_DAEMON {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      // If the FinalizerWatchdogDaemon thread is on the shortest path, then there was no other
      // reference to the object and it was about to be GCed.
      references += ignoredJavaLocal("FinalizerWatchdogDaemon")
    }
  },

  MAIN {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      // The main thread stack is ever changing so local variables aren't likely to hold references
      // for long. If this is on the shortest path, it's probably that there's a longer path with
      // a real leak.
      references += ignoredJavaLocal("main")
    }
  },

  LEAK_CANARY_THREAD {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      references += ignoredJavaLocal(LEAK_CANARY_THREAD_NAME)
    }
  },

  LEAK_CANARY_HEAP_DUMPER {
    override fun add(references: MutableList<ReferenceMatcher>) {
      // Holds on to the resumed activity (which is never destroyed), so this will not cause leaks
      // but may surface on the path when a resumed activity holds on to destroyed objects.
      // Would have a path that doesn't include LeakCanary instead.
      references += ignoredInstanceField("leakcanary.internal.AndroidHeapDumper", "resumedActivity")
    }
  },

  LEAK_CANARY_INTERNAL {
    override fun add(references: MutableList<ReferenceMatcher>) {
      references += ignoredInstanceField("leakcanary.internal.InternalLeakCanary", "application")
    }
  },

  EVENT_RECEIVER__MMESSAGE_QUEUE {
    override fun add(
      references: MutableList<ReferenceMatcher>
    ) {
      //  DisplayEventReceiver keeps a reference message queue object so that it is not GC'd while
      // the native peer of the receiver is using them.
      // The main thread message queue is held on by the main Looper, but that might be a longer
      // path. Let's not confuse people with a shorter path that is less meaningful.
      references += ignoredInstanceField(
        "android.view.Choreographer\$FrameDisplayEventReceiver", "mMessageQueue"
      )
    }
  },

  ;

  internal abstract fun add(references: MutableList<ReferenceMatcher>)

  companion object {
    private const val LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump"
    const val SAMSUNG = "samsung"
    const val MOTOROLA = "motorola"
    const val LENOVO = "LENOVO"
    const val LG = "LGE"
    const val NVIDIA = "NVIDIA"
    const val MEIZU = "Meizu"
    const val ONE_PLUS = "OnePlus"
    const val HUAWEI = "HUAWEI"
    const val VIVO = "vivo"
    const val RAZER = "Razer"
    const val SHARP = "SHARP"

    /**
     * Returns a list of [ReferenceMatcher] that only contains [IgnoredReferenceMatcher] and no
     * [LibraryLeakReferenceMatcher].
     */
    @JvmStatic
    val ignoredReferencesOnly: List<ReferenceMatcher>
      get() = buildKnownReferences(
        EnumSet.of(
          REFERENCES,
          FINALIZER_WATCHDOG_DAEMON,
          MAIN,
          LEAK_CANARY_THREAD,
          EVENT_RECEIVER__MMESSAGE_QUEUE
        )
      )

    /**
     * @see [AndroidReferenceMatchers]
     */
    @JvmStatic
    val appDefaults: List<ReferenceMatcher>
      get() = buildKnownReferences(EnumSet.allOf(AndroidReferenceMatchers::class.java))

    /**
     * Builds a list of [ReferenceMatcher] from the [referenceMatchers] set of
     * [AndroidReferenceMatchers].
     */
    @JvmStatic
    fun buildKnownReferences(referenceMatchers: Set<AndroidReferenceMatchers>): List<ReferenceMatcher> {
      val resultSet = mutableListOf<ReferenceMatcher>()
      referenceMatchers.forEach {
        it.add(resultSet)
      }
      return resultSet
    }

    private val ALWAYS: AndroidBuildMirror.() -> Boolean = {
      true
    }

    /**
     * Creates a [LibraryLeakReferenceMatcher] that matches a [StaticFieldPattern].
     * [description] should convey what we know about this library leak.
     */
    @JvmStatic
    fun staticFieldLeak(
      className: String,
      fieldName: String,
      description: String = "",
      patternApplies: AndroidBuildMirror.() -> Boolean = ALWAYS
    ): LibraryLeakReferenceMatcher {
      return libraryLeak(StaticFieldPattern(className, fieldName), description, patternApplies)
    }

    /**
     * Creates a [LibraryLeakReferenceMatcher] that matches a [InstanceFieldPattern].
     * [description] should convey what we know about this library leak.
     */
    @JvmStatic
    fun instanceFieldLeak(
      className: String,
      fieldName: String,
      description: String = "",
      patternApplies: AndroidBuildMirror.() -> Boolean = ALWAYS
    ): LibraryLeakReferenceMatcher {
      return libraryLeak(InstanceFieldPattern(className, fieldName), description, patternApplies)
    }

    @JvmStatic
    fun nativeGlobalVariableLeak(
      className: String,
      description: String = "",
      patternApplies: AndroidBuildMirror.() -> Boolean = ALWAYS
    ): LibraryLeakReferenceMatcher {
      return libraryLeak(NativeGlobalVariablePattern(className), description, patternApplies)
    }

    private fun libraryLeak(
      referencePattern: ReferencePattern,
      description: String,
      patternApplies: AndroidBuildMirror.() -> Boolean
    ): LibraryLeakReferenceMatcher {
      return LibraryLeakReferenceMatcher(
        pattern = referencePattern,
        description = description,
        patternApplies = { graph ->
          AndroidBuildMirror.fromHeapGraph(graph)
            .patternApplies()
        }
      )
    }

    /**
     * Creates a [IgnoredReferenceMatcher] that matches a [InstanceFieldPattern].
     */
    @JvmStatic
    fun ignoredInstanceField(
      className: String,
      fieldName: String
    ): IgnoredReferenceMatcher {
      return IgnoredReferenceMatcher(pattern = InstanceFieldPattern(className, fieldName))
    }

    /**
     * Creates a [IgnoredReferenceMatcher] that matches a [JavaLocalPattern].
     */
    @JvmStatic
    fun ignoredJavaLocal(
      threadName: String
    ): IgnoredReferenceMatcher {
      return IgnoredReferenceMatcher(pattern = JavaLocalPattern(threadName))
    }
  }
}

package shark

import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.IntArrayDump

class AndroidResourceIdNames private constructor(
  private val resourceIds: IntArray,
  private val names: Array<String>
) {

  operator fun get(id: Int): String? {
    val indexOfId = resourceIds.binarySearch(id)
    return if (indexOfId >= 0) {
      names[indexOfId]
    } else {
      null
    }
  }

  companion object {

    internal const val FIRST_APP_RESOURCE_ID = 0x7F010000
    internal const val RESOURCE_ID_TYPE_ITERATOR = 0x00010000

    @Volatile
    @JvmStatic
    private var holderField: AndroidResourceIdNames? = null

    /**
     * @param getResourceTypeName a function that delegates to Android
     * Resources.getResourceTypeName but returns null when the name isn't found instead of
     * throwing an exception.
     *
     * @param getResourceEntryName a function that delegates to Android
     * Resources.getResourceEntryName but returns null when the name isn't found instead of
     * throwing an exception.
     */
    @Synchronized fun saveToMemory(
      getResourceTypeName: (Int) -> String?,
      getResourceEntryName: (Int) -> String?
    ) {
      if (holderField != null) {
        return
      }

      // This is based on https://jebware.com/blog/?p=600 which itself is based on
      // https://stackoverflow.com/a/6646113/703646

      val idToNamePairs = mutableListOf<Pair<Int, String>>()
      findIdTypeResourceIdStart(getResourceTypeName)?.let { idTypeResourceIdStart ->
        var resourceId = idTypeResourceIdStart
        while (true) {
          val entry = getResourceEntryName(resourceId) ?: break
          idToNamePairs += resourceId to entry
          resourceId++
        }
      }
      val resourceIds = idToNamePairs.map { it.first }
        .toIntArray()
      val names = idToNamePairs.map { it.second }
        .toTypedArray()
      holderField = AndroidResourceIdNames(resourceIds, names)
    }

    private fun findIdTypeResourceIdStart(getResourceTypeName: (Int) -> String?): Int? {
      var resourceTypeId = FIRST_APP_RESOURCE_ID
      while (true) {
        when (getResourceTypeName(resourceTypeId)) {
          null -> return null
          "id" -> return resourceTypeId
          else -> resourceTypeId += RESOURCE_ID_TYPE_ITERATOR
        }
      }
    }

    fun readFromHeap(graph: HeapGraph): AndroidResourceIdNames? {
      return graph.context.getOrPut(AndroidResourceIdNames::class.java.name) {
        val className = AndroidResourceIdNames::class.java.name
        val holderClass = graph.findClassByName(className)
        holderClass?.let {
          val holderField = holderClass["holderField"]!!
          holderField.valueAsInstance?.let { instance ->
            val resourceIdsField = instance[className, "resourceIds"]!!
            val resourceIdsArray = resourceIdsField.valueAsPrimitiveArray!!
            val resourceIds =
              (resourceIdsArray.readRecord() as IntArrayDump).array
            val names = instance[className, "names"]!!.valueAsObjectArray!!.readElements()
              .map { it.readAsJavaString()!! }
              .toList()
              .toTypedArray()
            AndroidResourceIdNames(resourceIds, names)
          }
        }
      }
    }

    internal fun resetForTests() {
      holderField = null
    }
  }
}

package shark

object AndroidServices {
  val HeapGraph.aliveAndroidServiceObjectIds: List<Long>
    get() {
      return context.getOrPut(AndroidServices::class.java.name) {
        val activityThreadClass = findClassByName("android.app.ActivityThread")!!
        val currentActivityThread = activityThreadClass
          .readStaticField("sCurrentActivityThread")!!
          .valueAsInstance!!

        val mServices = currentActivityThread["android.app.ActivityThread", "mServices"]!!
          .valueAsInstance!!

        val servicesArray = mServices["android.util.ArrayMap", "mArray"]!!.valueAsObjectArray!!

        servicesArray.readElements()
          .filterIndexed { index, heapValue ->
            // ArrayMap<IBinder, Service>
            // even: key, odd: value
            index % 2 == 1
              && heapValue.isNonNullReference
          }
          .map { it.asNonNullObjectId!! }
          .toList()
      }
    }
}

package shark

import com.github.ajalt.clikt.core.CliktCommand
import shark.SharkCliCommand.Companion.echo
import shark.SharkCliCommand.Companion.retrieveHeapDumpFile
import shark.SharkCliCommand.Companion.sharkCliParams
import java.io.File

class AnalyzeCommand : CliktCommand(
  name = "analyze",
  help = "Analyze a heap dump."
) {

  override fun run() {
    val params = context.sharkCliParams
    analyze(retrieveHeapDumpFile(params), params.obfuscationMappingPath)
  }

  companion object {
    fun CliktCommand.analyze(
      heapDumpFile: File,
      proguardMappingFile: File?
    ) {
      val proguardMapping = proguardMappingFile?.let {
        ProguardMappingReader(it.inputStream()).readProguardMapping()
      }
      val objectInspectors = AndroidObjectInspectors.appDefaults.toMutableList()

      val listener = OnAnalysisProgressListener { step ->
        SharkLog.d { "Analysis in progress, working on: ${step.name}" }
      }

      val heapAnalyzer = HeapAnalyzer(listener)
      SharkLog.d { "Analyzing heap dump $heapDumpFile" }

      val heapAnalysis = heapAnalyzer.analyze(
        heapDumpFile = heapDumpFile,
        leakingObjectFinder = FilteringLeakingObjectFinder(
          AndroidObjectInspectors.appLeakingObjectFilters
        ),
        referenceMatchers = AndroidReferenceMatchers.appDefaults,
        computeRetainedHeapSize = true,
        objectInspectors = objectInspectors,
        proguardMapping = proguardMapping
      )
      echo(heapAnalysis)
    }
  }
}

package shark

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.PrintMessage
import shark.SharkCliCommand.Companion.retrieveHeapDumpFile
import shark.SharkCliCommand.Companion.sharkCliParams

class DeobfuscateHprofCommand : CliktCommand(
  name = "deobfuscate-hprof",
  help = "Deobfuscate the provided heap dump and generate a new \"-deobfuscated.hprof\" file."
) {

  override fun run() {
    val params = context.sharkCliParams
    val obfuscationMappingFile = params.obfuscationMappingPath
      ?: throw PrintMessage("Error: Missing obfuscation mapping file")
    val heapDumpFile = retrieveHeapDumpFile(params)
    SharkLog.d { "Deobfuscating heap dump $heapDumpFile" }
    val proguardMapping =
      ProguardMappingReader(obfuscationMappingFile.inputStream()).readProguardMapping()
    val deobfuscator = HprofDeobfuscator()
    val outputFile = deobfuscator.deobfuscate(proguardMapping, heapDumpFile)
    echo("Created deobfuscated hprof to $outputFile")
  }
}

package shark

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.PrintMessage
import com.github.ajalt.clikt.core.UsageError
import shark.SharkCliCommand.Companion.echo
import shark.SharkCliCommand.Companion.retrieveHeapDumpFile
import shark.SharkCliCommand.Companion.runCommand
import shark.SharkCliCommand.Companion.sharkCliParams
import shark.SharkCliCommand.HeapDumpSource.ProcessSource
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class DumpProcessCommand : CliktCommand(
  name = "dump-process",
  help = "Dump the heap and pull the hprof file."
) {

  override fun run() {
    val params = context.sharkCliParams
    if (params.source !is ProcessSource) {
      throw UsageError("dump-process must be used with --process")
    }
    val file = retrieveHeapDumpFile(params)
    echo("Pulled heap dump to $file")
  }

  companion object {

    private val SPACE_PATTERN = Regex("\\s+")

    @Suppress("ThrowsCount")
    fun CliktCommand.dumpHeap(
      processNameParam: String,
      maybeDeviceId: String?
    ): File {
      val workingDirectory = File(System.getProperty("user.dir"))

      val deviceList = runCommand(workingDirectory, "adb", "devices")

      val connectedDevices = deviceList.lines()
        .drop(1)
        .filter { it.isNotBlank() }
        .map { SPACE_PATTERN.split(it)[0] }

      val deviceId = if (connectedDevices.isEmpty()) {
        throw PrintMessage("Error: No device connected to adb")
      } else if (maybeDeviceId == null) {
        if (connectedDevices.size == 1) {
          connectedDevices[0]
        } else {
          throw PrintMessage(
            "Error: more than one device/emulator connected to adb," +
              " use '--device ID' argument with one of $connectedDevices"
          )
        }
      } else {
        if (maybeDeviceId in connectedDevices) {
          maybeDeviceId
        } else {
          throw PrintMessage(
            "Error: device '$maybeDeviceId' not in the list of connected devices $connectedDevices"
          )
        }
      }

      val processList = runCommand(workingDirectory, "adb", "-s", deviceId, "shell", "ps")

      val matchingProcesses = processList.lines()
        .filter { it.contains(processNameParam) }
        .map {
          val columns = SPACE_PATTERN.split(it)
          columns[8] to columns[1]
        }

      val (processName, processId) = when {
        matchingProcesses.size == 1 -> {
          matchingProcesses[0]
        }
        matchingProcesses.isEmpty() -> {
          throw PrintMessage("Error: No process matching \"$processNameParam\"")
        }
        else -> {
          matchingProcesses.firstOrNull { it.first == processNameParam }
            ?: throw PrintMessage(
              "Error: More than one process matches \"$processNameParam\" but none matches exactly: ${matchingProcesses.map { it.first }}"
            )
        }
      }

      val heapDumpFileName =
        SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS'-$processName.hprof'", Locale.US).format(
          Date()
        )

      val heapDumpDevicePath = "/data/local/tmp/$heapDumpFileName"

      echo(
        "Dumping heap on $deviceId for process \"$processName\" with pid $processId to $heapDumpDevicePath"
      )

      runCommand(
        workingDirectory, "adb", "-s", deviceId, "shell", "am", "dumpheap", processId,
        heapDumpDevicePath
      )

      // Dump heap takes time but adb returns immediately.
      Thread.sleep(5000)

      SharkLog.d { "Pulling $heapDumpDevicePath" }

      val pullResult =
        runCommand(workingDirectory, "adb", "-s", deviceId, "pull", heapDumpDevicePath)
      SharkLog.d { pullResult }
      SharkLog.d { "Removing $heapDumpDevicePath" }

      runCommand(workingDirectory, "adb", "-s", deviceId, "shell", "rm", heapDumpDevicePath)

      val heapDumpFile = File(workingDirectory, heapDumpFileName)
      SharkLog.d { "Pulled heap dump to $heapDumpFile" }

      return heapDumpFile
    }
  }
}

package shark

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.PrintMessage
import jline.console.ConsoleReader
import jline.console.UserInterruptException
import jline.console.completer.CandidateListCompletionHandler
import jline.console.completer.StringsCompleter
import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import shark.HeapObject.HeapObjectArray
import shark.HeapObject.HeapPrimitiveArray
import shark.HprofHeapGraph.Companion.openHeapGraph
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.BooleanArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ByteArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.CharArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.DoubleArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.FloatArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.IntArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.LongArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ShortArrayDump
import shark.InteractiveCommand.COMMAND.ANALYZE
import shark.InteractiveCommand.COMMAND.ARRAY
import shark.InteractiveCommand.COMMAND.CLASS
import shark.InteractiveCommand.COMMAND.Companion.matchesCommand
import shark.InteractiveCommand.COMMAND.DETAILED_PATH_TO_INSTANCE
import shark.InteractiveCommand.COMMAND.EXIT
import shark.InteractiveCommand.COMMAND.HELP
import shark.InteractiveCommand.COMMAND.INSTANCE
import shark.InteractiveCommand.COMMAND.PATH_TO_INSTANCE
import shark.SharkCliCommand.Companion.echoNewline
import shark.SharkCliCommand.Companion.retrieveHeapDumpFile
import shark.SharkCliCommand.Companion.sharkCliParams
import shark.ValueHolder.BooleanHolder
import shark.ValueHolder.ByteHolder
import shark.ValueHolder.CharHolder
import shark.ValueHolder.DoubleHolder
import shark.ValueHolder.FloatHolder
import shark.ValueHolder.IntHolder
import shark.ValueHolder.LongHolder
import shark.ValueHolder.ReferenceHolder
import shark.ValueHolder.ShortHolder
import java.io.File
import java.util.Locale

class InteractiveCommand : CliktCommand(
  name = "interactive",
  help = "Explore a heap dump."
) {

  enum class COMMAND(
    val commandName: String,
    val suffix: String = "",
    val help: String
  ) {
    ANALYZE(
      commandName = "analyze",
      help = "Analyze the heap dump."
    ),
    CLASS(
      commandName = "class",
      suffix = "NAME@ID",
      help = "Show class with a matching NAME and Object ID."
    ),
    INSTANCE(
      commandName = "instance",
      suffix = "CLASS_NAME@ID",
      help = "Show instance with a matching CLASS_NAME and Object ID."
    ),
    ARRAY(
      commandName = "array",
      suffix = "CLASS_NAME@ID",
      help = "Show array instance with a matching CLASS_NAME and Object ID."
    ),
    PATH_TO_INSTANCE(
      commandName = "->instance",
      suffix = "CLASS_NAME@ID",
      help = "Show path from GC Roots to instance."
    ),
    DETAILED_PATH_TO_INSTANCE(
      commandName = "~>instance",
      suffix = "CLASS_NAME@ID",
      help = "Show path from GC Roots to instance, highlighting suspect references."
    ),
    HELP(
      commandName = "help",
      help = "Show this message."
    ),
    EXIT(
      commandName = "exit",
      help = "Exit this interactive prompt."
    ),
    ;

    val pattern: String
      get() = if (suffix.isEmpty()) commandName else "$commandName "

    val patternHelp: String
      get() = pattern + suffix

    override fun toString() = commandName

    companion object {
      infix fun String.matchesCommand(command: COMMAND): Boolean {
        return if (command.suffix.isEmpty()) {
          this == command.commandName
        } else {
          startsWith(command.pattern)
        }
      }
    }
  }

  override fun run() {
    openHprof { graph, heapDumpFile ->
      val console = setupConsole(graph)
      var exit = false
      while (!exit) {
        val input = console.readCommand()
        exit = handleCommand(input, heapDumpFile, graph)
        echoNewline()
      }
    }
  }

  private fun openHprof(block: (HeapGraph, File) -> Unit) {
    val params = context.sharkCliParams
    val heapDumpFile = retrieveHeapDumpFile(params)
    val obfuscationMappingPath = params.obfuscationMappingPath

    val proguardMapping = obfuscationMappingPath?.let {
      ProguardMappingReader(it.inputStream()).readProguardMapping()
    }

    heapDumpFile.openHeapGraph().use { graph ->
      block(graph, heapDumpFile)
    }
  }

  private fun setupConsole(graph: HeapGraph): ConsoleReader {
    val console = ConsoleReader()
    console.handleUserInterrupt = true

    console.addCompleter(StringsCompleter(COMMAND.values().map { it.pattern }))
    console.addCompleter { buffer, _, candidates ->
      if (buffer != null) {
        when {
          buffer matchesCommand CLASS -> {
            val matchingObjects = findMatchingObjects(buffer, graph.classes) {
              it.name
            }
            candidates.addAll(matchingObjects.map { renderHeapObject(it) })
          }
          buffer matchesCommand INSTANCE -> {
            val matchingObjects = findMatchingObjects(buffer, graph.instances) {
              it.instanceClassSimpleName
            }
            candidates.addAll(matchingObjects.map { renderHeapObject(it) })
          }
          buffer matchesCommand PATH_TO_INSTANCE -> {
            val matchingObjects = findMatchingObjects(buffer, graph.instances) {
              it.instanceClassSimpleName
            }
            candidates.addAll(matchingObjects.map { "->${renderHeapObject(it)}" })
          }
          buffer matchesCommand DETAILED_PATH_TO_INSTANCE -> {
            val matchingObjects = findMatchingObjects(buffer, graph.instances) {
              it.instanceClassSimpleName
            }
            candidates.addAll(matchingObjects.map { "~>${renderHeapObject(it)}" })
          }
          buffer matchesCommand ARRAY -> {
            val matchingObjects =
              findMatchingObjects(buffer, graph.primitiveArrays + graph.objectArrays) {
                if (it is HeapPrimitiveArray) {
                  it.arrayClassName
                } else {
                  (it as HeapObjectArray).arrayClassSimpleName
                }
              }
            candidates.addAll(matchingObjects.map { renderHeapObject(it) })
          }
        }
      }
      if (candidates.isEmpty()) -1 else 0
    }
    val completionHandler = CandidateListCompletionHandler()
    completionHandler.printSpaceAfterFullCompletion = false
    console.completionHandler = completionHandler
    console.prompt = "Enter command [help]:\n"
    return console
  }

  private fun ConsoleReader.readCommand(): String? {
    val input = try {
      readLine()
    } catch (ignored: UserInterruptException) {
      throw PrintMessage("Program interrupted by user")
    }
    echoNewline()
    return input
  }

  private fun handleCommand(
    input: String?,
    heapDumpFile: File,
    graph: HeapGraph
  ): Boolean {
    when {
      input == null -> throw PrintMessage("End Of File was encountered")
      input.isBlank() || input matchesCommand HELP -> echoHelp()
      input matchesCommand EXIT -> return true
      input matchesCommand ANALYZE -> analyze(heapDumpFile, graph)
      input matchesCommand PATH_TO_INSTANCE -> {
        analyzeMatchingObjects(heapDumpFile, input, graph.instances, false) {
          it.instanceClassSimpleName
        }
      }
      input matchesCommand DETAILED_PATH_TO_INSTANCE -> {
        analyzeMatchingObjects(heapDumpFile, input, graph.instances, true) {
          it.instanceClassSimpleName
        }
      }
      input matchesCommand CLASS -> {
        renderMatchingObjects(input, graph.classes) {
          it.name
        }
      }
      input matchesCommand INSTANCE -> {
        renderMatchingObjects(input, graph.instances) {
          it.instanceClassSimpleName
        }
      }
      input matchesCommand ARRAY -> {
        renderMatchingObjects(input, graph.primitiveArrays + graph.objectArrays) {
          if (it is HeapPrimitiveArray) {
            it.arrayClassName
          } else {
            (it as HeapObjectArray).arrayClassSimpleName
          }
        }
      }
      else -> {
        echo("Unknown command [$input].\n")
        echoHelp()
      }
    }
    return false
  }

  private fun echoHelp() {
    echo("Available commands:")
    val longestPatternHelp = COMMAND.values()
      .map { it.patternHelp }.maxBy { it.length }!!.length
    COMMAND.values()
      .forEach { command ->
        val patternHelp = command.patternHelp
        val extraSpaceCount = (longestPatternHelp - patternHelp.length)
        val extraSpaces = " ".repeat(extraSpaceCount)
        println("  $patternHelp$extraSpaces  ${command.help}")
      }
  }

  private fun <T : HeapObject> renderMatchingObjects(
    pattern: String,
    objects: Sequence<T>,
    namer: (T) -> String
  ) {
    val matchingObjects = findMatchingObjects(pattern, objects, namer)
    when {
      matchingObjects.size == 1 -> {
        matchingObjects.first()
          .show()
      }
      matchingObjects.isNotEmpty() -> {
        matchingObjects.forEach { heapObject ->
          echo(renderHeapObject(heapObject))
        }
      }
      else -> {
        echo("No object found matching [$pattern]")
      }
    }
  }

  private fun <T : HeapObject> analyzeMatchingObjects(
    heapDumpFile: File,
    pattern: String,
    objects: Sequence<T>,
    showDetails: Boolean,
    namer: (T) -> String
  ) {
    val matchingObjects = findMatchingObjects(pattern, objects, namer)
    when {
      matchingObjects.size == 1 -> {
        val heapObject = matchingObjects.first()
        analyze(heapDumpFile, heapObject.graph, showDetails, heapObject.objectId)
      }
      matchingObjects.isNotEmpty() -> {
        matchingObjects.forEach { heapObject ->
          echo(if (showDetails) "~>" else "->" + renderHeapObject(heapObject))
        }
      }
      else -> {
        echo("No object found matching [$pattern]")
      }
    }
  }

  private fun <T : HeapObject> findMatchingObjects(
    pattern: String,
    objects: Sequence<T>,
    namer: (T) -> String
  ): List<T> {
    val firstSpaceIndex = pattern.indexOf(' ')
    val contentStartIndex = firstSpaceIndex + 1
    val nextSpaceIndex = pattern.indexOf(' ', contentStartIndex)
    val endIndex = if (nextSpaceIndex != -1) nextSpaceIndex else pattern.length
    val content = pattern.substring(contentStartIndex, endIndex)
    val identifierIndex = content.indexOf('@')
    val (classNamePart, objectIdStart) = if (identifierIndex == -1) {
      content to null
    } else {
      content.substring(0, identifierIndex) to
        content.substring(identifierIndex + 1)
    }

    val objectId = objectIdStart?.toLongOrNull()
    val checkObjectId = objectId != null
    val matchingObjects = objects
      .filter {
        classNamePart in namer(it) &&
          (!checkObjectId ||
            it.objectId.toString().startsWith(objectIdStart!!))
      }
      .toList()

    if (objectIdStart != null) {
      val exactMatchingByObjectId = matchingObjects.firstOrNull { objectId == it.objectId }
      if (exactMatchingByObjectId != null) {
        return listOf(exactMatchingByObjectId)
      }
    }

    val exactMatchingByName = matchingObjects.filter { classNamePart == namer(it) }

    return if (exactMatchingByName.isNotEmpty()) {
      exactMatchingByName
    } else {
      matchingObjects
    }
  }

  private fun HeapObject.show() {
    when (this) {
      is HeapInstance -> showInstance()
      is HeapClass -> showClass()
      is HeapObjectArray -> showObjectArray()
      is HeapPrimitiveArray -> showPrimitiveArray()
    }
  }

  private fun HeapInstance.showInstance() {
    echo(renderHeapObject(this))
    echo("  Instance of ${renderHeapObject(instanceClass)}")

    val fieldsPerClass = readFields()
      .toList()
      .groupBy { it.declaringClass }
      .toList()
      .filter { it.first.name != "java.lang.Object" }
      .reversed()

    fieldsPerClass.forEach { (heapClass, fields) ->
      echo("  Fields from ${renderHeapObject(heapClass)}")
      fields.forEach { field ->
        echo("    ${field.name} = ${renderHeapValue(field.value)}")
      }
    }
  }

  private fun HeapClass.showClass() {
    echo(this@InteractiveCommand.renderHeapObject(this))
    val superclass = superclass
    if (superclass != null) {
      echo("  Extends ${renderHeapObject(superclass)}")
    }

    val staticFields = readStaticFields()
      .filter { field ->
        !field.name.startsWith(
          "\$class\$"
        ) && field.name != "\$classOverhead"
      }
      .toList()
    if (staticFields.isNotEmpty()) {
      echo("  Static fields")
      staticFields
        .forEach { field ->
          echo("    static ${field.name} = ${renderHeapValue(field.value)}")
        }
    }

    val instances = when {
      isPrimitiveArrayClass -> primitiveArrayInstances
      isObjectArrayClass -> objectArrayInstances
      else -> instances
    }.toList()
    if (instances.isNotEmpty()) {
      echo("  ${instances.size} instance" + if (instances.size != 1) "s" else "")
      instances.forEach { arrayOrInstance ->
        echo("    ${renderHeapObject(arrayOrInstance)}")
      }
    }
  }

  private fun HeapObjectArray.showObjectArray() {
    val elements = readElements()
    echo(renderHeapObject(this))
    echo("  Instance of ${renderHeapObject(arrayClass)}")
    var repeatedValue: HeapValue? = null
    var repeatStartIndex = 0
    var lastIndex = 0
    elements.forEachIndexed { index, element ->
      lastIndex = index
      if (repeatedValue == null) {
        repeatedValue = element
        repeatStartIndex = index
      } else if (repeatedValue != element) {
        val repeatEndIndex = index - 1
        if (repeatStartIndex == repeatEndIndex) {
          echo("  $repeatStartIndex = ${renderHeapValue(repeatedValue!!)}")
        } else {
          echo("  $repeatStartIndex..$repeatEndIndex = ${renderHeapValue(repeatedValue!!)}")
        }
        repeatedValue = element
        repeatStartIndex = index
      }
    }
    if (repeatedValue != null) {
      if (repeatStartIndex == lastIndex) {
        echo("  $repeatStartIndex = ${renderHeapValue(repeatedValue!!)}")
      } else {
        echo("  $repeatStartIndex..$lastIndex = ${renderHeapValue(repeatedValue!!)}")
      }
    }
  }

  private fun HeapPrimitiveArray.showPrimitiveArray() {
    val record = readRecord()
    echo(renderHeapObject(this))
    echo("  Instance of ${renderHeapObject(arrayClass)}")

    var repeatedValue: Any? = null
    var repeatStartIndex = 0
    var lastIndex = 0
    val action: (Int, Any) -> Unit = { index, value ->
      lastIndex = index
      if (repeatedValue == null) {
        repeatedValue = value
        repeatStartIndex = index
      } else if (repeatedValue != value) {
        val repeatEndIndex = index - 1
        if (repeatStartIndex == repeatEndIndex) {
          echo("  $repeatStartIndex = $repeatedValue")
        } else {
          echo("  $repeatStartIndex..$repeatEndIndex = $repeatedValue")
        }
        repeatedValue = value
        repeatStartIndex = index
      }
    }

    when (record) {
      is BooleanArrayDump -> record.array.forEachIndexed(action)
      is CharArrayDump -> record.array.forEachIndexed(action)
      is FloatArrayDump -> record.array.forEachIndexed(action)
      is DoubleArrayDump -> record.array.forEachIndexed(action)
      is ByteArrayDump -> record.array.forEachIndexed(action)
      is ShortArrayDump -> record.array.forEachIndexed(action)
      is IntArrayDump -> record.array.forEachIndexed(action)
      is LongArrayDump -> record.array.forEachIndexed(action)
    }
    if (repeatedValue != null) {
      if (repeatStartIndex == lastIndex) {
        echo("  $repeatStartIndex = $repeatedValue")
      } else {
        echo("  $repeatStartIndex..$lastIndex = $repeatedValue")
      }
    }
  }

  private fun renderHeapValue(heapValue: HeapValue): String {
    return when (val holder = heapValue.holder) {
      is ReferenceHolder -> {
        when {
          holder.isNull -> "null"
          !heapValue.graph.objectExists(holder.value) -> "@${holder.value} object not found"
          else -> {
            val heapObject = heapValue.asObject!!
            renderHeapObject(heapObject)
          }
        }
      }
      is BooleanHolder -> holder.value.toString()
      is CharHolder -> holder.value.toString()
      is FloatHolder -> holder.value.toString()
      is DoubleHolder -> holder.value.toString()
      is ByteHolder -> holder.value.toString()
      is ShortHolder -> holder.value.toString()
      is IntHolder -> holder.value.toString()
      is LongHolder -> holder.value.toString()
    }
  }

  private fun renderHeapObject(heapObject: HeapObject): String {
    return when (heapObject) {
      is HeapClass -> {
        val instanceCount = when {
          heapObject.isPrimitiveArrayClass -> heapObject.primitiveArrayInstances
          heapObject.isObjectArrayClass -> heapObject.objectArrayInstances
          else -> heapObject.instances
        }.count()
        val plural = if (instanceCount != 1) "s" else ""
        "$CLASS ${heapObject.name}@${heapObject.objectId} (${instanceCount} instance$plural)"
      }
      is HeapInstance -> {
        val asJavaString = heapObject.readAsJavaString()

        val value =
          if (asJavaString != null) {
            " \"${asJavaString}\""
          } else ""

        "$INSTANCE ${heapObject.instanceClassSimpleName}@${heapObject.objectId}$value"
      }
      is HeapObjectArray -> {
        val className = heapObject.arrayClassSimpleName.removeSuffix("[]")
        "$ARRAY $className[${heapObject.readElements().count()}]@${heapObject.objectId}"
      }
      is HeapPrimitiveArray -> {
        val record = heapObject.readRecord()
        val primitiveName = heapObject.primitiveType.name.toLowerCase(Locale.US)
        "$ARRAY $primitiveName[${record.size}]@${heapObject.objectId}"
      }
    }
  }

  private fun analyze(
    heapDumpFile: File,
    graph: HeapGraph,
    showDetails: Boolean = true,
    leakingObjectId: Long? = null
  ) {
    if (leakingObjectId != null) {
      if (!graph.objectExists(leakingObjectId)) {
        echo("@$leakingObjectId not found")
        return
      } else {
        val heapObject = graph.findObjectById(leakingObjectId)
        if (heapObject !is HeapInstance) {
          echo("${renderHeapObject(heapObject)} is not an instance")
          return
        }
      }
    }

    val objectInspectors =
      if (showDetails) AndroidObjectInspectors.appDefaults.toMutableList() else mutableListOf()

    objectInspectors += ObjectInspector {
      it.labels += renderHeapObject(it.heapObject)
    }

    val leakingObjectFinder = if (leakingObjectId == null) {
      FilteringLeakingObjectFinder(
        AndroidObjectInspectors.appLeakingObjectFilters
      )
    } else {
      LeakingObjectFinder {
        setOf(leakingObjectId)
      }
    }

    val listener = OnAnalysisProgressListener { step ->
      SharkLog.d { "Analysis in progress, working on: ${step.name}" }
    }

    val heapAnalyzer = HeapAnalyzer(listener)
    SharkLog.d { "Analyzing heap dump $heapDumpFile" }

    val heapAnalysis = heapAnalyzer.analyze(
      heapDumpFile = heapDumpFile,
      graph = graph,
      leakingObjectFinder = leakingObjectFinder,
      referenceMatchers = AndroidReferenceMatchers.appDefaults,
      computeRetainedHeapSize = true,
      objectInspectors = objectInspectors
    )

    if (leakingObjectId == null || heapAnalysis is HeapAnalysisFailure) {
      echo(heapAnalysis)
    } else {
      val leakTrace = (heapAnalysis as HeapAnalysisSuccess).allLeaks.first()
        .leakTraces.first()
      echo(if (showDetails) leakTrace else leakTrace.toSimplePathString())
    }
  }
}

package shark

import com.github.ajalt.clikt.core.subcommands

fun main(args: Array<String>) =
  SharkCliCommand().subcommands(
    InteractiveCommand(),
    AnalyzeCommand(),
    DumpProcessCommand(),
    StripHprofCommand(),
    DeobfuscateHprofCommand()
  ).main(args)

package shark

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.CliktError
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.UsageError
import com.github.ajalt.clikt.output.TermUi
import com.github.ajalt.clikt.parameters.groups.OptionGroup
import com.github.ajalt.clikt.parameters.groups.cooccurring
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.options.versionOption
import com.github.ajalt.clikt.parameters.types.file
import shark.DumpProcessCommand.Companion.dumpHeap
import shark.SharkCliCommand.HeapDumpSource.HprofFileSource
import shark.SharkCliCommand.HeapDumpSource.ProcessSource
import shark.SharkLog.Logger
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.util.Properties
import java.util.concurrent.TimeUnit.SECONDS

class SharkCliCommand : CliktCommand(
  name = "shark-cli",
  // This ASCII art is a remix of a shark from -David "TAZ" Baltazar- and chick from jgs.
  help = """
    |Version: $versionName
    |
    |```
    |$S                ^`.                 .=""=.
    |$S^_              \  \               / _  _ \
    |$S\ \             {   \             |  d  b  |
    |$S{  \           /     `~~~--__     \   /\   /
    |$S{   \___----~~'              `~~-_/'-=\/=-'\,
    |$S \                         /// a  `~.      \ \
    |$S / /~~~~-, ,__.    ,      ///  __,,,,)      \ |
    |$S \/      \/    `~~~;   ,---~~-_`/ \        / \/
    |$S                  /   /            '.    .'
    |$S                 '._.'             _|`~~`|_
    |$S                                   /|\  /|\
    |```
    """.trimMargin()
) {

  private class ProcessOptions : OptionGroup() {
    val processName by option(
      "--process", "-p",
      help = "Full or partial name of a process, e.g. \"example\" would match \"com.example.app\""
    ).required()

    val device by option(
      "-d", "--device", metavar = "ID", help = "device/emulator id"
    )
  }

  private val processOptions by ProcessOptions().cooccurring()

  private val obfuscationMappingPath by option(
    "-m", "--obfuscation-mapping", help = "path to obfuscation mapping file"
  ).file()

  private val verbose by option(
    help = "provide additional details as to what shark-cli is doing"
  ).flag("--no-verbose")

  private val heapDumpFile by option("--hprof", "-h", help = "path to a .hprof file").file(
    exists = true,
    folderOkay = false,
    readable = true
  )

  init {
    versionOption(versionName)
  }

  class CommandParams(
    val source: HeapDumpSource,
    val obfuscationMappingPath: File?
  )

  sealed class HeapDumpSource {
    class HprofFileSource(val file: File) : HeapDumpSource()
    class ProcessSource(
      val processName: String,
      val deviceId: String?
    ) : HeapDumpSource()
  }

  override fun run() {
    if (verbose) {
      setupVerboseLogger()
    }
    if (processOptions != null && heapDumpFile != null) {
      throw UsageError("Option --process cannot be used with --hprof")
    } else if (processOptions != null) {
      context.sharkCliParams = CommandParams(
        source = ProcessSource(processOptions!!.processName, processOptions!!.device),
        obfuscationMappingPath = obfuscationMappingPath
      )
    } else if (heapDumpFile != null) {
      context.sharkCliParams = CommandParams(
        source = HprofFileSource(heapDumpFile!!),
        obfuscationMappingPath = obfuscationMappingPath
      )
    } else {
      throw UsageError("Must provide one of --process, --hprof")
    }
  }

  private fun setupVerboseLogger() {
    class CLILogger : Logger {

      override fun d(message: String) {
        echo(message)
      }

      override fun d(
        throwable: Throwable,
        message: String
      ) {
        d("$message\n${getStackTraceString(throwable)}")
      }

      private fun getStackTraceString(throwable: Throwable): String {
        val stringWriter = StringWriter()
        val printWriter = PrintWriter(stringWriter, false)
        throwable.printStackTrace(printWriter)
        printWriter.flush()
        return stringWriter.toString()
      }
    }

    SharkLog.logger = CLILogger()
  }

  companion object {
    /** Zero width space */
    private const val S = '\u200b'

    var Context.sharkCliParams: CommandParams
      get() {
        var ctx: Context? = this
        while (ctx != null) {
          if (ctx.obj is CommandParams) return ctx.obj as CommandParams
          ctx = ctx.parent
        }
        throw IllegalStateException("CommandParams not found in Context.obj")
      }
      set(value) {
        obj = value
      }

    fun CliktCommand.retrieveHeapDumpFile(params: CommandParams): File {
      return when (val source = params.source) {
        is HprofFileSource -> source.file
        is ProcessSource -> dumpHeap(source.processName, source.deviceId)
      }
    }

    fun CliktCommand.echoNewline() {
      echo("")
    }

    /**
     * Copy of [CliktCommand.echo] to make it publicly visible and therefore accessible
     * from [CliktCommand] extension functions
     */
    fun CliktCommand.echo(
      message: Any?,
      trailingNewline: Boolean = true,
      err: Boolean = false,
      lineSeparator: String = context.console.lineSeparator
    ) {
      TermUi.echo(message, trailingNewline, err, context.console, lineSeparator)
    }

    fun runCommand(
      directory: File,
      vararg arguments: String
    ): String {
      val process = ProcessBuilder(*arguments)
        .directory(directory)
        .start()
        .also { it.waitFor(10, SECONDS) }

      // See https://github.com/square/leakcanary/issues/1711
      // On Windows, the process doesn't always exit; calling to readText() makes it finish, so
      // we're reading the output before checking for the exit value
      val output = process.inputStream.bufferedReader().readText()
      if (process.exitValue() != 0) {
        val command = arguments.joinToString(" ")
        val errorOutput = process.errorStream.bufferedReader()
          .readText()
        throw CliktError(
          "Failed command: '$command', error output:\n---\n$errorOutput---"
        )
      }
      return output
    }

    private val versionName = run {
      val properties = Properties()
      properties.load(
        SharkCliCommand::class.java.getResourceAsStream("/version.properties")
          ?: throw IllegalStateException("version.properties missing")
      )
      properties.getProperty("version_name") ?: throw IllegalStateException(
        "version_name property missing"
      )
    }
  }
}

package shark

import com.github.ajalt.clikt.core.CliktCommand
import shark.SharkCliCommand.Companion.retrieveHeapDumpFile
import shark.SharkCliCommand.Companion.sharkCliParams

class StripHprofCommand : CliktCommand(
  name = "strip-hprof",
  help = "Replace all primitive arrays from the provided heap dump with arrays of zeroes and generate a new \"-stripped.hprof\" file."
) {

  override fun run() {
    val heapDumpFile = retrieveHeapDumpFile(context.sharkCliParams)
    SharkLog.d { "Stripping primitive arrays in heap dump $heapDumpFile" }
    val stripper = HprofPrimitiveArrayStripper()
    val outputFile = stripper.stripPrimitiveArrays(heapDumpFile)
    echo("Created hprof with stripped primitive arrays to $outputFile")
  }
}

package shark

import java.io.Closeable

/**
 * A [HeapGraph] that should be closed after being used.
 */
interface CloseableHeapGraph : HeapGraph, Closeable

package shark

/**
 * In memory store that can be used to store objects in a given [HeapGraph] instance.
 * This is a simple [MutableMap] of [String] to [Any], but with unsafe generics access.
 */
class GraphContext {
  private val store = mutableMapOf<String, Any?>()
  operator fun <T> get(key: String): T? {
    @Suppress("UNCHECKED_CAST")
    return store[key] as T?
  }

  /**
   * @see MutableMap.getOrPut
   */
  fun <T> getOrPut(
    key: String,
    defaultValue: () -> T
  ): T {
    @Suppress("UNCHECKED_CAST")
    return store.getOrPut(key, {
      defaultValue()
    }) as T
  }

  /**
   * @see MutableMap.set
   */
  operator fun <T> set(
    key: String,
    value: T
  ) {
    store[key] = (value as Any?)
  }

  /**
   * @see MutableMap.containsKey
   */
  operator fun contains(key: String): Boolean {
    return key in store
  }

  /**
   * @see MutableMap.remove
   */
  operator fun minusAssign(key: String) {
    @Suppress("UNCHECKED_CAST")
    store -= key
  }
}

package shark

import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import shark.HeapObject.HeapObjectArray
import shark.HeapObject.HeapPrimitiveArray

/**
 * Represents a static field or an instance field.
 */
class HeapField(
  /**
   * The class this field was declared in.
   */
  val declaringClass: HeapClass,
  /**
   * Name of the field
   */
  val name: String,
  /**
   * Value of the field. Also see shorthands [valueAsClass], [valueAsInstance],
   * [valueAsObjectArray], [valueAsPrimitiveArray].
   */
  val value: HeapValue
) {

  /**
   * Return a [HeapClass] is [value] references a class, and null otherwise.
   */
  val valueAsClass: HeapClass?
    get() = value.asObject?.asClass

  /**
   * Return a [HeapInstance] is [value] references an instance, and null otherwise.
   */
  val valueAsInstance: HeapInstance?
    get() = value.asObject?.asInstance

  /**
   * Return a [HeapObjectArray] is [value] references an object array, and null otherwise.
   */
  val valueAsObjectArray: HeapObjectArray?
    get() = value.asObject?.asObjectArray

  /**
   * Return a [HeapPrimitiveArray] is [value] references a primitive array, and null
   * otherwise.
   */
  val valueAsPrimitiveArray: HeapPrimitiveArray?
    get() = value.asObject?.asPrimitiveArray
}

package shark

import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import shark.HeapObject.HeapObjectArray
import shark.HeapObject.HeapPrimitiveArray

/**
 * Enables navigation through the heap graph of objects.
 */
interface HeapGraph {
  val identifierByteSize: Int

  /**
   * In memory store that can be used to store objects this [HeapGraph] instance.
   */
  val context: GraphContext

  val objectCount: Int

  val classCount: Int

  val instanceCount: Int

  val objectArrayCount: Int

  val primitiveArrayCount: Int

  /**
   * All GC roots which type matches types known to this heap graph and which point to non null
   * references. You can retrieve the object that a GC Root points to by calling [findObjectById]
   * with [GcRoot.id], however you need to first check that [objectExists] returns true because
   * GC roots can point to objects that don't exist in the heap dump.
   */
  val gcRoots: List<GcRoot>

  /**
   * Sequence of all objects in the heap dump.
   *
   * This sequence does not trigger any IO reads.
   */
  val objects: Sequence<HeapObject>

  /**
   * Sequence of all classes in the heap dump.
   *
   * This sequence does not trigger any IO reads.
   */
  val classes: Sequence<HeapClass>

  /**
   * Sequence of all instances in the heap dump.
   *
   * This sequence does not trigger any IO reads.
   */
  val instances: Sequence<HeapInstance>

  /**
   * Sequence of all object arrays in the heap dump.
   *
   * This sequence does not trigger any IO reads.
   */
  val objectArrays: Sequence<HeapObjectArray>

  /**
   * Sequence of all primitive arrays in the heap dump.
   *
   * This sequence does not trigger any IO reads.
   */
  val primitiveArrays: Sequence<HeapPrimitiveArray>

  /**
   * Returns the [HeapObject] corresponding to the provided [objectId], and throws
   * [IllegalArgumentException] otherwise.
   */
  @Throws(IllegalArgumentException::class)
  fun findObjectById(objectId: Long): HeapObject

  /**
   * Returns the [HeapObject] corresponding to the provided [objectIndex], and throws
   * [IllegalArgumentException] if [objectIndex] is less than 0 or more than [objectCount] - 1.
   */
  @Throws(IllegalArgumentException::class)
  fun findObjectByIndex(objectIndex: Int): HeapObject

  /**
   * Returns the [HeapObject] corresponding to the provided [objectId] or null if it cannot be
   * found.
   */
  fun findObjectByIdOrNull(objectId: Long): HeapObject?

  /**
   * Returns the [HeapClass] corresponding to the provided [className], or null if the
   * class cannot be found.
   */
  fun findClassByName(className: String): HeapClass?

  /**
   * Returns true if the provided [objectId] exists in the heap dump.
   */
  fun objectExists(objectId: Long): Boolean
}

package shark

import shark.HprofRecord.HeapDumpRecord.ObjectRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.FieldRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.StaticFieldRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.InstanceDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ObjectArrayDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ByteArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.CharArrayDump
import shark.ValueHolder.ReferenceHolder
import shark.internal.IndexedObject.IndexedClass
import shark.internal.IndexedObject.IndexedInstance
import shark.internal.IndexedObject.IndexedObjectArray
import shark.internal.IndexedObject.IndexedPrimitiveArray
import java.nio.charset.Charset
import java.util.Locale
import kotlin.reflect.KClass

/**
 * An object in the heap dump.
 */
sealed class HeapObject {

  /**
   * The graph of objects in the heap, which you can use to navigate the heap.
   */

  abstract val graph: HeapGraph

  /**
   * The heap identifier of this object.
   */
  abstract val objectId: Long

  /**
   * An positive object index that's specific to how Shark stores objects in memory.
   * The index starts at 0 and ends at [HeapGraph.objectCount] - 1. There are no gaps, every index
   * value corresponds to an object. Classes are first, then instances, then object arrays then
   * primitive arrays.
   */
  abstract val objectIndex: Int

  /**
   * Reads and returns the underlying [ObjectRecord].
   *
   * This may trigger IO reads.
   */
  abstract fun readRecord(): ObjectRecord

  /**
   * The total byte size for the record of this object in the heap dump.
   */
  abstract val recordSize: Int

  /**
   * This [HeapObject] as a [HeapClass] if it is one, or null otherwise
   */
  val asClass: HeapClass?
    get() = if (this is HeapClass) this else null

  /**
   * This [HeapObject] as a [HeapInstance] if it is one, or null otherwise
   */
  val asInstance: HeapInstance?
    get() = if (this is HeapInstance) this else null

  /**
   * This [HeapObject] as a [HeapObjectArray] if it is one, or null otherwise
   */
  val asObjectArray: HeapObjectArray?
    get() = if (this is HeapObjectArray) this else null

  /**
   * This [HeapObject] as a [HeapPrimitiveArray] if it is one, or null otherwise
   */
  val asPrimitiveArray: HeapPrimitiveArray?
    get() = if (this is HeapPrimitiveArray) this else null

  /**
   * A class in the heap dump.
   */
  class HeapClass internal constructor(
    private val hprofGraph: HprofHeapGraph,
    private val indexedObject: IndexedClass,
    override val objectId: Long,
    override val objectIndex: Int
  ) : HeapObject() {
    override val graph: HeapGraph
      get() = hprofGraph

    /**
     * Whether this is class is a primitive wrapper type
     */
    val isPrimitiveWrapperClass: Boolean
      get() = (name in primitiveWrapperClassNames)

    /**
     * The name of this class, identical to [Class.getName].
     * If this class is an array class, the name has a suffix of brackets for each dimension of
     * the array, e.g. `com.Foo[][]` is a class for 2 dimensional arrays of `com.Foo`.
     *
     * The behavior for primitive types changes depending on the VM that dumped the heap. JVM
     * heap dumps don't have any [HeapClass] object for primitive types, instead the
     * `java.land.Class` class has 9 instances (the 8 primitive types and `void`). Android heap
     * dumps have an [HeapClass] object for primitive type and the `java.land.Class` class has no
     * instance.
     *
     * If this is an array class, you can find the component type by removing the brackets at the
     * end, e.g. `name.substringBefore('[')`. Be careful when doing this for JVM heap dumps though,
     * as if the component type is a primitive type there will not be a [HeapClass] object for it.
     * This is especially tricky with N dimension primitive type arrays, which are instances of
     * [HeapObjectArray] (vs single dimension primitive type arrays which are instances of
     * [HeapPrimitiveArray]).
     */
    val name: String
      get() = hprofGraph.className(objectId)

    /**
     * Returns [name] stripped of any string content before the last period (included).
     */
    val simpleName: String
      get() = classSimpleName(name)

    /**
     * The total byte size of fields for instances of this class, as registered in the class dump.
     * This includes the size of fields from superclasses.
     *
     * @see readFieldsByteSize
     */
    val instanceByteSize: Int
      get() = indexedObject.instanceSize

    override val recordSize: Int
      get() = indexedObject.recordSize.toInt()

    val hasReferenceInstanceFields: Boolean
      get() = hprofGraph.classDumpHasReferenceFields(indexedObject)

    /**
     * Returns true if this class is an array class, and false otherwise.
     */
    val isArrayClass: Boolean
      get() = name.endsWith("[]")

    val isPrimitiveArrayClass: Boolean
      get() = name in primitiveTypesByPrimitiveArrayClassName

    val isObjectArrayClass: Boolean
      get() = isArrayClass && !isPrimitiveArrayClass

    /**
     * The total byte size of fields for instances of this class, computed as the sum of the
     * individual size of each field of this class. This does not include the size of fields from
     * superclasses.
     *
     * This may trigger IO reads.
     *
     * @see instanceByteSize
     */
    fun readFieldsByteSize(): Int {
      return readRecordFields().sumBy {
        if (it.type == PrimitiveType.REFERENCE_HPROF_TYPE) {
          hprofGraph.identifierByteSize
        } else PrimitiveType.byteSizeByHprofType.getValue(it.type)
      }
    }

    /**
     * The [HeapClass] representing the superclass of this [HeapClass]. If this [HeapClass]
     * represents either the [Object] class or a primitive type, then
     * null is returned. If this [HeapClass] represents an array class then the
     * [HeapClass] object representing the [Object] class is returned.
     */
    val superclass: HeapClass?
      get() {
        if (indexedObject.superclassId == ValueHolder.NULL_REFERENCE) return null
        return hprofGraph.findObjectById(indexedObject.superclassId) as HeapClass
      }

    /**
     * The class hierarchy starting at this class (included) and ending at the [Object] class
     * (included).
     */
    val classHierarchy: Sequence<HeapClass>
      get() = generateSequence(this) { it.superclass }

    /**
     * All the subclasses (direct and indirect) of this class,
     * in the order they were recorded in the heap dump.
     */
    val subclasses: Sequence<HeapClass>
      get() = hprofGraph.classes.filter { it subclassOf this }

    /**
     * Returns true if [subclass] is a sub class of this [HeapClass].
     */
    infix fun superclassOf(subclass: HeapClass): Boolean {
      return subclass.classHierarchy.any { it.objectId == objectId }
    }

    /**
     * Returns true if [superclass] is a superclass of this [HeapClass].
     */
    infix fun subclassOf(superclass: HeapClass): Boolean {
      return superclass.objectId != objectId && classHierarchy.any { it.objectId == superclass.objectId }
    }

    /**
     * All instances of this class, including instances of subclasses of this class.
     */
    val instances: Sequence<HeapInstance>
      get() = if (!isArrayClass) {
        hprofGraph.instances.filter { it instanceOf this }
      } else {
        emptySequence()
      }

    val objectArrayInstances: Sequence<HeapObjectArray>
      get() = if (isObjectArrayClass) {
        hprofGraph.objectArrays.filter { it.indexedObject.arrayClassId == objectId }
      } else {
        emptySequence()
      }

    /**
     * Primitive arrays are one dimensional arrays of a primitive type.
     * N-dimension arrays of primitive types (e.g. int[][]) are object arrays pointing to primitive
     * arrays.
     */
    val primitiveArrayInstances: Sequence<HeapPrimitiveArray>
      get() {
        val primitiveType = primitiveTypesByPrimitiveArrayClassName[name]
        return if (primitiveType != null) {
          hprofGraph.primitiveArrays.filter { it.primitiveType == primitiveType }
        } else {
          emptySequence()
        }
      }

    /**
     * All direct instances of this class, ie excluding any instance of subclasses of this class.
     */
    val directInstances: Sequence<HeapInstance>
      get() = hprofGraph.instances.filter { it.indexedObject.classId == objectId }

    /**
     * Reads and returns the underlying [ClassDumpRecord].
     *
     * This may trigger IO reads.
     */
    override fun readRecord(): ClassDumpRecord {
      return hprofGraph.readClassDumpRecord(objectId, indexedObject)
    }

    fun readRecordStaticFields() = hprofGraph.classDumpStaticFields(indexedObject)

    fun readRecordFields() = hprofGraph.classDumpFields(indexedObject)

    /**
     * Returns the name of the field declared in this class for the specified [fieldRecord].
     */
    fun instanceFieldName(fieldRecord: FieldRecord): String {
      return hprofGraph.fieldName(objectId, fieldRecord)
    }

    /**
     * The static fields of this class, as a sequence of [HeapField].
     *
     * This may trigger IO reads.
     */
    fun readStaticFields(): Sequence<HeapField> {
      return readRecordStaticFields().asSequence()
        .map { fieldRecord ->
          HeapField(
            this, hprofGraph.staticFieldName(objectId, fieldRecord),
            HeapValue(hprofGraph, fieldRecord.value)
          )
        }
    }

    /**
     * Returns a [HeapField] object that reflects the specified declared
     * field of the class represented by this [HeapClass] object, or null if this field does not
     * exist. The [name] parameter specifies the simple name of the desired field.
     *
     * Also available as a convenience operator: [get]
     *
     * This may trigger IO reads.
     */
    fun readStaticField(fieldName: String): HeapField? {
      for (fieldRecord in readRecordStaticFields()) {
        val recordFieldName = hprofGraph.staticFieldName(objectId, fieldRecord)
        if (recordFieldName == fieldName) {
          return HeapField(this, fieldName, HeapValue(hprofGraph, fieldRecord.value))
        }
      }
      return null
    }

    /**
     * @see readStaticField
     */
    operator fun get(fieldName: String) = readStaticField(fieldName)

    override fun toString(): String {
      return "class $name"
    }
  }

  /**
   * An instance in the heap dump.
   */
  class HeapInstance internal constructor(
    private val hprofGraph: HprofHeapGraph,
    internal val indexedObject: IndexedInstance,
    override val objectId: Long,
    override val objectIndex: Int
  ) : HeapObject() {

    /**
     * Whether this is an instance of a primitive wrapper type.
     */
    val isPrimitiveWrapper: Boolean
      get() = instanceClassName in primitiveWrapperClassNames

    override val graph: HeapGraph
      get() = hprofGraph

    /**
     * @see HeapClass.instanceByteSize
     */
    val byteSize
      get() = instanceClass.instanceByteSize

    /**
     * The name of the class of this instance, identical to [Class.getName].
     */
    val instanceClassName: String
      get() = hprofGraph.className(indexedObject.classId)

    /**
     * Returns [instanceClassName] stripped of any string content before the last period (included).
     */
    val instanceClassSimpleName: String
      get() = classSimpleName(instanceClassName)

    /**
     * The class of this instance.
     */
    val instanceClass: HeapClass
      get() = hprofGraph.findObjectById(indexedObject.classId) as HeapClass

    /**
     * The heap identifier of the class of this instance.
     */
    val instanceClassId: Long
      get() = indexedObject.classId

    /**
     * Reads and returns the underlying [InstanceDumpRecord].
     *
     * This may trigger IO reads.
     */
    override fun readRecord(): InstanceDumpRecord {
      return hprofGraph.readInstanceDumpRecord(objectId, indexedObject)
    }

    override val recordSize: Int
      get() = indexedObject.recordSize.toInt()

    /**
     * Returns true if this is an instance of the class named [className] or an instance of a
     * subclass of that class.
     */
    infix fun instanceOf(className: String): Boolean =
      instanceClass.classHierarchy.any { it.name == className }

    /**
     * Returns true if this is an instance of [expectedClass] or an instance of a subclass of that
     * class.
     */
    infix fun instanceOf(expectedClass: KClass<*>) =
      this instanceOf expectedClass.java.name

    /**
     * Returns true if this is an instance of [expectedClass] or an instance of a subclass of that
     * class.
     */
    infix fun instanceOf(expectedClass: HeapClass) =
      instanceClass.classHierarchy.any { it.objectId == expectedClass.objectId }

    /**
     * @see readField
     */
    fun readField(
      declaringClass: KClass<out Any>,
      fieldName: String
    ): HeapField? {
      return readField(declaringClass.java.name, fieldName)
    }

    /**
     * Returns a [HeapField] object that reflects the specified declared
     * field of the instance represented by this [HeapInstance] object, or null if this field does
     * not exist. The [declaringClassName] specifies the class in which the desired field is
     * declared, and the [fieldName] parameter specifies the simple name of the desired field.
     *
     * Also available as a convenience operator: [get]
     *
     * This may trigger IO reads.
     */
    fun readField(
      declaringClassName: String,
      fieldName: String
    ): HeapField? {
      return readFields().firstOrNull { field -> field.declaringClass.name == declaringClassName && field.name == fieldName }
    }

    /**
     * @see readField
     */
    operator fun get(
      declaringClass: KClass<out Any>,
      fieldName: String
    ): HeapField? {
      return readField(declaringClass, fieldName)
    }

    /**
     * @see readField
     */
    operator fun get(
      declaringClassName: String,
      fieldName: String
    ) = readField(declaringClassName, fieldName)

    /**
     * The fields of this instance, as a sequence of [HeapField].
     *
     * This may trigger IO reads.
     */
    fun readFields(): Sequence<HeapField> {
      val fieldReader by lazy {
        hprofGraph.createFieldValuesReader(readRecord())
      }
      return instanceClass.classHierarchy
        .map { heapClass ->
          heapClass.readRecordFields().asSequence()
            .map { fieldRecord ->
              val fieldName = hprofGraph.fieldName(heapClass.objectId, fieldRecord)
              val fieldValue = fieldReader.readValue(fieldRecord)
              HeapField(heapClass, fieldName, HeapValue(hprofGraph, fieldValue))
            }
        }
        .flatten()
    }

    /**
     * If this [HeapInstance] is an instance of the [String] class, returns a [String] instance
     * with content that matches the string in the heap dump. Otherwise returns null.
     *
     * This may trigger IO reads.
     */
    fun readAsJavaString(): String? {
      if (instanceClassName != "java.lang.String") {
        return null
      }

      // JVM strings don't have a count field.
      val count = this["java.lang.String", "count"]?.value?.asInt
      if (count == 0) {
        return ""
      }

      // Prior to API 26 String.value was a char array.
      // Since API 26 String.value is backed by native code. The vast majority of strings in a
      // heap dump are backed by a byte array, but we still find a few backed by a char array.
      when (val valueRecord =
        this["java.lang.String", "value"]!!.value.asObject!!.readRecord()) {
        is CharArrayDump -> {
          // < API 23
          // As of Marshmallow, substrings no longer share their parent strings' char arrays
          // eliminating the need for String.offset
          // https://android-review.googlesource.com/#/c/83611/
          val offset = this["java.lang.String", "offset"]?.value?.asInt

          val chars = if (count != null && offset != null) {
            // Handle heap dumps where all primitive arrays have been replaced with empty arrays,
            // e.g. with HprofPrimitiveArrayStripper
            val toIndex = if (offset + count > valueRecord.array.size) {
              valueRecord.array.size
            } else offset + count
            valueRecord.array.copyOfRange(offset, toIndex)
          } else {
            valueRecord.array
          }
          return String(chars)
        }
        is ByteArrayDump -> {
          return String(valueRecord.array, Charset.forName("UTF-8"))
        }
        else -> throw UnsupportedOperationException(
          "'value' field ${this["java.lang.String", "value"]!!.value} was expected to be either" +
            " a char or byte array in string instance with id $objectId"
        )
      }
    }

    override fun toString(): String {
      return "instance @$objectId of $instanceClassName"
    }
  }

  /**
   * An object array in the heap dump.
   */
  class HeapObjectArray internal constructor(
    private val hprofGraph: HprofHeapGraph,
    internal val indexedObject: IndexedObjectArray,
    override val objectId: Long,
    override val objectIndex: Int
  ) : HeapObject() {

    override val graph: HeapGraph
      get() = hprofGraph

    /**
     * The name of the class of this array, identical to [Class.getName].
     */
    val arrayClassName: String
      get() = hprofGraph.className(indexedObject.arrayClassId)

    /**
     * Returns [arrayClassName] stripped of any string content before the last period (included).
     */
    val arrayClassSimpleName: String
      get() = classSimpleName(arrayClassName)

    /**
     * The class of this array.
     */
    val arrayClass: HeapClass
      get() = hprofGraph.findObjectById(indexedObject.arrayClassId) as HeapClass

    /**
     * The total byte shallow size of elements in this array.
     */
    fun readByteSize(): Int {
      return hprofGraph.readObjectArrayByteSize(objectId, indexedObject)
    }

    /**
     * Reads and returns the underlying [ObjectArrayDumpRecord].
     *
     * This may trigger IO reads.
     */
    override fun readRecord(): ObjectArrayDumpRecord {
      return hprofGraph.readObjectArrayDumpRecord(objectId, indexedObject)
    }

    override val recordSize: Int
      get() = indexedObject.recordSize.toInt()

    /**
     * The elements in this array, as a sequence of [HeapValue].
     *
     * This may trigger IO reads.
     */
    fun readElements(): Sequence<HeapValue> {
      return readRecord().elementIds.asSequence()
        .map { HeapValue(hprofGraph, ReferenceHolder(it)) }
    }

    override fun toString(): String {
      return "object array @$objectId of $arrayClassName"
    }
  }

  /**
   * A primitive array in the heap dump.
   */
  class HeapPrimitiveArray internal constructor(
    private val hprofGraph: HprofHeapGraph,
    private val indexedObject: IndexedPrimitiveArray,
    override val objectId: Long,
    override val objectIndex: Int
  ) : HeapObject() {

    override val graph: HeapGraph
      get() = hprofGraph

    /**
     * The total byte shallow size of elements in this array.
     */
    fun readByteSize(): Int {
      return hprofGraph.readPrimitiveArrayByteSize(objectId, indexedObject)
    }

    /**
     * The [PrimitiveType] of elements in this array.
     */
    val primitiveType: PrimitiveType
      get() = indexedObject.primitiveType

    /**
     * The name of the class of this array, identical to [Class.getName].
     */
    val arrayClassName: String
      get() = "${primitiveType.name.toLowerCase(Locale.US)}[]"

    /**
     * The class of this array.
     */
    val arrayClass: HeapClass
      get() = graph.findClassByName(arrayClassName)!!

    /**
     * Reads and returns the underlying [PrimitiveArrayDumpRecord].
     *
     * This may trigger IO reads.
     */
    override fun readRecord(): PrimitiveArrayDumpRecord {
      return hprofGraph.readPrimitiveArrayDumpRecord(objectId, indexedObject)
    }

    override val recordSize: Int
      get() = indexedObject.recordSize.toInt()

    override fun toString(): String {
      return "primitive array @$objectId of $arrayClassName"
    }
  }

  companion object {

    internal val primitiveTypesByPrimitiveArrayClassName = PrimitiveType.values()
      .map { "${it.name.toLowerCase(Locale.US)}[]" to it }
      .toMap()

    private val primitiveWrapperClassNames = setOf<String>(
      Boolean::class.javaObjectType.name, Char::class.javaObjectType.name,
      Float::class.javaObjectType.name,
      Double::class.javaObjectType.name, Byte::class.javaObjectType.name,
      Short::class.javaObjectType.name,
      Int::class.javaObjectType.name, Long::class.javaObjectType.name
    )

    private fun classSimpleName(className: String): String {
      val separator = className.lastIndexOf('.')
      return if (separator == -1) {
        className
      } else {
        className.substring(separator + 1)
      }
    }
  }
}

package shark

import shark.ValueHolder.BooleanHolder
import shark.ValueHolder.ByteHolder
import shark.ValueHolder.CharHolder
import shark.ValueHolder.DoubleHolder
import shark.ValueHolder.FloatHolder
import shark.ValueHolder.IntHolder
import shark.ValueHolder.LongHolder
import shark.ValueHolder.ReferenceHolder
import shark.ValueHolder.ShortHolder

/**
 * Represents a value in the heap dump, which can be an object reference or
 * a primitive type.
 */
class HeapValue(
  /**
   * The graph of objects in the heap, which you can use to navigate the heap.
   */
  val graph: HeapGraph,
  /**
   * Holds the actual value that this [HeapValue] represents.
   */
  val holder: ValueHolder
) {

  /**
   * This [HeapValue] as a [Boolean] if it represents one, or null otherwise.
   */
  val asBoolean: Boolean?
    get() = if (holder is BooleanHolder) holder.value else null

  /**
   * This [HeapValue] as a [Char] if it represents one, or null otherwise.
   */
  val asChar: Char?
    get() = if (holder is CharHolder) holder.value else null

  /**
   * This [HeapValue] as a [Float] if it represents one, or null otherwise.
   */
  val asFloat: Float?
    get() = if (holder is FloatHolder) holder.value else null

  /**
   * This [HeapValue] as a [Double] if it represents one, or null otherwise.
   */
  val asDouble: Double?
    get() = if (holder is DoubleHolder) holder.value else null

  /**
   * This [HeapValue] as a [Byte] if it represents one, or null otherwise.
   */
  val asByte: Byte?
    get() = if (holder is ByteHolder) holder.value else null

  /**
   * This [HeapValue] as a [Short] if it represents one, or null otherwise.
   */
  val asShort: Short?
    get() = if (holder is ShortHolder) holder.value else null

  /**
   * This [HeapValue] as an [Int] if it represents one, or null otherwise.
   */
  val asInt: Int?
    get() = if (holder is IntHolder) holder.value else null

  /**
   * This [HeapValue] as a [Long] if it represents one, or null otherwise.
   */
  val asLong: Long?
    get() = if (holder is LongHolder) holder.value else null

  /**
   * This [HeapValue] as a [Long] if it represents an object reference, or null otherwise.
   */
  val asObjectId: Long?
    get() = if (holder is ReferenceHolder) holder.value else null

  /**
   * This [HeapValue] as a [Long] if it represents a non null object reference, or null otherwise.
   */
  val asNonNullObjectId: Long?
    get() = if (holder is ReferenceHolder && !holder.isNull) holder.value else null

  /**
   * True is this [HeapValue] represents a null object reference, false otherwise.
   */
  val isNullReference: Boolean
    get() = holder is ReferenceHolder && holder.isNull

  /**
   * True is this [HeapValue] represents a non null object reference, false otherwise.
   */
  val isNonNullReference: Boolean
    get() = holder is ReferenceHolder && !holder.isNull

  /**
   * The [HeapObject] referenced by this [HeapValue] if it represents a non null object reference,
   * or null otherwise.
   */
  val asObject: HeapObject?
    get() {
      return if (holder is ReferenceHolder && !holder.isNull) {
        return graph.findObjectById(holder.value)
      } else {
        null
      }
    }

  /**
   * If this [HeapValue] if it represents a non null object reference to an instance of the
   * [String] class that exists in the heap dump, returns a [String] instance with content that
   * matches the string in the heap dump. Otherwise returns null.
   *
   * This may trigger IO reads.
   */
  fun readAsJavaString(): String? {
    if (holder is ReferenceHolder && !holder.isNull) {
      val heapObject = graph.findObjectByIdOrNull(holder.value)
      return heapObject?.asInstance?.readAsJavaString()
    }
    return null
  }
}

package shark

import shark.GcRoot.Debugger
import shark.GcRoot.Finalizing
import shark.GcRoot.InternedString
import shark.GcRoot.JavaFrame
import shark.GcRoot.JniGlobal
import shark.GcRoot.JniLocal
import shark.GcRoot.JniMonitor
import shark.GcRoot.MonitorUsed
import shark.GcRoot.NativeStack
import shark.GcRoot.ReferenceCleanup
import shark.GcRoot.StickyClass
import shark.GcRoot.ThreadBlock
import shark.GcRoot.ThreadObject
import shark.GcRoot.Unknown
import shark.GcRoot.Unreachable
import shark.GcRoot.VmInternal
import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import shark.HeapObject.HeapObjectArray
import shark.HeapObject.HeapPrimitiveArray
import shark.HprofRecord.HeapDumpRecord.ObjectRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.FieldRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.StaticFieldRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.InstanceDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ObjectArrayDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.BooleanArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ByteArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.CharArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.DoubleArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.FloatArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.IntArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.LongArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ShortArrayDump
import shark.HprofVersion.ANDROID
import shark.internal.FieldValuesReader
import shark.internal.HprofInMemoryIndex
import shark.internal.IndexedObject
import shark.internal.IndexedObject.IndexedClass
import shark.internal.IndexedObject.IndexedInstance
import shark.internal.IndexedObject.IndexedObjectArray
import shark.internal.IndexedObject.IndexedPrimitiveArray
import shark.internal.LruCache
import java.io.File
import kotlin.reflect.KClass

/**
 * A [HeapGraph] that reads from an Hprof file indexed by [HprofIndex].
 */
class HprofHeapGraph internal constructor(
  private val header: HprofHeader,
  private val reader: RandomAccessHprofReader,
  private val index: HprofInMemoryIndex
) : CloseableHeapGraph {

  override val identifierByteSize: Int get() = header.identifierByteSize

  override val context = GraphContext()

  override val objectCount: Int
    get() = classCount + instanceCount + objectArrayCount + primitiveArrayCount

  override val classCount: Int
    get() = index.classCount

  override val instanceCount: Int
    get() = index.instanceCount

  override val objectArrayCount: Int
    get() = index.objectArrayCount

  override val primitiveArrayCount: Int
    get() = index.primitiveArrayCount

  override val gcRoots: List<GcRoot>
    get() = index.gcRoots()

  override val objects: Sequence<HeapObject>
    get() {
      var objectIndex = 0
      return index.indexedObjectSequence()
        .map {
          wrapIndexedObject(objectIndex++, it.second, it.first)
        }
    }

  override val classes: Sequence<HeapClass>
    get() {
      var objectIndex = 0
      return index.indexedClassSequence()
        .map {
          val objectId = it.first
          val indexedObject = it.second
          HeapClass(this, indexedObject, objectId, objectIndex++)
        }
    }

  override val instances: Sequence<HeapInstance>
    get() {
      var objectIndex = classCount
      return index.indexedInstanceSequence()
        .map {
          val objectId = it.first
          val indexedObject = it.second
          HeapInstance(this, indexedObject, objectId, objectIndex++)
        }
    }

  override val objectArrays: Sequence<HeapObjectArray>
    get() {
      var objectIndex = classCount + instanceCount
      return index.indexedObjectArraySequence().map {
        val objectId = it.first
        val indexedObject = it.second
        HeapObjectArray(this, indexedObject, objectId, objectIndex++)
      }
    }

  override val primitiveArrays: Sequence<HeapPrimitiveArray>
    get() {
      var objectIndex = classCount + instanceCount + objectArrayCount
      return index.indexedPrimitiveArraySequence().map {
        val objectId = it.first
        val indexedObject = it.second
        HeapPrimitiveArray(this, indexedObject, objectId, objectIndex++)
      }
    }

  private val objectCache = LruCache<Long, ObjectRecord>(INTERNAL_LRU_CACHE_SIZE)

  // java.lang.Object is the most accessed class in Heap, so we want to memoize a reference to it
  private val javaLangObjectClass: HeapClass? = findClassByName("java.lang.Object")

  /**
   * This is only public so that we can publish stats. Accessing this requires casting
   * [HeapGraph] to [HprofHeapGraph] so it's really not a public API. May change at any time!
   */
  fun lruCacheStats(): String = objectCache.toString()

  override fun findObjectById(objectId: Long): HeapObject {
    return findObjectByIdOrNull(objectId) ?: throw IllegalArgumentException(
      "Object id $objectId not found in heap dump."
    )
  }

  override fun findObjectByIndex(objectIndex: Int): HeapObject {
    require(objectIndex in 0 until objectCount) {
      "$objectIndex should be in range [0, $objectCount["
    }
    val (objectId, indexedObject) = index.objectAtIndex(objectIndex)
    return wrapIndexedObject(objectIndex, indexedObject, objectId)
  }

  override fun findObjectByIdOrNull(objectId: Long): HeapObject? {
    if (objectId == javaLangObjectClass?.objectId) return javaLangObjectClass

    val (objectIndex, indexedObject) = index.indexedObjectOrNull(objectId) ?: return null
    return wrapIndexedObject(objectIndex, indexedObject, objectId)
  }

  override fun findClassByName(className: String): HeapClass? {
    val heapDumpClassName = if (header.version != ANDROID) {
      val indexOfArrayChar = className.indexOf('[')
      if (indexOfArrayChar != -1) {
        val dimensions = (className.length - indexOfArrayChar) / 2
        val componentClassName = className.substring(0, indexOfArrayChar)
        "[".repeat(dimensions) + when (componentClassName) {
          "char" -> 'C'
          "float" -> 'F'
          "double" -> 'D'
          "byte" -> 'B'
          "short" -> 'S'
          "int" -> 'I'
          "long" -> 'J'
          else -> "L$componentClassName;"
        }
      } else {
        className
      }
    } else {
      className
    }
    val classId = index.classId(heapDumpClassName)
    return if (classId == null) {
      null
    } else {
      return findObjectById(classId) as HeapClass
    }
  }

  override fun objectExists(objectId: Long): Boolean {
    return index.objectIdIsIndexed(objectId)
  }

  override fun close() {
    reader.close()
  }

  internal fun classDumpStaticFields(indexedClass: IndexedClass): List<StaticFieldRecord> {
    return index.classFieldsReader.classDumpStaticFields(indexedClass)
  }

  internal fun classDumpFields(indexedClass: IndexedClass): List<FieldRecord> {
    return index.classFieldsReader.classDumpFields(indexedClass)
  }

  internal fun classDumpHasReferenceFields(indexedClass: IndexedClass): Boolean {
    return index.classFieldsReader.classDumpHasReferenceFields(indexedClass)
  }

  internal fun fieldName(
    classId: Long,
    fieldRecord: FieldRecord
  ): String {
    return index.fieldName(classId, fieldRecord.nameStringId)
  }

  internal fun staticFieldName(
    classId: Long,
    fieldRecord: StaticFieldRecord
  ): String {
    return index.fieldName(classId, fieldRecord.nameStringId)
  }

  internal fun createFieldValuesReader(record: InstanceDumpRecord) =
    FieldValuesReader(record, identifierByteSize)

  internal fun className(classId: Long): String {
    val hprofClassName = index.className(classId)
    if (header.version != ANDROID) {
      if (hprofClassName.startsWith('[')) {
        val arrayCharLastIndex = hprofClassName.lastIndexOf('[')
        val brackets = "[]".repeat(arrayCharLastIndex + 1)
        return when (val typeChar = hprofClassName[arrayCharLastIndex + 1]) {
          'L' -> {
            val classNameStart = arrayCharLastIndex + 2
            hprofClassName.substring(classNameStart, hprofClassName.length - 1) + brackets
          }
          'Z' -> "boolean$brackets"
          'C' -> "char$brackets"
          'F' -> "float$brackets"
          'D' -> "double$brackets"
          'B' -> "byte$brackets"
          'S' -> "short$brackets"
          'I' -> "int$brackets"
          'J' -> "long$brackets"
          else -> error("Unexpected type char $typeChar")
        }
      }
    }
    return hprofClassName
  }

  internal fun readObjectArrayDumpRecord(
    objectId: Long,
    indexedObject: IndexedObjectArray
  ): ObjectArrayDumpRecord {
    return readObjectRecord(objectId, indexedObject) {
      readObjectArrayDumpRecord()
    }
  }

  internal fun readObjectArrayByteSize(
    objectId: Long,
    indexedObject: IndexedObjectArray
  ): Int {
    val cachedRecord = objectCache[objectId] as ObjectArrayDumpRecord?
    if (cachedRecord != null) {
      return cachedRecord.elementIds.size * identifierByteSize
    }
    val position = indexedObject.position + identifierByteSize + PrimitiveType.INT.byteSize
    val size = PrimitiveType.INT.byteSize.toLong()
    val thinRecordSize = reader.readRecord(position, size) {
      readInt()
    }
    return thinRecordSize * identifierByteSize
  }

  internal fun readPrimitiveArrayDumpRecord(
    objectId: Long,
    indexedObject: IndexedPrimitiveArray
  ): PrimitiveArrayDumpRecord {
    return readObjectRecord(objectId, indexedObject) {
      readPrimitiveArrayDumpRecord()
    }
  }

  internal fun readPrimitiveArrayByteSize(
    objectId: Long,
    indexedObject: IndexedPrimitiveArray
  ): Int {
    val cachedRecord = objectCache[objectId] as PrimitiveArrayDumpRecord?
    if (cachedRecord != null) {
      return when (cachedRecord) {
        is BooleanArrayDump -> cachedRecord.array.size * PrimitiveType.BOOLEAN.byteSize
        is CharArrayDump -> cachedRecord.array.size * PrimitiveType.CHAR.byteSize
        is FloatArrayDump -> cachedRecord.array.size * PrimitiveType.FLOAT.byteSize
        is DoubleArrayDump -> cachedRecord.array.size * PrimitiveType.DOUBLE.byteSize
        is ByteArrayDump -> cachedRecord.array.size * PrimitiveType.BYTE.byteSize
        is ShortArrayDump -> cachedRecord.array.size * PrimitiveType.SHORT.byteSize
        is IntArrayDump -> cachedRecord.array.size * PrimitiveType.INT.byteSize
        is LongArrayDump -> cachedRecord.array.size * PrimitiveType.LONG.byteSize
      }
    }
    val position = indexedObject.position + identifierByteSize + PrimitiveType.INT.byteSize
    val size = reader.readRecord(position, PrimitiveType.INT.byteSize.toLong()) {
      readInt()
    }
    return size * indexedObject.primitiveType.byteSize
  }

  internal fun readClassDumpRecord(
    objectId: Long,
    indexedObject: IndexedClass
  ): ClassDumpRecord {
    return readObjectRecord(objectId, indexedObject) {
      readClassDumpRecord()
    }
  }

  internal fun readInstanceDumpRecord(
    objectId: Long,
    indexedObject: IndexedInstance
  ): InstanceDumpRecord {
    return readObjectRecord(objectId, indexedObject) {
      readInstanceDumpRecord()
    }
  }

  private fun <T : ObjectRecord> readObjectRecord(
    objectId: Long,
    indexedObject: IndexedObject,
    readBlock: HprofRecordReader.() -> T
  ): T {
    val objectRecordOrNull = objectCache[objectId]
    @Suppress("UNCHECKED_CAST")
    if (objectRecordOrNull != null) {
      return objectRecordOrNull as T
    }
    return reader.readRecord(indexedObject.position, indexedObject.recordSize) {
      readBlock()
    }.apply { objectCache.put(objectId, this) }
  }

  private fun wrapIndexedObject(
    objectIndex: Int,
    indexedObject: IndexedObject,
    objectId: Long
  ): HeapObject {
    return when (indexedObject) {
      is IndexedClass -> {
        HeapClass(this, indexedObject, objectId, objectIndex)
      }
      is IndexedInstance -> {
        HeapInstance(this, indexedObject, objectId, objectIndex)
      }
      is IndexedObjectArray -> {
        HeapObjectArray(this, indexedObject, objectId, objectIndex)
      }
      is IndexedPrimitiveArray -> HeapPrimitiveArray(this, indexedObject, objectId, objectIndex)
    }
  }

  companion object {

    /**
     * This is not a public API, it's only public so that we can evaluate the effectiveness of
     * different cache size in tests in a different module.
     *
     * LRU cache size of 3000 is a sweet spot to balance hits vs memory usage.
     * This is based on running InstrumentationLeakDetectorTest a bunch of time on a
     * Pixel 2 XL API 28. Hit count was ~120K, miss count ~290K
     */
    var INTERNAL_LRU_CACHE_SIZE = 3000

    /**
     * A facility for opening a [CloseableHeapGraph] from a [File].
     * This first parses the file headers with [HprofHeader.parseHeaderOf], then indexes the file content
     * with [HprofIndex.indexRecordsOf] and then opens a [CloseableHeapGraph] from the index, which
     * you are responsible for closing after using.
     */
    fun File.openHeapGraph(
      proguardMapping: ProguardMapping? = null,
      indexedGcRootTypes: Set<HprofRecordTag> = HprofIndex.defaultIndexedGcRootTags()
    ): CloseableHeapGraph {
      return FileSourceProvider(this).openHeapGraph(proguardMapping, indexedGcRootTypes)
    }

    fun DualSourceProvider.openHeapGraph(
      proguardMapping: ProguardMapping? = null,
      indexedGcRootTypes: Set<HprofRecordTag> = HprofIndex.defaultIndexedGcRootTags()
    ): CloseableHeapGraph {
      val header = openStreamingSource().use { HprofHeader.parseHeaderOf(it) }
      val index = HprofIndex.indexRecordsOf(this, header, proguardMapping, indexedGcRootTypes)
      return index.openHeapGraph()
    }

    @Deprecated(
      "Replaced by HprofIndex.indexRecordsOf().openHeapGraph() or File.openHeapGraph()",
      replaceWith = ReplaceWith(
        "HprofIndex.indexRecordsOf(hprof, proguardMapping, indexedGcRootTypes)" +
          ".openHeapGraph()"
      )
    )
    fun indexHprof(
      hprof: Hprof,
      proguardMapping: ProguardMapping? = null,
      indexedGcRootTypes: Set<KClass<out GcRoot>> = deprecatedDefaultIndexedGcRootTypes()
    ): HeapGraph {
      val indexedRootTags = indexedGcRootTypes.map {
        when (it) {
          Unknown::class -> HprofRecordTag.ROOT_UNKNOWN
          JniGlobal::class -> HprofRecordTag.ROOT_JNI_GLOBAL
          JniLocal::class -> HprofRecordTag.ROOT_JNI_LOCAL
          JavaFrame::class -> HprofRecordTag.ROOT_JAVA_FRAME
          NativeStack::class -> HprofRecordTag.ROOT_NATIVE_STACK
          StickyClass::class -> HprofRecordTag.ROOT_STICKY_CLASS
          ThreadBlock::class -> HprofRecordTag.ROOT_THREAD_BLOCK
          MonitorUsed::class -> HprofRecordTag.ROOT_MONITOR_USED
          ThreadObject::class -> HprofRecordTag.ROOT_THREAD_OBJECT
          InternedString::class -> HprofRecordTag.ROOT_INTERNED_STRING
          Finalizing::class -> HprofRecordTag.ROOT_FINALIZING
          Debugger::class -> HprofRecordTag.ROOT_DEBUGGER
          ReferenceCleanup::class -> HprofRecordTag.ROOT_REFERENCE_CLEANUP
          VmInternal::class -> HprofRecordTag.ROOT_VM_INTERNAL
          JniMonitor::class -> HprofRecordTag.ROOT_JNI_MONITOR
          Unreachable::class -> HprofRecordTag.ROOT_UNREACHABLE
          else -> error("Unknown root $it")
        }
      }.toSet()
      val index =
        HprofIndex.indexRecordsOf(
          FileSourceProvider(hprof.file), hprof.header, proguardMapping, indexedRootTags
        )
      val graph = index.openHeapGraph()
      hprof.attachClosable(graph)
      return graph
    }

    private fun deprecatedDefaultIndexedGcRootTypes() = setOf(
      JniGlobal::class,
      JavaFrame::class,
      JniLocal::class,
      MonitorUsed::class,
      NativeStack::class,
      StickyClass::class,
      ThreadBlock::class,
      ThreadObject::class,
      JniMonitor::class
    )
  }
}

package shark

import shark.internal.HprofInMemoryIndex
import java.util.EnumSet

/**
 * An index on a Hprof file. See [openHeapGraph].
 */
class HprofIndex private constructor(
  private val sourceProvider: RandomAccessSourceProvider,
  private val header: HprofHeader,
  private val index: HprofInMemoryIndex
) {

  /**
   * Opens a [CloseableHeapGraph] which you can use to navigate the indexed hprof and then close.
   */
  fun openHeapGraph(): CloseableHeapGraph {
    val reader = RandomAccessHprofReader.openReaderFor(sourceProvider, header)
    return HprofHeapGraph(header, reader, index)
  }

  companion object {
    /**
     * Creates an in memory index of an hprof source provided by [hprofSourceProvider].
     */
    fun indexRecordsOf(
      hprofSourceProvider: DualSourceProvider,
      hprofHeader: HprofHeader,
      proguardMapping: ProguardMapping? = null,
      indexedGcRootTags: Set<HprofRecordTag> = defaultIndexedGcRootTags()
    ): HprofIndex {
      val reader = StreamingHprofReader.readerFor(hprofSourceProvider, hprofHeader)
      val index = HprofInMemoryIndex.indexHprof(
        reader = reader,
        hprofHeader = hprofHeader,
        proguardMapping = proguardMapping,
        indexedGcRootTags = indexedGcRootTags
      )
      return HprofIndex(hprofSourceProvider, hprofHeader, index)
    }

    fun defaultIndexedGcRootTags() = EnumSet.of(
      HprofRecordTag.ROOT_JNI_GLOBAL,
      HprofRecordTag.ROOT_JAVA_FRAME,
      HprofRecordTag.ROOT_JNI_LOCAL,
      HprofRecordTag.ROOT_MONITOR_USED,
      HprofRecordTag.ROOT_NATIVE_STACK,
      HprofRecordTag.ROOT_STICKY_CLASS,
      HprofRecordTag.ROOT_THREAD_BLOCK,
      // ThreadObject points to threads, which we need to find the thread that a JavaLocalPattern
      // belongs to
      HprofRecordTag.ROOT_THREAD_OBJECT,
      HprofRecordTag.ROOT_JNI_MONITOR
      /*
      Not included here:

      VmInternal: Ignoring because we've got 150K of it, but is this the right thing
      to do? What's VmInternal exactly? History does not go further than
      https://android.googlesource.com/platform/dalvik2/+/refs/heads/master/hit/src/com/android/hit/HprofParser.java#77
      We should log to figure out what objects VmInternal points to.

      ReferenceCleanup: We used to keep it, but the name doesn't seem like it should create a leak.

      Unknown: it's unknown, should we care?

      We definitely don't care about those for leak finding: InternedString, Finalizing, Debugger, Unreachable
       */
    )
  }
}

package shark

import okio.Buffer
import okio.BufferedSource
import okio.Source
import java.io.IOException

class ByteArraySourceProvider(private val byteArray: ByteArray) : DualSourceProvider {
  override fun openStreamingSource(): BufferedSource = Buffer().apply { write(byteArray) }

  override fun openRandomAccessSource(): RandomAccessSource {
    return object : RandomAccessSource {

      var closed = false

      override fun read(
        sink: Buffer,
        position: Long,
        byteCount: Long
      ): Long {
        if (closed) {
          throw IOException("Source closed")
        }
        val maxByteCount = byteCount.coerceAtMost(byteArray.size - position)
        sink.write(byteArray, position.toInt(), maxByteCount.toInt())
        return maxByteCount
      }

      override fun close() {
        closed = true
      }
    }
  }
}

package shark

import okio.Buffer
import kotlin.math.absoluteValue

/**
 * Captures IO read metrics without using much memory.
 */
class ConstantMemoryMetricsDualSourceProvider(
  private val realSourceProvider: DualSourceProvider
) : DualSourceProvider {

  var randomAccessByteReads = 0L
    internal set

  var randomAccessReadCount = 0L
    internal set

  var randomAccessByteTravel = 0L
    internal set

  private var lastRandomAccessPosition = -1L
  private var minPosition = -1L
  private var maxPosition = -1L

  private fun updateRandomAccessStatsOnRead(
    position: Long,
    bytesRead: Long
  ) {
    randomAccessByteReads += bytesRead
    randomAccessReadCount++
    if (lastRandomAccessPosition != -1L) {
      randomAccessByteTravel += (position - lastRandomAccessPosition).absoluteValue
      minPosition = minPosition.coerceAtMost(position)
      maxPosition = maxPosition.coerceAtLeast(position)
    } else {
      minPosition = position
      maxPosition = position
    }

    lastRandomAccessPosition = position
  }

  val byteTravelRange
    get() = (maxPosition - minPosition)

  override fun openStreamingSource() = realSourceProvider.openStreamingSource()

  override fun openRandomAccessSource(): RandomAccessSource {
    val randomAccessSource = realSourceProvider.openRandomAccessSource()
    return object : RandomAccessSource {
      override fun read(
        sink: Buffer,
        position: Long,
        byteCount: Long
      ): Long {
        val bytesRead = randomAccessSource.read(sink, position, byteCount)
        updateRandomAccessStatsOnRead(position, bytesRead)
        return bytesRead
      }

      override fun close() = randomAccessSource.close()
    }
  }
}

package shark

/**
 * Both a [StreamingSourceProvider] and a [RandomAccessSourceProvider]
 */
interface DualSourceProvider : StreamingSourceProvider, RandomAccessSourceProvider

package shark

import okio.Buffer
import okio.BufferedSource
import okio.Okio
import okio.Source
import java.io.File

class FileSourceProvider(private val file: File) : DualSourceProvider {
  override fun openStreamingSource(): BufferedSource = Okio.buffer(Okio.source(file.inputStream()))

  override fun openRandomAccessSource(): RandomAccessSource {
    val channel = file.inputStream().channel
    return object : RandomAccessSource {
      override fun read(
        sink: Buffer,
        position: Long,
        byteCount: Long
      ) = channel.transferTo(position, byteCount, sink)

      override fun close() = channel.close()
    }
  }
}

package shark

/**
 * A GcRoot as identified by [HprofRecord.HeapDumpRecord.GcRootRecord] in the heap dump.
 */
sealed class GcRoot {

  /**
   * The object id of the object that this gc root references.
   */
  abstract val id: Long

  /**
   * An unknown gc root.
   */
  class Unknown(override val id: Long) : GcRoot()

  /**
   * A global variable in native code.
   */
  class JniGlobal(
    override val id: Long,
    val jniGlobalRefId: Long
  ) : GcRoot()

  /**
   * A local variable in native code.
   */
  class JniLocal(
    override val id: Long,
    /** Corresponds to [ThreadObject.threadSerialNumber] */
    val threadSerialNumber: Int,
    /**
     * frame number in stack trace (-1 for empty)
     */
    val frameNumber: Int
  ) : GcRoot()

  /**
   * A java local variable
   */
  class JavaFrame(
    override val id: Long,
    /** Corresponds to [ThreadObject.threadSerialNumber] */
    val threadSerialNumber: Int,
    /**
     * frame number in stack trace (-1 for empty)
     */
    val frameNumber: Int
  ) : GcRoot()

  /**
   * Input or output parameters in native code
   */
  class NativeStack(
    override val id: Long,
    /**
     * Corresponds to [ThreadObject.threadSerialNumber]
     * Note: the corresponding thread is sometimes not found, see:
     * https://issuetracker.google.com/issues/122713143
     */
    val threadSerialNumber: Int
  ) : GcRoot()

  /**
   * A system class
   */
  class StickyClass(override val id: Long) : GcRoot()

  class ThreadBlock(
    override val id: Long,
    /** Corresponds to [ThreadObject.threadSerialNumber] */
    val threadSerialNumber: Int
  ) : GcRoot()

  /**
   * Everything that called the wait() or notify() methods, or
   * that is synchronized.
   */
  class MonitorUsed(override val id: Long) : GcRoot()

  /**
   * A thread.
   *
   * Added at https://android.googlesource.com/platform/tools/base/+/c0f0d528c155cab32e372dac77370569a386245c
   */
  class ThreadObject(
    override val id: Long,
    val threadSerialNumber: Int,
    val stackTraceSerialNumber: Int
  ) : GcRoot()

  /**
   * It's unclear what this is, documentation welcome.
   */
  class ReferenceCleanup(override val id: Long) : GcRoot()

  /**
   * It's unclear what this is, documentation welcome.
   */
  class VmInternal(override val id: Long) : GcRoot()

  /**
   * It's unclear what this is, documentation welcome.
   */
  class JniMonitor(
    override val id: Long,
    val stackTraceSerialNumber: Int,
    val stackDepth: Int
  ) : GcRoot()

  /**
   * An interned string, see [java.lang.String.intern].
   */
  class InternedString(override val id: Long) : GcRoot()

  /**
   * An object that is in a queue, waiting for a finalizer to run.
   */
  class Finalizing(override val id: Long) : GcRoot()

  /**
   * An object held by a connected debugger
   */
  class Debugger(override val id: Long) : GcRoot()

  /**
   * An object that is unreachable from any other root, but not a root itself.
   */
  class Unreachable(override val id: Long) : GcRoot()
}

package shark

import java.io.Closeable
import java.io.File

/**
 * Hprof is deprecated, and we offer partial backward compatibility. Any code that was
 * previously using HprofReader directly now has to call [StreamingHprofReader.readerFor] or
 * [HprofRandomAcccessReader.readerFor]
 */
@Deprecated("Replaced by HprofStreamingReader.readerFor or HprofRandomAccessReader.openReaderFor")
class Hprof private constructor(
  val file: File,
  val header: HprofHeader
) : Closeable {

  val reader: HprofReader = HprofReader(this)

  val heapDumpTimestamp: Long
    get() = header.heapDumpTimestamp

  val hprofVersion: HprofVersion
    get() = HprofVersion.valueOf(header.version.name)

  val fileLength: Long
    get() = file.length()

  private val closeables = mutableListOf<Closeable>()

  /**
   * Maintains backward compatibility because [Hprof.open] returns a closeable. This allows
   * consuming libraries to attach a closeable that will be closed whe [Hprof] is closed.
   */
  fun attachClosable(closeable: Closeable) {
    closeables += closeable
  }

  override fun close() {
    closeables.forEach { it.close() }
  }

  @Deprecated(message = "Moved to top level class", replaceWith = ReplaceWith("shark.HprofVersion"))
  enum class HprofVersion {
    JDK1_2_BETA3,
    JDK1_2_BETA4,
    JDK_6,
    ANDROID;

    val versionString: String
      get() = shark.HprofVersion.valueOf(name).versionString
  }

  companion object {
    @Deprecated(
      message = "Replaced by HprofStreamingReader.readerFor or HprofRandomAccessReader.openReaderFor"
    )
    fun open(hprofFile: File): Hprof = Hprof(hprofFile, HprofHeader.parseHeaderOf(hprofFile))
  }
}

package shark

import shark.HprofHeader.Companion.parseHeaderOf
import shark.HprofRecord.HeapDumpEndRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.FieldRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.StaticFieldRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.InstanceDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ObjectArrayDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord
import shark.HprofRecord.LoadClassRecord
import shark.HprofRecord.StackFrameRecord
import shark.HprofRecord.StringRecord
import shark.StreamingRecordReaderAdapter.Companion.asStreamingRecordReader
import java.io.File

/**
 * Converts a Hprof file to another file with deobfuscated class and field names.
 */
class HprofDeobfuscator {

  /**
   * @see HprofDeobfuscator
   */
  fun deobfuscate(
    proguardMapping: ProguardMapping,
    inputHprofFile: File,
    /**
     * Optional output file. Defaults to a file in the same directory as [inputHprofFile], with
     * the same name and "-deobfuscated" prepended before the ".hprof" extension. If the file extension
     * is not ".hprof", then "-deobfuscated" is added at the end of the file.
     */
    outputHprofFile: File = File(
      inputHprofFile.parent, inputHprofFile.name.replace(
      ".hprof", "-deobfuscated.hprof"
    ).let { if (it != inputHprofFile.name) it else inputHprofFile.name + "-deobfuscated" })
  ): File {
    val (hprofStringCache, classNames, maxId) = readHprofRecords(inputHprofFile)

    return writeHprofRecords(
      inputHprofFile,
      outputHprofFile,
      proguardMapping,
      hprofStringCache,
      classNames,
      maxId + 1
    )
  }

  /**
   * Reads StringRecords and LoadClassRecord from an Hprof file and tracks maximum HprofRecord id
   * value.
   *
   * @return a Triple of: hprofStringCache map, classNames map and maxId value
   */
  private fun readHprofRecords(
    inputHprofFile: File
  ): Triple<Map<Long, String>, Map<Long, Long>, Long> {
    val hprofStringCache = mutableMapOf<Long, String>()
    val classNames = mutableMapOf<Long, Long>()

    var maxId: Long = 0

    val reader = StreamingHprofReader.readerFor(inputHprofFile).asStreamingRecordReader()
    reader.readRecords(setOf(HprofRecord::class),
      OnHprofRecordListener { _, record ->
        when (record) {
          is StringRecord -> {
            maxId = maxId.coerceAtLeast(record.id)
            hprofStringCache[record.id] = record.string
          }
          is LoadClassRecord -> {
            maxId = maxId.coerceAtLeast(record.id)
            classNames[record.id] = record.classNameStringId
          }
          is StackFrameRecord -> maxId = maxId.coerceAtLeast(record.id)
          is ObjectRecord -> {
            maxId = when (record) {
              is ClassDumpRecord -> maxId.coerceAtLeast(record.id)
              is InstanceDumpRecord -> maxId.coerceAtLeast(record.id)
              is ObjectArrayDumpRecord -> maxId.coerceAtLeast(record.id)
              is PrimitiveArrayDumpRecord -> maxId.coerceAtLeast(record.id)
            }
          }
        }
      })
    return Triple(hprofStringCache, classNames, maxId)
  }

  @Suppress("LongParameterList")
  private fun writeHprofRecords(
    inputHprofFile: File,
    outputHprofFile: File,
    proguardMapping: ProguardMapping,
    hprofStringCache: Map<Long, String>,
    classNames: Map<Long, Long>,
    firstId: Long
  ): File {
    var id = firstId

    val hprofHeader = parseHeaderOf(inputHprofFile)
    val reader =
      StreamingHprofReader.readerFor(inputHprofFile, hprofHeader).asStreamingRecordReader()
    HprofWriter.openWriterFor(
      outputHprofFile,
      hprofHeader = HprofHeader(
        identifierByteSize = hprofHeader.identifierByteSize,
        version = hprofHeader.version
      )
    ).use { writer ->
      reader.readRecords(setOf(HprofRecord::class),
        OnHprofRecordListener { _,
          record ->
          // HprofWriter automatically emits HeapDumpEndRecord, because it flushes
          // all continuous heap dump sub records as one heap dump record.
          if (record is HeapDumpEndRecord) {
            return@OnHprofRecordListener
          }

          when (record) {
            is StringRecord -> {
              writer.write(
                createDeobfuscatedStringRecord(record, proguardMapping, hprofStringCache)
              )
            }
            is ClassDumpRecord -> {
              val (recordsToWrite, maxId) = createDeobfuscatedClassDumpRecord(
                record, proguardMapping, hprofStringCache, classNames, id
              )
              id = maxId
              recordsToWrite.forEach {
                writer.write(it)
              }
            }
            else -> writer.write(record)
          }
        })
    }

    return outputHprofFile
  }

  private fun createDeobfuscatedStringRecord(
    record: StringRecord,
    proguardMapping: ProguardMapping,
    hprofStringCache: Map<Long, String>
  ): StringRecord {
    val obfuscatedName = hprofStringCache[record.id]!!
    return StringRecord(
      record.id, proguardMapping.deobfuscateClassName(obfuscatedName)
    )
  }

  /**
   * Deobfuscated ClassDumpRecord's field names. Different classes can have fields with the same
   * names. We need to generate new StringRecords in such cases.
   *
   * @return a Pair of: list of HprofRecords to write and new maxId value
   */
  private fun createDeobfuscatedClassDumpRecord(
    record: ClassDumpRecord,
    proguardMapping: ProguardMapping,
    hprofStringCache: Map<Long, String>,
    classNames: Map<Long, Long>,
    maxId: Long
  ): Pair<List<HprofRecord>, Long> {
    val recordsToWrite = mutableListOf<HprofRecord>()

    var id = maxId

    val newFields = record.fields.map { field ->
      val className = hprofStringCache[classNames[record.id]]!!
      val fieldName = hprofStringCache[field.nameStringId]!!
      val deobfuscatedName =
        proguardMapping.deobfuscateFieldName(className, fieldName)

      val newStringRecord = StringRecord(id++, deobfuscatedName)
      recordsToWrite.add(newStringRecord)

      FieldRecord(newStringRecord.id, field.type)
    }
    val newStaticFields = record.staticFields.map { field ->
      val className = hprofStringCache[classNames[record.id]]!!
      val fieldName = hprofStringCache[field.nameStringId]!!
      val deobfuscatedName =
        proguardMapping.deobfuscateFieldName(className, fieldName)

      val newStringRecord = StringRecord(id++, deobfuscatedName)
      recordsToWrite.add(newStringRecord)

      StaticFieldRecord(newStringRecord.id, field.type, field.value)
    }

    recordsToWrite.add(
      ClassDumpRecord(
        record.id,
        record.stackTraceSerialNumber,
        record.superclassId,
        record.classLoaderId,
        record.signersId,
        record.protectionDomainId,
        record.instanceSize,
        newStaticFields,
        newFields
      )
    )

    return Pair(recordsToWrite, id)
  }
}

package shark

import okio.BufferedSource
import okio.Okio
import java.io.File

/**
 * Represents the header metadata of a Hprof file.
 */
data class HprofHeader(
  /** Unix timestamp at which the heap was dumped. */
  val heapDumpTimestamp: Long = System.currentTimeMillis(),
  /** Hprof version, which is tied to the runtime where the heap was dumped. */
  val version: HprofVersion = HprofVersion.ANDROID,
  /**
   * Size of Hprof identifiers. Identifiers are used to represent UTF8 strings, objects,
   * stack traces, etc. They can have the same size as host pointers or sizeof(void*), but are not
   * required to be.
   */
  val identifierByteSize: Int = 4
) {
  /**
   * How many bytes from the beginning of the file can we find the hprof records at.
   * Version string, 0 delimiter (1 byte), identifier byte size int (4 bytes) ,timestamp long
   * (8 bytes)
   */
  val recordsPosition: Int = version.versionString.toByteArray(Charsets.UTF_8).size + 1 + 4 + 8

  companion object {
    private val supportedVersions = HprofVersion.values()
      .map { it.versionString to it }
      .toMap()

    /**
     * Reads the header of the provided [hprofFile] and returns it as a [HprofHeader]
     */
    fun parseHeaderOf(hprofFile: File): HprofHeader {
      val fileLength = hprofFile.length()
      if (fileLength == 0L) {
        throw IllegalArgumentException("Hprof file is 0 byte length")
      }
      return Okio.buffer(Okio.source(hprofFile.inputStream())).use {
        parseHeaderOf(it)
      }
    }

    /**
     * Reads the header of the provided [source] and returns it as a [HprofHeader].
     * This does not close the [source].
     */
    fun parseHeaderOf(source: BufferedSource): HprofHeader {
      require(!source.exhausted()) {
        throw IllegalArgumentException("Source has no available bytes")
      }
      val endOfVersionString = source.indexOf(0)
      val versionName = source.readUtf8(endOfVersionString)

      val version = supportedVersions[versionName]
      checkNotNull(version) {
        "Unsupported Hprof version [$versionName] not in supported list ${supportedVersions.keys}"
      }
      // Skip the 0 at the end of the version string.
      source.skip(1)
      val identifierByteSize = source.readInt()
      val heapDumpTimestamp = source.readLong()
      return HprofHeader(heapDumpTimestamp, version, identifierByteSize)
    }
  }
}

package shark

import okio.BufferedSink
import okio.Okio
import shark.HprofRecord.HeapDumpEndRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.BooleanArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ByteArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.CharArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.DoubleArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.FloatArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.IntArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.LongArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ShortArrayDump
import shark.StreamingRecordReaderAdapter.Companion.asStreamingRecordReader
import java.io.File

/**
 * Converts a Hprof file to another file with all primitive arrays replaced with arrays of zeroes,
 * which can be useful to remove PII. Char arrays are handled slightly differently because 0 would
 * be the null character so instead these become arrays of '?'.
 */
class HprofPrimitiveArrayStripper {

  /**
   * @see HprofPrimitiveArrayStripper
   */
  fun stripPrimitiveArrays(
    inputHprofFile: File,
    /**
     * Optional output file. Defaults to a file in the same directory as [inputHprofFile], with
     * the same name and "-stripped" prepended before the ".hprof" extension. If the file extension
     * is not ".hprof", then "-stripped" is added at the end of the file.
     */
    outputHprofFile: File = File(
      inputHprofFile.parent, inputHprofFile.name.replace(
      ".hprof", "-stripped.hprof"
    ).let { if (it != inputHprofFile.name) it else inputHprofFile.name + "-stripped" })
  ): File {
    stripPrimitiveArrays(
      hprofSourceProvider = FileSourceProvider(inputHprofFile),
      hprofSink = Okio.buffer(Okio.sink(outputHprofFile.outputStream()))
    )
    return outputHprofFile
  }

  /**
   * @see HprofPrimitiveArrayStripper
   */
  fun stripPrimitiveArrays(
    hprofSourceProvider: StreamingSourceProvider,
    hprofSink: BufferedSink
  ) {
    val header = hprofSourceProvider.openStreamingSource().use { HprofHeader.parseHeaderOf(it) }
    val reader =
      StreamingHprofReader.readerFor(hprofSourceProvider, header).asStreamingRecordReader()
    HprofWriter.openWriterFor(
      hprofSink,
      hprofHeader = HprofHeader(
        identifierByteSize = header.identifierByteSize,
        version = header.version
      )
    )
      .use { writer ->
        reader.readRecords(setOf(HprofRecord::class),
          OnHprofRecordListener { _,
            record ->
            // HprofWriter automatically emits HeapDumpEndRecord, because it flushes
            // all continuous heap dump sub records as one heap dump record.
            if (record is HeapDumpEndRecord) {
              return@OnHprofRecordListener
            }
            writer.write(
              when (record) {
                is BooleanArrayDump -> BooleanArrayDump(
                  record.id, record.stackTraceSerialNumber,
                  BooleanArray(record.array.size)
                )
                is CharArrayDump -> CharArrayDump(
                  record.id, record.stackTraceSerialNumber,
                  CharArray(record.array.size) {
                    '?'
                  }
                )
                is FloatArrayDump -> FloatArrayDump(
                  record.id, record.stackTraceSerialNumber,
                  FloatArray(record.array.size)
                )
                is DoubleArrayDump -> DoubleArrayDump(
                  record.id, record.stackTraceSerialNumber,
                  DoubleArray(record.array.size)
                )
                is ByteArrayDump -> ByteArrayDump(
                  record.id, record.stackTraceSerialNumber,
                  ByteArray(record.array.size)
                )
                is ShortArrayDump -> ShortArrayDump(
                  record.id, record.stackTraceSerialNumber,
                  ShortArray(record.array.size)
                )
                is IntArrayDump -> IntArrayDump(
                  record.id, record.stackTraceSerialNumber,
                  IntArray(record.array.size)
                )
                is LongArrayDump -> LongArrayDump(
                  record.id, record.stackTraceSerialNumber,
                  LongArray(record.array.size)
                )
                else -> {
                  record
                }
              }
            )
          })
      }
  }
}

package shark

import shark.StreamingRecordReaderAdapter.Companion.asStreamingRecordReader
import kotlin.reflect.KClass

@Deprecated("Replaced by HprofStreamingReader.readerFor or HprofRandomAccessReader.openReaderFor")
class HprofReader internal constructor(
  private val hprof: Hprof
) {
  val identifierByteSize: Int
    get() = hprof.header.identifierByteSize

  val startPosition: Long
    get() = hprof.header.recordsPosition.toLong()

  fun readHprofRecords(
    recordTypes: Set<KClass<out HprofRecord>>,
    listener: OnHprofRecordListener
  ) {
    val reader = StreamingHprofReader.readerFor(hprof.file, hprof.header).asStreamingRecordReader()
    reader.readRecords(recordTypes, listener)
  }
}

package shark

/**
 * A Hprof record. These data structure map 1:1 with how records are written in hprof files.
 */
sealed class HprofRecord {
  class StringRecord(
    val id: Long,
    val string: String
  ) : HprofRecord()

  class LoadClassRecord(
    val classSerialNumber: Int,
    val id: Long,
    val stackTraceSerialNumber: Int,
    val classNameStringId: Long
  ) : HprofRecord()

  /**
   * Terminates a series of heap dump segments. Concatenation of heap dump segments equals a
   * heap dump.
   */
  object HeapDumpEndRecord : HprofRecord()

  class StackFrameRecord(
    val id: Long,
    val methodNameStringId: Long,
    val methodSignatureStringId: Long,
    val sourceFileNameStringId: Long,
    val classSerialNumber: Int,
    /**
     * >0 line number
     * 0 no line information available
     * -1 unknown location
     * -2 compiled method (Not implemented)
     * -3 native method (Not implemented)
     */
    val lineNumber: Int
  ) : HprofRecord()

  class StackTraceRecord(
    val stackTraceSerialNumber: Int,
    val threadSerialNumber: Int,
    val stackFrameIds: LongArray
  ) : HprofRecord()

  sealed class HeapDumpRecord : HprofRecord() {
    class GcRootRecord(
      val gcRoot: GcRoot
    ) : HeapDumpRecord()

    sealed class ObjectRecord : HeapDumpRecord() {
      class ClassDumpRecord(
        val id: Long,
        val stackTraceSerialNumber: Int,
        val superclassId: Long,
        val classLoaderId: Long,
        val signersId: Long,
        val protectionDomainId: Long,
        val instanceSize: Int,
        val staticFields: List<StaticFieldRecord>,
        val fields: List<FieldRecord>
      ) : ObjectRecord() {
        data class StaticFieldRecord(
          val nameStringId: Long,
          val type: Int,
          val value: ValueHolder
        )

        data class FieldRecord(
          val nameStringId: Long,
          val type: Int
        )
      }

      class InstanceDumpRecord(
        val id: Long,
        val stackTraceSerialNumber: Int,
        val classId: Long,
        /**
         * Instance field values (this class, followed by super class, etc)
         */
        val fieldValues: ByteArray
      ) : ObjectRecord()

      class ObjectArrayDumpRecord(
        val id: Long,
        val stackTraceSerialNumber: Int,
        val arrayClassId: Long,
        val elementIds: LongArray
      ) : ObjectRecord()

      sealed class PrimitiveArrayDumpRecord : ObjectRecord() {
        abstract val id: Long
        abstract val stackTraceSerialNumber: Int
        abstract val size: Int

        class BooleanArrayDump(
          override val id: Long,
          override val stackTraceSerialNumber: Int,
          val array: BooleanArray
        ) : PrimitiveArrayDumpRecord() {
          override val size: Int
            get() = array.size
        }

        class CharArrayDump(
          override val id: Long,
          override val stackTraceSerialNumber: Int,
          val array: CharArray
        ) : PrimitiveArrayDumpRecord() {
          override val size: Int
            get() = array.size
        }

        class FloatArrayDump(
          override val id: Long,
          override val stackTraceSerialNumber: Int,
          val array: FloatArray
        ) : PrimitiveArrayDumpRecord() {
          override val size: Int
            get() = array.size
        }

        class DoubleArrayDump(
          override val id: Long,
          override val stackTraceSerialNumber: Int,
          val array: DoubleArray
        ) : PrimitiveArrayDumpRecord() {
          override val size: Int
            get() = array.size
        }

        class ByteArrayDump(
          override val id: Long,
          override val stackTraceSerialNumber: Int,
          val array: ByteArray
        ) : PrimitiveArrayDumpRecord() {
          override val size: Int
            get() = array.size
        }

        class ShortArrayDump(
          override val id: Long,
          override val stackTraceSerialNumber: Int,
          val array: ShortArray
        ) : PrimitiveArrayDumpRecord() {
          override val size: Int
            get() = array.size
        }

        class IntArrayDump(
          override val id: Long,
          override val stackTraceSerialNumber: Int,
          val array: IntArray
        ) : PrimitiveArrayDumpRecord() {
          override val size: Int
            get() = array.size
        }

        class LongArrayDump(
          override val id: Long,
          override val stackTraceSerialNumber: Int,
          val array: LongArray
        ) : PrimitiveArrayDumpRecord() {
          override val size: Int
            get() = array.size
        }
      }
    }

    class HeapDumpInfoRecord(
      val heapId: Int,
      val heapNameStringId: Long
    ) : HeapDumpRecord()
  }
}

package shark

import okio.BufferedSource
import shark.GcRoot.Debugger
import shark.GcRoot.Finalizing
import shark.GcRoot.InternedString
import shark.GcRoot.JavaFrame
import shark.GcRoot.JniGlobal
import shark.GcRoot.JniLocal
import shark.GcRoot.JniMonitor
import shark.GcRoot.MonitorUsed
import shark.GcRoot.NativeStack
import shark.GcRoot.ReferenceCleanup
import shark.GcRoot.StickyClass
import shark.GcRoot.ThreadBlock
import shark.GcRoot.ThreadObject
import shark.GcRoot.Unknown
import shark.GcRoot.Unreachable
import shark.GcRoot.VmInternal
import shark.HprofRecord.HeapDumpRecord.HeapDumpInfoRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.FieldRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.StaticFieldRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.InstanceDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ObjectArrayDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.BooleanArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ByteArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.CharArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.DoubleArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.FloatArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.IntArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.LongArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ShortArrayDump
import shark.HprofRecord.LoadClassRecord
import shark.HprofRecord.StackFrameRecord
import shark.HprofRecord.StackTraceRecord
import shark.HprofRecord.StringRecord
import shark.PrimitiveType.BOOLEAN
import shark.PrimitiveType.BYTE
import shark.PrimitiveType.CHAR
import shark.PrimitiveType.DOUBLE
import shark.PrimitiveType.FLOAT
import shark.PrimitiveType.INT
import shark.PrimitiveType.LONG
import shark.PrimitiveType.SHORT
import shark.ValueHolder.BooleanHolder
import shark.ValueHolder.ByteHolder
import shark.ValueHolder.CharHolder
import shark.ValueHolder.DoubleHolder
import shark.ValueHolder.FloatHolder
import shark.ValueHolder.IntHolder
import shark.ValueHolder.LongHolder
import shark.ValueHolder.ReferenceHolder
import shark.ValueHolder.ShortHolder
import java.nio.charset.Charset

/**
 * Reads hprof content from an Okio [BufferedSource].
 *
 * Binary Dump Format reference: http://hg.openjdk.java.net/jdk6/jdk6/jdk/raw-file/tip/src/share
 * /demo/jvmti/hprof/manual.html#mozTocId848088
 *
 * The Android Hprof format differs in some ways from that reference. This parser implementation
 * is largely adapted from https://android.googlesource.com/platform/tools/base/+/studio-master-dev
 * /perflib/src/main/java/com/android/tools/perflib
 *
 * Not thread safe, should be used from a single thread.
 */
@Suppress("LargeClass", "TooManyFunctions")
class HprofRecordReader internal constructor(
  header: HprofHeader,
  private val source: BufferedSource
) {

  /**
   * How many bytes this reader has read from [source]. Can only increase.
   */
  var bytesRead = 0L
    private set

  private val identifierByteSize = header.identifierByteSize

  private val typeSizes: IntArray

  init {
    val typeSizesMap =
      PrimitiveType.byteSizeByHprofType + (PrimitiveType.REFERENCE_HPROF_TYPE to identifierByteSize)

    val maxKey = typeSizesMap.keys.max()!!

    typeSizes = IntArray(maxKey + 1) { key ->
      typeSizesMap[key] ?: 0
    }
  }

  fun sizeOf(type: Int) = typeSizes[type]

  fun readStringRecord(length: Long) = StringRecord(
    id = readId(),
    string = readUtf8(length - identifierByteSize)
  )

  fun readLoadClassRecord() = LoadClassRecord(
    classSerialNumber = readInt(),
    id = readId(),
    stackTraceSerialNumber = readInt(),
    classNameStringId = readId()
  )

  fun readStackFrameRecord() = StackFrameRecord(
    id = readId(),
    methodNameStringId = readId(),
    methodSignatureStringId = readId(),
    sourceFileNameStringId = readId(),
    classSerialNumber = readInt(),
    lineNumber = readInt()
  )

  fun readStackTraceRecord() = StackTraceRecord(
    stackTraceSerialNumber = readInt(),
    threadSerialNumber = readInt(),
    stackFrameIds = readIdArray(readInt())
  )

  fun readUnknownGcRootRecord() = Unknown(id = readId())

  fun readJniGlobalGcRootRecord() = JniGlobal(
    id = readId(),
    jniGlobalRefId = readId()
  )

  fun readJniLocalGcRootRecord() = JniLocal(
    id = readId(),
    threadSerialNumber = readInt(),
    frameNumber = readInt()
  )

  fun readJavaFrameGcRootRecord() = JavaFrame(
    id = readId(),
    threadSerialNumber = readInt(),
    frameNumber = readInt()
  )

  fun readNativeStackGcRootRecord() = NativeStack(
    id = readId(),
    threadSerialNumber = readInt()
  )

  fun readStickyClassGcRootRecord() = StickyClass(id = readId())

  fun readThreadBlockGcRootRecord() = ThreadBlock(id = readId(), threadSerialNumber = readInt())

  fun readMonitorUsedGcRootRecord() = MonitorUsed(id = readId())

  fun readThreadObjectGcRootRecord() = ThreadObject(
    id = readId(),
    threadSerialNumber = readInt(),
    stackTraceSerialNumber = readInt()
  )

  fun readInternedStringGcRootRecord() = InternedString(id = readId())

  fun readFinalizingGcRootRecord() = Finalizing(id = readId())

  fun readDebuggerGcRootRecord() = Debugger(id = readId())

  fun readReferenceCleanupGcRootRecord() = ReferenceCleanup(id = readId())

  fun readVmInternalGcRootRecord() = VmInternal(id = readId())

  fun readJniMonitorGcRootRecord() = JniMonitor(
    id = readId(),
    stackTraceSerialNumber = readInt(),
    stackDepth = readInt()
  )

  fun readUnreachableGcRootRecord() = Unreachable(id = readId())

  /**
   * Reads a full instance record after a instance dump tag.
   */
  fun readInstanceDumpRecord(): InstanceDumpRecord {
    val id = readId()
    val stackTraceSerialNumber = readInt()
    val classId = readId()
    val remainingBytesInInstance = readInt()
    val fieldValues = readByteArray(remainingBytesInInstance)
    return InstanceDumpRecord(
      id = id,
      stackTraceSerialNumber = stackTraceSerialNumber,
      classId = classId,
      fieldValues = fieldValues
    )
  }

  fun readHeapDumpInfoRecord(): HeapDumpInfoRecord {
    val heapId = readInt()
    return HeapDumpInfoRecord(heapId = heapId, heapNameStringId = readId())
  }

  /**
   * Reads a full class record after a class dump tag.
   */
  fun readClassDumpRecord(): ClassDumpRecord {
    val id = readId()
    // stack trace serial number
    val stackTraceSerialNumber = readInt()
    val superclassId = readId()
    // class loader object ID
    val classLoaderId = readId()
    // signers object ID
    val signersId = readId()
    // protection domain object ID
    val protectionDomainId = readId()
    // reserved
    readId()
    // reserved
    readId()

    // instance size (in bytes)
    // Useful to compute retained size
    val instanceSize = readInt()

    // Skip over the constant pool
    val constantPoolCount = readUnsignedShort()
    for (i in 0 until constantPoolCount) {
      // constant pool index
      skip(SHORT_SIZE)
      skip(typeSizes[readUnsignedByte()])
    }

    val staticFieldCount = readUnsignedShort()
    val staticFields = ArrayList<StaticFieldRecord>(staticFieldCount)
    for (i in 0 until staticFieldCount) {

      val nameStringId = readId()
      val type = readUnsignedByte()
      val value = readValue(type)

      staticFields.add(
        StaticFieldRecord(
          nameStringId = nameStringId,
          type = type,
          value = value
        )
      )
    }

    val fieldCount = readUnsignedShort()
    val fields = ArrayList<FieldRecord>(fieldCount)
    for (i in 0 until fieldCount) {
      fields.add(FieldRecord(nameStringId = readId(), type = readUnsignedByte()))
    }

    return ClassDumpRecord(
      id = id,
      stackTraceSerialNumber = stackTraceSerialNumber,
      superclassId = superclassId,
      classLoaderId = classLoaderId,
      signersId = signersId,
      protectionDomainId = protectionDomainId,
      instanceSize = instanceSize,
      staticFields = staticFields,
      fields = fields
    )
  }

  /**
   * Reads a full primitive array record after a primitive array dump tag.
   */
  fun readPrimitiveArrayDumpRecord(): PrimitiveArrayDumpRecord {
    val id = readId()
    val stackTraceSerialNumber = readInt()
    // length
    val arrayLength = readInt()
    return when (val type = readUnsignedByte()) {
      BOOLEAN_TYPE -> BooleanArrayDump(
        id, stackTraceSerialNumber, readBooleanArray(arrayLength)
      )
      CHAR_TYPE -> CharArrayDump(
        id, stackTraceSerialNumber, readCharArray(arrayLength)
      )
      FLOAT_TYPE -> FloatArrayDump(
        id, stackTraceSerialNumber, readFloatArray(arrayLength)
      )
      DOUBLE_TYPE -> DoubleArrayDump(
        id, stackTraceSerialNumber, readDoubleArray(arrayLength)
      )
      BYTE_TYPE -> ByteArrayDump(
        id, stackTraceSerialNumber, readByteArray(arrayLength)
      )
      SHORT_TYPE -> ShortArrayDump(
        id, stackTraceSerialNumber, readShortArray(arrayLength)
      )
      INT_TYPE -> IntArrayDump(
        id, stackTraceSerialNumber, readIntArray(arrayLength)
      )
      LONG_TYPE -> LongArrayDump(
        id, stackTraceSerialNumber, readLongArray(arrayLength)
      )
      else -> throw IllegalStateException("Unexpected type $type")
    }
  }

  /**
   * Reads a full object array record after a object array dump tag.
   */
  fun readObjectArrayDumpRecord(): ObjectArrayDumpRecord {
    val id = readId()
    // stack trace serial number
    val stackTraceSerialNumber = readInt()
    val arrayLength = readInt()
    val arrayClassId = readId()
    val elementIds = readIdArray(arrayLength)
    return ObjectArrayDumpRecord(
      id = id,
      stackTraceSerialNumber = stackTraceSerialNumber,
      arrayClassId = arrayClassId,
      elementIds = elementIds
    )
  }

  fun skipClassDumpHeader() {
    skip(INT_SIZE * 2 + identifierByteSize * 7)
    skipClassDumpConstantPool()
  }

  fun skipClassDumpConstantPool() {
    // Skip over the constant pool
    val constantPoolCount = readUnsignedShort()
    for (i in 0 until constantPoolCount) {
      // constant pool index
      skip(SHORT.byteSize)
      skip(sizeOf(readUnsignedByte()))
    }
  }

  fun skipClassDumpStaticFields() {
    val staticFieldCount = readUnsignedShort()
    for (i in 0 until staticFieldCount) {
      skip(identifierByteSize)
      val type = readUnsignedByte()
      skip(
        if (type == PrimitiveType.REFERENCE_HPROF_TYPE) {
          identifierByteSize
        } else {
          PrimitiveType.byteSizeByHprofType.getValue(type)
        }
      )
    }
  }

  fun skipClassDumpFields() {
    val fieldCount = readUnsignedShort()
    skip((identifierByteSize + 1) * fieldCount)
  }

  fun skipInstanceDumpRecord() {
    skip(identifierByteSize + INT_SIZE + identifierByteSize)
    val remainingBytesInInstance = readInt()
    skip(remainingBytesInInstance)
  }

  fun skipClassDumpRecord() {
    skip(
      identifierByteSize + INT_SIZE + identifierByteSize + identifierByteSize + identifierByteSize +
        identifierByteSize + identifierByteSize + identifierByteSize + INT_SIZE
    )
    // Skip over the constant pool
    val constantPoolCount = readUnsignedShort()
    for (i in 0 until constantPoolCount) {
      // constant pool index
      skip(SHORT_SIZE)
      skip(typeSizes[readUnsignedByte()])
    }

    val staticFieldCount = readUnsignedShort()

    for (i in 0 until staticFieldCount) {
      skip(identifierByteSize)
      val type = readUnsignedByte()
      skip(typeSizes[type])
    }

    val fieldCount = readUnsignedShort()
    skip(fieldCount * (identifierByteSize + BYTE_SIZE))
  }

  fun skipObjectArrayDumpRecord() {
    skip(identifierByteSize + INT_SIZE)
    val arrayLength = readInt()
    skip(identifierByteSize + arrayLength * identifierByteSize)
  }

  fun skipPrimitiveArrayDumpRecord() {
    skip(identifierByteSize + INT_SIZE)
    val arrayLength = readInt()
    val type = readUnsignedByte()
    skip(arrayLength * typeSizes[type])
  }

  fun skipHeapDumpInfoRecord() {
    skip(identifierByteSize + identifierByteSize)
  }

  fun skip(byteCount: Int) {
    bytesRead += byteCount
    return source.skip(byteCount.toLong())
  }

  fun skip(byteCount: Long) {
    bytesRead += byteCount
    return source.skip(byteCount)
  }

  fun readUnsignedInt(): Long {
    return readInt().toLong() and INT_MASK
  }

  fun readUnsignedByte(): Int {
    return readByte().toInt() and BYTE_MASK
  }

  /**
   * Reads a value in the heap dump, which can be a reference or a primitive type.
   */
  fun readValue(type: Int): ValueHolder {
    return when (type) {
      PrimitiveType.REFERENCE_HPROF_TYPE -> ReferenceHolder(readId())
      BOOLEAN_TYPE -> BooleanHolder(readBoolean())
      CHAR_TYPE -> CharHolder(readChar())
      FLOAT_TYPE -> FloatHolder(readFloat())
      DOUBLE_TYPE -> DoubleHolder(readDouble())
      BYTE_TYPE -> ByteHolder(readByte())
      SHORT_TYPE -> ShortHolder(readShort())
      INT_TYPE -> IntHolder(readInt())
      LONG_TYPE -> LongHolder(readLong())
      else -> throw IllegalStateException("Unknown type $type")
    }
  }

  fun readShort(): Short {
    bytesRead += SHORT_SIZE
    return source.readShort()
  }

  fun readInt(): Int {
    bytesRead += INT_SIZE
    return source.readInt()
  }

  fun readIdArray(arrayLength: Int): LongArray {
    return LongArray(arrayLength) { readId() }
  }

  fun readBooleanArray(arrayLength: Int): BooleanArray {
    return BooleanArray(arrayLength) { readByte().toInt() != 0 }
  }

  fun readCharArray(arrayLength: Int): CharArray {
    return CharArray(arrayLength) {
      readChar()
    }
  }

  fun readString(
    byteCount: Int,
    charset: Charset
  ): String {
    bytesRead += byteCount
    return source.readString(byteCount.toLong(), charset)
  }

  fun readFloatArray(arrayLength: Int): FloatArray {
    return FloatArray(arrayLength) { readFloat() }
  }

  fun readDoubleArray(arrayLength: Int): DoubleArray {
    return DoubleArray(arrayLength) { readDouble() }
  }

  fun readShortArray(arrayLength: Int): ShortArray {
    return ShortArray(arrayLength) { readShort() }
  }

  fun readIntArray(arrayLength: Int): IntArray {
    return IntArray(arrayLength) { readInt() }
  }

  fun readLongArray(arrayLength: Int): LongArray {
    return LongArray(arrayLength) { readLong() }
  }

  fun readLong(): Long {
    bytesRead += LONG_SIZE
    return source.readLong()
  }

  fun readByte(): Byte {
    bytesRead += BYTE_SIZE
    return source.readByte()
  }

  fun readBoolean(): Boolean {
    bytesRead += BOOLEAN_SIZE
    return source.readByte()
      .toInt() != 0
  }

  fun readByteArray(byteCount: Int): ByteArray {
    bytesRead += byteCount
    return source.readByteArray(byteCount.toLong())
  }

  fun readChar(): Char {
    return readString(CHAR_SIZE, Charsets.UTF_16BE)[0]
  }

  fun readFloat(): Float {
    return Float.fromBits(readInt())
  }

  fun readDouble(): Double {
    return Double.fromBits(readLong())
  }

  fun readId(): Long {
    // As long as we don't interpret IDs, reading signed values here is fine.
    return when (identifierByteSize) {
      1 -> readByte().toLong()
      2 -> readShort().toLong()
      4 -> readInt().toLong()
      8 -> readLong()
      else -> throw IllegalArgumentException("ID Length must be 1, 2, 4, or 8")
    }
  }

  fun readUtf8(byteCount: Long): String {
    bytesRead += byteCount
    return source.readUtf8(byteCount)
  }

  fun readUnsignedShort(): Int {
    return readShort().toInt() and 0xFFFF
  }

  companion object {
    private val BOOLEAN_SIZE = BOOLEAN.byteSize
    private val CHAR_SIZE = CHAR.byteSize
    private val BYTE_SIZE = BYTE.byteSize
    private val SHORT_SIZE = SHORT.byteSize
    private val INT_SIZE = INT.byteSize
    private val LONG_SIZE = LONG.byteSize

    private val BOOLEAN_TYPE = BOOLEAN.hprofType
    private val CHAR_TYPE = CHAR.hprofType
    private val FLOAT_TYPE = FLOAT.hprofType
    private val DOUBLE_TYPE = DOUBLE.hprofType
    private val BYTE_TYPE = BYTE.hprofType
    private val SHORT_TYPE = SHORT.hprofType
    private val INT_TYPE = INT.hprofType
    private val LONG_TYPE = LONG.hprofType

    private const val INT_MASK = 0xffffffffL
    private const val BYTE_MASK = 0xff
  }
}

package shark

import java.util.EnumSet

enum class HprofRecordTag(val tag: Int) {
  STRING_IN_UTF8(0x01),
  LOAD_CLASS(0x02),

  // Currently ignored
  UNLOAD_CLASS(0x03),
  STACK_FRAME(0x04),
  STACK_TRACE(0x05),

  // Currently ignored
  ALLOC_SITES(0x06),

  // Currently ignored
  HEAP_SUMMARY(0x07),

  // Currently ignored
  START_THREAD(0x0a),

  // Currently ignored
  END_THREAD(0x0b),

  // Currently not reported
  HEAP_DUMP(0x0c),

  // Currently not reported
  HEAP_DUMP_SEGMENT(0x1c),
  HEAP_DUMP_END(0x2c),

  // Currently ignored
  CPU_SAMPLES(0x0d),

  // Currently ignored
  CONTROL_SETTINGS(0x0e),
  ROOT_UNKNOWN(0xff),
  ROOT_JNI_GLOBAL(0x01),
  ROOT_JNI_LOCAL(0x02),
  ROOT_JAVA_FRAME(0x03),
  ROOT_NATIVE_STACK(0x04),
  ROOT_STICKY_CLASS(0x05),

  // An object that was referenced from an active thread block.
  ROOT_THREAD_BLOCK(0x06),
  ROOT_MONITOR_USED(0x07),
  ROOT_THREAD_OBJECT(0x08),

  /**
   * Android format addition
   *
   * Specifies information about which heap certain objects came from. When a sub-tag of this type
   * appears in a HPROF_HEAP_DUMP or HPROF_HEAP_DUMP_SEGMENT record, entries that follow it will
   * be associated with the specified heap.  The HEAP_DUMP_INFO data is reset at the end of the
   * HEAP_DUMP[_SEGMENT].  Multiple HEAP_DUMP_INFO entries may appear in a single
   * HEAP_DUMP[_SEGMENT].
   *
   * Format: u1: Tag value (0xFE) u4: heap ID ID: heap name string ID
   */
  HEAP_DUMP_INFO(0xfe),
  ROOT_INTERNED_STRING(0x89),
  ROOT_FINALIZING(0x8a),
  ROOT_DEBUGGER(0x8b),
  ROOT_REFERENCE_CLEANUP(0x8c),
  ROOT_VM_INTERNAL(0x8d),
  ROOT_JNI_MONITOR(0x8e),
  ROOT_UNREACHABLE(0x90),

  // Not supported.
  PRIMITIVE_ARRAY_NODATA(0xc3),
  CLASS_DUMP(0x20),
  INSTANCE_DUMP(0x21),
  OBJECT_ARRAY_DUMP(0x22),
  PRIMITIVE_ARRAY_DUMP(0x23),
  ;

  companion object {
    val rootTags: EnumSet<HprofRecordTag> = EnumSet.of(
      ROOT_UNKNOWN,
      ROOT_JNI_GLOBAL,
      ROOT_JNI_LOCAL,
      ROOT_JAVA_FRAME,
      ROOT_NATIVE_STACK,
      ROOT_STICKY_CLASS,
      ROOT_THREAD_BLOCK,
      ROOT_MONITOR_USED,
      ROOT_THREAD_OBJECT,
      ROOT_INTERNED_STRING,
      ROOT_FINALIZING,
      ROOT_DEBUGGER,
      ROOT_REFERENCE_CLEANUP,
      ROOT_VM_INTERNAL,
      ROOT_JNI_MONITOR,
      ROOT_UNREACHABLE
    )
  }
}

package shark

/**
 * Supported hprof versions
 */
enum class HprofVersion(val versionString: String) {
  JDK1_2_BETA3("JAVA PROFILE 1.0"),
  JDK1_2_BETA4("JAVA PROFILE 1.0.1"),
  JDK_6("JAVA PROFILE 1.0.2"),
  ANDROID("JAVA PROFILE 1.0.3")
}

package shark

import okio.Buffer
import okio.BufferedSink
import okio.Okio
import shark.GcRoot.Debugger
import shark.GcRoot.Finalizing
import shark.GcRoot.InternedString
import shark.GcRoot.JavaFrame
import shark.GcRoot.JniGlobal
import shark.GcRoot.JniLocal
import shark.GcRoot.JniMonitor
import shark.GcRoot.MonitorUsed
import shark.GcRoot.NativeStack
import shark.GcRoot.ReferenceCleanup
import shark.GcRoot.StickyClass
import shark.GcRoot.ThreadBlock
import shark.GcRoot.ThreadObject
import shark.GcRoot.Unknown
import shark.GcRoot.Unreachable
import shark.GcRoot.VmInternal
import shark.HprofRecord.HeapDumpEndRecord
import shark.HprofRecord.HeapDumpRecord.GcRootRecord
import shark.HprofRecord.HeapDumpRecord.HeapDumpInfoRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.InstanceDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ObjectArrayDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.BooleanArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ByteArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.CharArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.DoubleArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.FloatArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.IntArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.LongArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ShortArrayDump
import shark.HprofRecord.LoadClassRecord
import shark.HprofRecord.StackTraceRecord
import shark.HprofRecord.StringRecord
import shark.PrimitiveType.BOOLEAN
import shark.PrimitiveType.BYTE
import shark.PrimitiveType.CHAR
import shark.PrimitiveType.DOUBLE
import shark.PrimitiveType.FLOAT
import shark.PrimitiveType.INT
import shark.PrimitiveType.LONG
import shark.PrimitiveType.SHORT
import shark.ValueHolder.BooleanHolder
import shark.ValueHolder.ByteHolder
import shark.ValueHolder.CharHolder
import shark.ValueHolder.DoubleHolder
import shark.ValueHolder.FloatHolder
import shark.ValueHolder.IntHolder
import shark.ValueHolder.LongHolder
import shark.ValueHolder.ReferenceHolder
import shark.ValueHolder.ShortHolder
import java.io.Closeable
import java.io.File

/**
 * Generates Hprof files.
 *
 * Call [openWriterFor] to obtain a new instance.
 *
 * Call [write] to add records and [close] when you're done.
 */
class HprofWriter private constructor(
  private val sink: BufferedSink,
  val hprofHeader: HprofHeader
) : Closeable {

  @Deprecated(
    "Replaced by HprofWriter.hprofHeader.identifierByteSize",
    ReplaceWith("hprofHeader.identifierByteSize")
  )
  val identifierByteSize: Int
    get() = hprofHeader.identifierByteSize

  @Deprecated(
    "Replaced by HprofWriter.hprofHeader.version",
    ReplaceWith("hprofHeader.version")
  )
  val hprofVersion: Hprof.HprofVersion
    get() = Hprof.HprofVersion.valueOf(hprofHeader.version.name)

  private val workBuffer = Buffer()

  /**
   * Appends a [HprofRecord] to the heap dump. If [record] is a [HprofRecord.HeapDumpRecord] then
   * it will not be written to an in memory buffer and written to file only when the next a record
   * that is not a [HprofRecord.HeapDumpRecord] is written or when [close] is called.
   */
  fun write(record: HprofRecord) {
    sink.write(record)
  }

  /**
   * Helper method for creating a [ByteArray] for [InstanceDumpRecord.fieldValues] from a
   * list of [ValueHolder].
   */
  fun valuesToBytes(values: List<ValueHolder>): ByteArray {
    val valuesBuffer = Buffer()
    values.forEach { value ->
      valuesBuffer.writeValue(value)
    }
    return valuesBuffer.readByteArray()
  }

  /**
   * Flushes to disk all [HprofRecord.HeapDumpRecord] that are currently written to the in memory
   * buffer, then closes the file.
   */
  override fun close() {
    sink.flushHeapBuffer()
    sink.close()
  }

  private fun BufferedSink.writeValue(wrapper: ValueHolder) {
    when (wrapper) {
      is ReferenceHolder -> writeId(wrapper.value)
      is BooleanHolder -> writeBoolean(wrapper.value)
      is CharHolder -> write(charArrayOf(wrapper.value))
      is FloatHolder -> writeFloat(wrapper.value)
      is DoubleHolder -> writeDouble(wrapper.value)
      is ByteHolder -> writeByte(wrapper.value.toInt())
      is ShortHolder -> writeShort(wrapper.value.toInt())
      is IntHolder -> writeInt(wrapper.value)
      is LongHolder -> writeLong(wrapper.value)
    }
  }

  @Suppress("LongMethod")
  private fun BufferedSink.write(record: HprofRecord) {
    when (record) {
      is StringRecord -> {
        writeNonHeapRecord(HprofRecordTag.STRING_IN_UTF8.tag) {
          writeId(record.id)
          writeUtf8(record.string)
        }
      }
      is LoadClassRecord -> {
        writeNonHeapRecord(HprofRecordTag.LOAD_CLASS.tag) {
          writeInt(record.classSerialNumber)
          writeId(record.id)
          writeInt(record.stackTraceSerialNumber)
          writeId(record.classNameStringId)
        }
      }
      is StackTraceRecord -> {
        writeNonHeapRecord(HprofRecordTag.STACK_TRACE.tag) {
          writeInt(record.stackTraceSerialNumber)
          writeInt(record.threadSerialNumber)
          writeInt(record.stackFrameIds.size)
          writeIdArray(record.stackFrameIds)
        }
      }
      is GcRootRecord -> {
        with(workBuffer) {
          when (val gcRoot = record.gcRoot) {
            is Unknown -> {
              writeByte(HprofRecordTag.ROOT_UNKNOWN.tag)
              writeId(gcRoot.id)
            }
            is JniGlobal -> {
              writeByte(
                HprofRecordTag.ROOT_JNI_GLOBAL.tag
              )
              writeId(gcRoot.id)
              writeId(gcRoot.jniGlobalRefId)
            }
            is JniLocal -> {
              writeByte(HprofRecordTag.ROOT_JNI_LOCAL.tag)
              writeId(gcRoot.id)
              writeInt(gcRoot.threadSerialNumber)
              writeInt(gcRoot.frameNumber)
            }
            is JavaFrame -> {
              writeByte(HprofRecordTag.ROOT_JAVA_FRAME.tag)
              writeId(gcRoot.id)
              writeInt(gcRoot.threadSerialNumber)
              writeInt(gcRoot.frameNumber)
            }
            is NativeStack -> {
              writeByte(HprofRecordTag.ROOT_NATIVE_STACK.tag)
              writeId(gcRoot.id)
              writeInt(gcRoot.threadSerialNumber)
            }
            is StickyClass -> {
              writeByte(HprofRecordTag.ROOT_STICKY_CLASS.tag)
              writeId(gcRoot.id)
            }
            is ThreadBlock -> {
              writeByte(HprofRecordTag.ROOT_THREAD_BLOCK.tag)
              writeId(gcRoot.id)
              writeInt(gcRoot.threadSerialNumber)
            }
            is MonitorUsed -> {
              writeByte(HprofRecordTag.ROOT_MONITOR_USED.tag)
              writeId(gcRoot.id)
            }
            is ThreadObject -> {
              writeByte(HprofRecordTag.ROOT_THREAD_OBJECT.tag)
              writeId(gcRoot.id)
              writeInt(gcRoot.threadSerialNumber)
              writeInt(gcRoot.stackTraceSerialNumber)
            }
            is ReferenceCleanup -> {
              writeByte(HprofRecordTag.ROOT_REFERENCE_CLEANUP.tag)
              writeId(gcRoot.id)
            }
            is VmInternal -> {
              writeByte(HprofRecordTag.ROOT_VM_INTERNAL.tag)
              writeId(gcRoot.id)
            }
            is JniMonitor -> {
              writeByte(HprofRecordTag.ROOT_JNI_MONITOR.tag)
              writeId(gcRoot.id)
              writeInt(gcRoot.stackTraceSerialNumber)
              writeInt(gcRoot.stackDepth)
            }
            is InternedString -> {
              writeByte(HprofRecordTag.ROOT_INTERNED_STRING.tag)
              writeId(gcRoot.id)
            }
            is Finalizing -> {
              writeByte(HprofRecordTag.ROOT_FINALIZING.tag)
              writeId(gcRoot.id)
            }
            is Debugger -> {
              writeByte(HprofRecordTag.ROOT_DEBUGGER.tag)
              writeId(gcRoot.id)
            }
            is Unreachable -> {
              writeByte(HprofRecordTag.ROOT_UNREACHABLE.tag)
              writeId(gcRoot.id)
            }
          }
        }
      }
      is ClassDumpRecord -> {
        with(workBuffer) {
          writeByte(HprofRecordTag.CLASS_DUMP.tag)
          writeId(record.id)
          writeInt(record.stackTraceSerialNumber)
          writeId(record.superclassId)
          writeId(record.classLoaderId)
          writeId(record.signersId)
          writeId(record.protectionDomainId)
          // reserved
          writeId(0)
          // reserved
          writeId(0)
          writeInt(record.instanceSize)
          // Not writing anything in the constant pool
          val constantPoolCount = 0
          writeShort(constantPoolCount)
          writeShort(record.staticFields.size)
          record.staticFields.forEach { field ->
            writeId(field.nameStringId)
            writeByte(field.type)
            writeValue(field.value)
          }
          writeShort(record.fields.size)
          record.fields.forEach { field ->
            writeId(field.nameStringId)
            writeByte(field.type)
          }
        }
      }
      is InstanceDumpRecord -> {
        with(workBuffer) {
          writeByte(HprofRecordTag.INSTANCE_DUMP.tag)
          writeId(record.id)
          writeInt(record.stackTraceSerialNumber)
          writeId(record.classId)
          writeInt(record.fieldValues.size)
          write(record.fieldValues)
        }
      }
      is ObjectArrayDumpRecord -> {
        with(workBuffer) {
          writeByte(HprofRecordTag.OBJECT_ARRAY_DUMP.tag)
          writeId(record.id)
          writeInt(record.stackTraceSerialNumber)
          writeInt(record.elementIds.size)
          writeId(record.arrayClassId)
          writeIdArray(record.elementIds)
        }
      }
      is PrimitiveArrayDumpRecord -> {
        with(workBuffer) {
          writeByte(HprofRecordTag.PRIMITIVE_ARRAY_DUMP.tag)
          writeId(record.id)
          writeInt(record.stackTraceSerialNumber)

          when (record) {
            is BooleanArrayDump -> {
              writeInt(record.array.size)
              writeByte(BOOLEAN.hprofType)
              write(record.array)
            }
            is CharArrayDump -> {
              writeInt(record.array.size)
              writeByte(CHAR.hprofType)
              write(record.array)
            }
            is FloatArrayDump -> {
              writeInt(record.array.size)
              writeByte(FLOAT.hprofType)
              write(record.array)
            }
            is DoubleArrayDump -> {
              writeInt(record.array.size)
              writeByte(DOUBLE.hprofType)
              write(record.array)
            }
            is ByteArrayDump -> {
              writeInt(record.array.size)
              writeByte(BYTE.hprofType)
              write(record.array)
            }
            is ShortArrayDump -> {
              writeInt(record.array.size)
              writeByte(SHORT.hprofType)
              write(record.array)
            }
            is IntArrayDump -> {
              writeInt(record.array.size)
              writeByte(INT.hprofType)
              write(record.array)
            }
            is LongArrayDump -> {
              writeInt(record.array.size)
              writeByte(LONG.hprofType)
              write(record.array)
            }
          }
        }
      }
      is HeapDumpInfoRecord -> {
        with(workBuffer) {
          writeByte(HprofRecordTag.HEAP_DUMP_INFO.tag)
          writeInt(record.heapId)
          writeId(record.heapNameStringId)
        }
      }
      is HeapDumpEndRecord -> {
        throw IllegalArgumentException("HprofWriter automatically emits HeapDumpEndRecord")
      }
    }
  }

  private fun BufferedSink.writeDouble(value: Double) {
    writeLong(value.toBits())
  }

  private fun BufferedSink.writeFloat(value: Float) {
    writeInt(value.toBits())
  }

  private fun BufferedSink.writeBoolean(value: Boolean) {
    writeByte(if (value) 1 else 0)
  }

  private fun BufferedSink.writeIdArray(array: LongArray) {
    array.forEach { writeId(it) }
  }

  private fun BufferedSink.write(array: BooleanArray) {
    array.forEach { writeByte(if (it) 1 else 0) }
  }

  private fun BufferedSink.write(array: CharArray) {
    writeString(String(array), Charsets.UTF_16BE)
  }

  private fun BufferedSink.write(array: FloatArray) {
    array.forEach { writeFloat(it) }
  }

  private fun BufferedSink.write(array: DoubleArray) {
    array.forEach { writeDouble(it) }
  }

  private fun BufferedSink.write(array: ShortArray) {
    array.forEach { writeShort(it.toInt()) }
  }

  private fun BufferedSink.write(array: IntArray) {
    array.forEach { writeInt(it) }
  }

  private fun BufferedSink.write(array: LongArray) {
    array.forEach { writeLong(it) }
  }

  private fun BufferedSink.writeNonHeapRecord(
    tag: Int,
    block: BufferedSink.() -> Unit
  ) {
    flushHeapBuffer()
    workBuffer.block()
    writeTagHeader(tag, workBuffer.size())
    writeAll(workBuffer)
  }

  private fun BufferedSink.flushHeapBuffer() {
    if (workBuffer.size() > 0) {
      writeTagHeader(HprofRecordTag.HEAP_DUMP.tag, workBuffer.size())
      writeAll(workBuffer)
      writeTagHeader(HprofRecordTag.HEAP_DUMP_END.tag, 0)
    }
  }

  private fun BufferedSink.writeTagHeader(
    tag: Int,
    length: Long
  ) {
    writeByte(tag)
    // number of microseconds since the time stamp in the header
    writeInt(0)
    writeInt(length.toInt())
  }

  private fun BufferedSink.writeId(id: Long) {
    when (hprofHeader.identifierByteSize) {
      1 -> writeByte(id.toInt())
      2 -> writeShort(id.toInt())
      4 -> writeInt(id.toInt())
      8 -> writeLong(id)
      else -> throw IllegalArgumentException("ID Length must be 1, 2, 4, or 8")
    }
  }

  companion object {

    fun openWriterFor(
      hprofFile: File,
      hprofHeader: HprofHeader = HprofHeader()
    ): HprofWriter {
      return openWriterFor(Okio.buffer(Okio.sink(hprofFile.outputStream())), hprofHeader)
    }

    fun openWriterFor(
      hprofSink: BufferedSink,
      hprofHeader: HprofHeader = HprofHeader()
    ): HprofWriter {
      hprofSink.writeUtf8(hprofHeader.version.versionString)
      hprofSink.writeByte(0)
      hprofSink.writeInt(hprofHeader.identifierByteSize)
      hprofSink.writeLong(hprofHeader.heapDumpTimestamp)
      return HprofWriter(hprofSink, hprofHeader)
    }

    @Deprecated(
      "Replaced by HprofWriter.openWriterFor()",
      ReplaceWith(
        "shark.HprofWriter.openWriterFor(hprofFile)"
      )
    )
    fun open(
      hprofFile: File,
      identifierByteSize: Int = 4,
      hprofVersion: Hprof.HprofVersion = Hprof.HprofVersion.ANDROID
    ): HprofWriter = openWriterFor(
      hprofFile,
      HprofHeader(
        version = HprofVersion.valueOf(hprofVersion.name),
        identifierByteSize = identifierByteSize
      )
    )
  }
}

package shark

/**
 * Listener passed in to [StreamingHprofReader.readRecords], gets notified for each [HprofRecord]
 * found in the heap dump which types is in the set of the recordTypes parameter passed to
 * [StreamingHprofReader.readRecords].
 *
 * This is a functional interface with which you can create a [OnHprofRecordListener] from a lambda.
 */
fun interface OnHprofRecordListener {
  fun onHprofRecord(
    /**
     * The position of the record in the underlying hprof file.
     */
    position: Long,
    record: HprofRecord
  )

  companion object {
    /**
     * Utility function to create a [OnHprofRecordListener] from the passed in [block] lambda
     * instead of using the anonymous `object : OnHprofRecordListener` syntax.
     *
     * Usage:
     *
     * ```kotlin
     * val listener = OnHprofRecordListener { position, record ->
     *
     * }
     * ```
     */
    inline operator fun invoke(crossinline block: (Long, HprofRecord) -> Unit): OnHprofRecordListener =
      object : OnHprofRecordListener {
        override fun onHprofRecord(
          position: Long,
          record: HprofRecord
        ) {
          block(position, record)
        }
      }
  }
}

package shark

/**
 * Listener passed in to [StreamingHprofReader.readRecords], gets notified for each
 * [HprofRecordTag] found in the heap dump.
 *
 * Listener implementations are expected to read all bytes corresponding to a given tag from the
 * provided reader before returning.
 */
interface OnHprofRecordTagListener {
  fun onHprofRecord(
    tag: HprofRecordTag,
    /**
     * Length of the record or -1 if there is the length is not known
     */
    length: Long,
    reader: HprofRecordReader
  )

  companion object {
    /**
     * Utility function to create a [OnHprofRecordTagListener] from the passed in [block] lambda
     * instead of using the anonymous `object : OnHprofRecordTagListener` syntax.
     *
     * Usage:
     *
     * ```kotlin
     * val listener = OnHprofRecordTagListener { tag, length, reader ->
     *
     * }
     * ```
     */
    inline operator fun invoke(crossinline block: (HprofRecordTag, Long, HprofRecordReader) -> Unit): OnHprofRecordTagListener =
      object : OnHprofRecordTagListener {
        override fun onHprofRecord(
          tag: HprofRecordTag,
          length: Long,
          reader: HprofRecordReader
        ) {
          block(tag, length, reader)
        }
      }
  }
}

package shark

/**
 * A primitive type in the prof.
 */
enum class PrimitiveType(
  /**
   * The hprof defined "basic type".
   */
  val hprofType: Int,
  /**
   * The size in bytes for each value of that type.
   */
  val byteSize: Int
) {
  BOOLEAN(4, 1),
  CHAR(5, 2),
  FLOAT(6, 4),
  DOUBLE(7, 8),
  BYTE(8, 1),
  SHORT(9, 2),
  INT(10, 4),
  LONG(11, 8);

  companion object {
    /**
     * The hprof defined "basic type" for references.
     */
    const val REFERENCE_HPROF_TYPE = 2

    val byteSizeByHprofType = values().map { it.hprofType to it.byteSize }.toMap()

    val primitiveTypeByHprofType = values().map { it.hprofType to it }.toMap()
  }
}

package shark

class ProguardMapping {

  // Classes can be simply stored as a obfuscatedName -> clearName
  // For fields it's a bit more complicated since we need to know
  // the class that a given field belongs to (two different classes
  // can have a field with the same name). So files are stored as:
  // obfuscatedClassName.obfuscatedFieldName -> clearFieldName
  private val obfuscatedToClearNamesMap = linkedMapOf<String, String>()

  /**
   * Returns deobfuscated class name or original string if there is no
   * mapping for given obfuscated name.
   */
  fun deobfuscateClassName(obfuscatedClassName: String): String {
    return obfuscatedToClearNamesMap[obfuscatedClassName] ?: obfuscatedClassName
  }

  /**
   * Returns deobfuscated field name or original string if there is no
   * mapping for given obfuscated name.
   */
  fun deobfuscateFieldName(
    obfuscatedClass: String,
    obfuscatedField: String
  ): String {
    return obfuscatedToClearNamesMap["$obfuscatedClass.$obfuscatedField"] ?: return obfuscatedField
  }

  /**
   * Adds entry to the obfuscatedToClearNamesMap map.
   */
  fun addMapping(
    obfuscatedName: String,
    clearName: String
  ) {
    obfuscatedToClearNamesMap[obfuscatedName] = clearName
  }
}

package shark

import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.text.ParseException

class ProguardMappingReader(
  private val proguardMappingInputStream: InputStream
) {

  @Throws(FileNotFoundException::class, IOException::class, ParseException::class)
  fun readProguardMapping(): ProguardMapping {
    val proguardMapping = ProguardMapping()
    proguardMappingInputStream.bufferedReader(Charsets.UTF_8).use { bufferedReader ->

      var currentClassName: String? = null
      while (true) {
        val line = bufferedReader.readLine()?.trim() ?: break

        if (line.isEmpty() || line.startsWith(HASH_SYMBOL)) {
          // empty line or comment
          continue
        }

        if (line.endsWith(COLON_SYMBOL)) {
          currentClassName = parseClassMapping(line, proguardMapping)
        } else if (currentClassName != null) {
          val isMethodMapping = line.contains(OPENING_PAREN_SYMBOL)
          if (!isMethodMapping) {
            parseClassField(line, currentClassName, proguardMapping)
          }
        }
      }
    }
    return proguardMapping
  }

  // classes are stored as "clearName -> obfuscatedName:"
  private fun parseClassMapping(
    line: String,
    proguardMapping: ProguardMapping
  ): String? {
    val arrowPosition = line.indexOf(ARROW_SYMBOL)
    if (arrowPosition == -1) {
      return null
    }

    val colonPosition = line.indexOf(COLON_SYMBOL, arrowPosition + ARROW_SYMBOL.length)
    if (colonPosition == -1) {
      return null
    }

    val clearClassName = line.substring(0, arrowPosition).trim()
    val obfuscatedClassName =
      line.substring(arrowPosition + ARROW_SYMBOL.length, colonPosition).trim()

    proguardMapping.addMapping(obfuscatedClassName, clearClassName)

    return obfuscatedClassName
  }

  // fields are stored as "typeName clearFieldName -> obfuscatedFieldName"
  private fun parseClassField(
    line: String,
    currentClassName: String,
    proguardMapping: ProguardMapping
  ) {
    val spacePosition = line.indexOf(SPACE_SYMBOL)
    if (spacePosition == -1) {
      return
    }

    val arrowPosition = line.indexOf(ARROW_SYMBOL, spacePosition + SPACE_SYMBOL.length)
    if (arrowPosition == -1) {
      return
    }

    val clearFieldName = line.substring(spacePosition + SPACE_SYMBOL.length, arrowPosition).trim()
    val obfuscatedFieldName = line.substring(arrowPosition + ARROW_SYMBOL.length).trim()

    proguardMapping.addMapping("$currentClassName.$obfuscatedFieldName", clearFieldName)
  }

  companion object {
    private const val HASH_SYMBOL = "#"
    private const val ARROW_SYMBOL = "->"
    private const val COLON_SYMBOL = ":"
    private const val SPACE_SYMBOL = " "
    private const val OPENING_PAREN_SYMBOL = "("
  }
}

package shark

import okio.Buffer
import java.io.Closeable
import java.io.File

/**
 * Reads records in a Hprof source, one at a time with a specific position and size.
 * Call [openReaderFor] to obtain a new instance.
 */
class RandomAccessHprofReader private constructor(
  private val source: RandomAccessSource,
  hprofHeader: HprofHeader
) : Closeable {
  private val buffer = Buffer()
  private val reader = HprofRecordReader(hprofHeader, buffer)

  /**
   * Loads [recordSize] bytes at [recordPosition] into the buffer that backs [HprofRecordReader]
   * then calls [withRecordReader] with that reader as a receiver. [withRecordReader] is expected
   * to use the receiver reader to read one record of exactly [recordSize] bytes.
   * @return the results from [withRecordReader]
   */
  fun <T> readRecord(
    recordPosition: Long,
    recordSize: Long,
    withRecordReader: HprofRecordReader.() -> T
  ): T {
    require(recordSize > 0L) {
      "recordSize $recordSize must be > 0"
    }
    var mutablePos = recordPosition
    var mutableByteCount = recordSize

    while (mutableByteCount > 0L) {
      val bytesRead = source.read(buffer, mutablePos, mutableByteCount)
      check(bytesRead > 0) {
        "Requested $mutableByteCount bytes after reading ${mutablePos - recordPosition}, got 0 bytes instead."
      }
      mutablePos += bytesRead
      mutableByteCount -= bytesRead
    }
    return withRecordReader(reader).apply {
      check(buffer.size() == 0L) {
        "Buffer not fully consumed: ${buffer.size()} bytes left"
      }
    }
  }

  override fun close() {
    source.close()
  }

  companion object {

    fun openReaderFor(
      hprofFile: File,
      hprofHeader: HprofHeader = HprofHeader.parseHeaderOf(hprofFile)
    ): RandomAccessHprofReader {
      val sourceProvider = FileSourceProvider(hprofFile)
      return openReaderFor(sourceProvider, hprofHeader)
    }

    fun openReaderFor(
      hprofSourceProvider: RandomAccessSourceProvider,
      hprofHeader: HprofHeader = hprofSourceProvider.openRandomAccessSource()
        .use { HprofHeader.parseHeaderOf(it.asStreamingSource()) }
    ): RandomAccessHprofReader {
      return RandomAccessHprofReader(hprofSourceProvider.openRandomAccessSource(), hprofHeader)
    }
  }
}

package shark

import okio.Buffer
import okio.BufferedSource
import okio.Okio
import okio.Source
import okio.Timeout
import java.io.Closeable
import java.io.IOException

interface RandomAccessSource : Closeable {
  @Throws(IOException::class)
  fun read(
    sink: Buffer,
    position: Long,
    byteCount: Long
  ): Long

  fun asStreamingSource(): BufferedSource {
    return Okio.buffer(object : Source {
      var position = 0L

      override fun timeout() = Timeout.NONE

      override fun close() {
        position = -1
      }

      override fun read(
        sink: Buffer,
        byteCount: Long
      ): Long {
        if (position == -1L) {
          throw IOException("Source closed")
        }
        val bytesRead = read(sink, position, byteCount)
        if (bytesRead == 0L) {
          return -1;
        }
        position += bytesRead
        return bytesRead
      }
    })
  }
}

package shark

import okio.Source

/**
 * Can open [RandomAccessSource] instances.
 */
interface RandomAccessSourceProvider {
  fun openRandomAccessSource(): RandomAccessSource
}

package shark

import okio.Source
import shark.HprofRecord.HeapDumpEndRecord
import shark.HprofRecord.HeapDumpRecord
import shark.HprofRecord.HeapDumpRecord.GcRootRecord
import shark.HprofRecord.HeapDumpRecord.HeapDumpInfoRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.InstanceDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ObjectArrayDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord
import shark.HprofRecord.LoadClassRecord
import shark.HprofRecord.StackFrameRecord
import shark.HprofRecord.StackTraceRecord
import shark.HprofRecord.StringRecord
import shark.HprofRecordTag.CLASS_DUMP
import shark.HprofRecordTag.HEAP_DUMP
import shark.HprofRecordTag.HEAP_DUMP_END
import shark.HprofRecordTag.HEAP_DUMP_INFO
import shark.HprofRecordTag.HEAP_DUMP_SEGMENT
import shark.HprofRecordTag.INSTANCE_DUMP
import shark.HprofRecordTag.LOAD_CLASS
import shark.HprofRecordTag.OBJECT_ARRAY_DUMP
import shark.HprofRecordTag.PRIMITIVE_ARRAY_DUMP
import shark.HprofRecordTag.PRIMITIVE_ARRAY_NODATA
import shark.HprofRecordTag.ROOT_DEBUGGER
import shark.HprofRecordTag.ROOT_FINALIZING
import shark.HprofRecordTag.ROOT_INTERNED_STRING
import shark.HprofRecordTag.ROOT_JAVA_FRAME
import shark.HprofRecordTag.ROOT_JNI_GLOBAL
import shark.HprofRecordTag.ROOT_JNI_LOCAL
import shark.HprofRecordTag.ROOT_JNI_MONITOR
import shark.HprofRecordTag.ROOT_MONITOR_USED
import shark.HprofRecordTag.ROOT_NATIVE_STACK
import shark.HprofRecordTag.ROOT_REFERENCE_CLEANUP
import shark.HprofRecordTag.ROOT_STICKY_CLASS
import shark.HprofRecordTag.ROOT_THREAD_BLOCK
import shark.HprofRecordTag.ROOT_THREAD_OBJECT
import shark.HprofRecordTag.ROOT_UNKNOWN
import shark.HprofRecordTag.ROOT_UNREACHABLE
import shark.HprofRecordTag.ROOT_VM_INTERNAL
import shark.HprofRecordTag.STACK_FRAME
import shark.HprofRecordTag.STACK_TRACE
import shark.HprofRecordTag.STRING_IN_UTF8
import shark.PrimitiveType.Companion.REFERENCE_HPROF_TYPE
import shark.PrimitiveType.INT
import java.io.File
import java.util.EnumSet
import kotlin.reflect.KClass

/**
 * Reads the entire content of a Hprof source in one fell swoop.
 * Call [readerFor] to obtain a new instance.
 */
class StreamingHprofReader private constructor(
  private val sourceProvider: StreamingSourceProvider,
  private val header: HprofHeader
) {

  /**
   * Obtains a new source to read all hprof records from and calls [listener] back for each record
   * that matches one of the provided [recordTags].
   *
   * @return the number of bytes read from the source
   */
  @Suppress("ComplexMethod", "LongMethod", "NestedBlockDepth")
  fun readRecords(
    recordTags: Set<HprofRecordTag>,
    listener: OnHprofRecordTagListener
  ): Long {
    return sourceProvider.openStreamingSource().use { source ->
      val reader = HprofRecordReader(header, source)
      reader.skip(header.recordsPosition)

      // Local ref optimizations
      val intByteSize = INT.byteSize
      val identifierByteSize = reader.sizeOf(REFERENCE_HPROF_TYPE)

      while (!source.exhausted()) {
        // type of the record
        val tag = reader.readUnsignedByte()

        // number of microseconds since the time stamp in the header
        reader.skip(intByteSize)

        // number of bytes that follow and belong to this record
        val length = reader.readUnsignedInt()

        when (tag) {
          STRING_IN_UTF8.tag -> {
            if (STRING_IN_UTF8 in recordTags) {
              listener.onHprofRecord(STRING_IN_UTF8, length, reader)
            } else {
              reader.skip(length)
            }
          }
          LOAD_CLASS.tag -> {
            if (LOAD_CLASS in recordTags) {
              listener.onHprofRecord(LOAD_CLASS, length, reader)
            } else {
              reader.skip(length)
            }
          }
          STACK_FRAME.tag -> {
            if (STACK_FRAME in recordTags) {
              listener.onHprofRecord(STACK_FRAME, length, reader)
            } else {
              reader.skip(length)
            }
          }
          STACK_TRACE.tag -> {
            if (STACK_TRACE in recordTags) {
              listener.onHprofRecord(STACK_TRACE, length, reader)
            } else {
              reader.skip(length)
            }
          }
          HEAP_DUMP.tag, HEAP_DUMP_SEGMENT.tag -> {
            val heapDumpStart = reader.bytesRead
            var previousTag = 0
            var previousTagPosition = 0L
            while (reader.bytesRead - heapDumpStart < length) {
              val heapDumpTagPosition = reader.bytesRead
              val heapDumpTag = reader.readUnsignedByte()
              when (heapDumpTag) {
                ROOT_UNKNOWN.tag -> {
                  if (ROOT_UNKNOWN in recordTags) {
                    listener.onHprofRecord(ROOT_UNKNOWN, -1, reader)
                  } else {
                    reader.skip(identifierByteSize)
                  }
                }
                ROOT_JNI_GLOBAL.tag -> {
                  if (ROOT_JNI_GLOBAL in recordTags) {
                    listener.onHprofRecord(ROOT_JNI_GLOBAL, -1, reader)
                  } else {
                    reader.skip(identifierByteSize + identifierByteSize)
                  }
                }
                ROOT_JNI_LOCAL.tag -> {
                  if (ROOT_JNI_LOCAL in recordTags) {
                    listener.onHprofRecord(ROOT_JNI_LOCAL, -1, reader)
                  } else {
                    reader.skip(identifierByteSize + intByteSize + intByteSize)
                  }
                }

                ROOT_JAVA_FRAME.tag -> {
                  if (ROOT_JAVA_FRAME in recordTags) {
                    listener.onHprofRecord(ROOT_JAVA_FRAME, -1, reader)
                  } else {
                    reader.skip(identifierByteSize + intByteSize + intByteSize)
                  }
                }

                ROOT_NATIVE_STACK.tag -> {
                  if (ROOT_NATIVE_STACK in recordTags) {
                    listener.onHprofRecord(ROOT_NATIVE_STACK, -1, reader)
                  } else {
                    reader.skip(identifierByteSize + intByteSize)
                  }
                }

                ROOT_STICKY_CLASS.tag -> {
                  if (ROOT_STICKY_CLASS in recordTags) {
                    listener.onHprofRecord(ROOT_STICKY_CLASS, -1, reader)
                  } else {
                    reader.skip(identifierByteSize)
                  }
                }
                ROOT_THREAD_BLOCK.tag -> {
                  if (ROOT_THREAD_BLOCK in recordTags) {
                    listener.onHprofRecord(ROOT_THREAD_BLOCK, -1, reader)
                  } else {
                    reader.skip(identifierByteSize + intByteSize)
                  }
                }

                ROOT_MONITOR_USED.tag -> {
                  if (ROOT_MONITOR_USED in recordTags) {
                    listener.onHprofRecord(ROOT_MONITOR_USED, -1, reader)
                  } else {
                    reader.skip(identifierByteSize)
                  }
                }

                ROOT_THREAD_OBJECT.tag -> {
                  if (ROOT_THREAD_OBJECT in recordTags) {
                    listener.onHprofRecord(ROOT_THREAD_OBJECT, -1, reader)
                  } else {
                    reader.skip(identifierByteSize + intByteSize + intByteSize)
                  }
                }

                ROOT_INTERNED_STRING.tag -> {
                  if (ROOT_INTERNED_STRING in recordTags) {
                    listener.onHprofRecord(ROOT_INTERNED_STRING, -1, reader)
                  } else {
                    reader.skip(identifierByteSize)
                  }
                }

                ROOT_FINALIZING.tag -> {
                  if (ROOT_FINALIZING in recordTags) {
                    listener.onHprofRecord(ROOT_FINALIZING, -1, reader)
                  } else {
                    reader.skip(identifierByteSize)
                  }
                }

                ROOT_DEBUGGER.tag -> {
                  if (ROOT_DEBUGGER in recordTags) {
                    listener.onHprofRecord(ROOT_DEBUGGER, -1, reader)
                  } else {
                    reader.skip(identifierByteSize)
                  }
                }

                ROOT_REFERENCE_CLEANUP.tag -> {
                  if (ROOT_REFERENCE_CLEANUP in recordTags) {
                    listener.onHprofRecord(ROOT_REFERENCE_CLEANUP, -1, reader)
                  } else {
                    reader.skip(identifierByteSize)
                  }
                }

                ROOT_VM_INTERNAL.tag -> {
                  if (ROOT_VM_INTERNAL in recordTags) {
                    listener.onHprofRecord(ROOT_VM_INTERNAL, -1, reader)
                  } else {
                    reader.skip(identifierByteSize)
                  }
                }

                ROOT_JNI_MONITOR.tag -> {
                  if (ROOT_JNI_MONITOR in recordTags) {
                    listener.onHprofRecord(ROOT_JNI_MONITOR, -1, reader)
                  } else {
                    reader.skip(identifierByteSize + intByteSize + intByteSize)
                  }
                }

                ROOT_UNREACHABLE.tag -> {
                  if (ROOT_UNREACHABLE in recordTags) {
                    listener.onHprofRecord(ROOT_UNREACHABLE, -1, reader)
                  } else {
                    reader.skip(identifierByteSize)
                  }
                }
                CLASS_DUMP.tag -> {
                  if (CLASS_DUMP in recordTags) {
                    listener.onHprofRecord(CLASS_DUMP, -1, reader)
                  } else {
                    reader.skipClassDumpRecord()
                  }
                }
                INSTANCE_DUMP.tag -> {
                  if (INSTANCE_DUMP in recordTags) {
                    listener.onHprofRecord(INSTANCE_DUMP, -1, reader)
                  } else {
                    reader.skipInstanceDumpRecord()
                  }
                }

                OBJECT_ARRAY_DUMP.tag -> {
                  if (OBJECT_ARRAY_DUMP in recordTags) {
                    listener.onHprofRecord(OBJECT_ARRAY_DUMP, -1, reader)
                  } else {
                    reader.skipObjectArrayDumpRecord()
                  }
                }

                PRIMITIVE_ARRAY_DUMP.tag -> {
                  if (PRIMITIVE_ARRAY_DUMP in recordTags) {
                    listener.onHprofRecord(PRIMITIVE_ARRAY_DUMP, -1, reader)
                  } else {
                    reader.skipPrimitiveArrayDumpRecord()
                  }
                }

                PRIMITIVE_ARRAY_NODATA.tag -> {
                  throw UnsupportedOperationException("$PRIMITIVE_ARRAY_NODATA cannot be parsed")
                }

                HEAP_DUMP_INFO.tag -> {
                  if (HEAP_DUMP_INFO in recordTags) {
                    listener.onHprofRecord(HEAP_DUMP_INFO, -1, reader)
                  } else {
                    reader.skipHeapDumpInfoRecord()
                  }
                }
                else -> throw IllegalStateException(
                  "Unknown tag ${
                    "0x%02x".format(
                      heapDumpTag
                    )
                  } at $heapDumpTagPosition after ${
                    "0x%02x".format(
                      previousTag
                    )
                  } at $previousTagPosition"
                )
              }
              previousTag = heapDumpTag
              previousTagPosition = heapDumpTagPosition
            }
          }
          HEAP_DUMP_END.tag -> {
            if (HEAP_DUMP_END in recordTags) {
              listener.onHprofRecord(HEAP_DUMP_END, length, reader)
            }
          }
          else -> {
            reader.skip(length)
          }
        }
      }
      reader.bytesRead
    }
  }

  companion object {

    /**
     * Creates a [StreamingHprofReader] for the provided [hprofFile]. [hprofHeader] will be read from
     * [hprofFile] unless you provide it.
     */
    fun readerFor(
      hprofFile: File,
      hprofHeader: HprofHeader = HprofHeader.parseHeaderOf(hprofFile)
    ): StreamingHprofReader {
      val sourceProvider = FileSourceProvider(hprofFile)
      return readerFor(sourceProvider, hprofHeader)
    }

    /**
     * Creates a [StreamingHprofReader] that will call [StreamingSourceProvider.openStreamingSource]
     * on every [readRecords] to obtain a [Source] to read the hprof data from. Before reading the
     * hprof records, [StreamingHprofReader] will skip [HprofHeader.recordsPosition] bytes.
     */
    fun readerFor(
      hprofSourceProvider: StreamingSourceProvider,
      hprofHeader: HprofHeader = hprofSourceProvider.openStreamingSource()
        .use { HprofHeader.parseHeaderOf(it) }
    ): StreamingHprofReader {
      return StreamingHprofReader(hprofSourceProvider, hprofHeader)
    }
  }
}

package shark

import shark.HprofRecord.HeapDumpEndRecord
import shark.HprofRecord.HeapDumpRecord
import shark.HprofRecord.HeapDumpRecord.GcRootRecord
import shark.HprofRecord.HeapDumpRecord.HeapDumpInfoRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.InstanceDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ObjectArrayDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord
import shark.HprofRecord.LoadClassRecord
import shark.HprofRecord.StackFrameRecord
import shark.HprofRecord.StackTraceRecord
import shark.HprofRecord.StringRecord
import shark.HprofRecordTag.CLASS_DUMP
import shark.HprofRecordTag.HEAP_DUMP_END
import shark.HprofRecordTag.HEAP_DUMP_INFO
import shark.HprofRecordTag.INSTANCE_DUMP
import shark.HprofRecordTag.LOAD_CLASS
import shark.HprofRecordTag.OBJECT_ARRAY_DUMP
import shark.HprofRecordTag.PRIMITIVE_ARRAY_DUMP
import shark.HprofRecordTag.ROOT_DEBUGGER
import shark.HprofRecordTag.ROOT_FINALIZING
import shark.HprofRecordTag.ROOT_INTERNED_STRING
import shark.HprofRecordTag.ROOT_JAVA_FRAME
import shark.HprofRecordTag.ROOT_JNI_GLOBAL
import shark.HprofRecordTag.ROOT_JNI_LOCAL
import shark.HprofRecordTag.ROOT_JNI_MONITOR
import shark.HprofRecordTag.ROOT_MONITOR_USED
import shark.HprofRecordTag.ROOT_NATIVE_STACK
import shark.HprofRecordTag.ROOT_REFERENCE_CLEANUP
import shark.HprofRecordTag.ROOT_STICKY_CLASS
import shark.HprofRecordTag.ROOT_THREAD_BLOCK
import shark.HprofRecordTag.ROOT_THREAD_OBJECT
import shark.HprofRecordTag.ROOT_UNKNOWN
import shark.HprofRecordTag.ROOT_UNREACHABLE
import shark.HprofRecordTag.ROOT_VM_INTERNAL
import shark.HprofRecordTag.STACK_FRAME
import shark.HprofRecordTag.STACK_TRACE
import shark.HprofRecordTag.STRING_IN_UTF8
import java.util.EnumSet
import kotlin.reflect.KClass

/**
 * Wraps a [StreamingHprofReader] to provide a higher level API that streams [HprofRecord]
 * instances.
 */
class StreamingRecordReaderAdapter(private val streamingHprofReader: StreamingHprofReader) {

  /**
   * Obtains a new source to read all hprof records from and calls [listener] back for each record
   * that matches one of the provided [recordTypes].
   *
   * @return the number of bytes read from the source
   */
  @Suppress("ComplexMethod", "LongMethod", "NestedBlockDepth")
  fun readRecords(
    recordTypes: Set<KClass<out HprofRecord>>,
    listener: OnHprofRecordListener
  ): Long {
    val recordTags = recordTypes.asHprofTags()
    return streamingHprofReader.readRecords(
      recordTags, OnHprofRecordTagListener { tag, length, reader ->
      when (tag) {
        STRING_IN_UTF8 -> {
          val recordPosition = reader.bytesRead
          val record = reader.readStringRecord(length)
          listener.onHprofRecord(recordPosition, record)
        }
        LOAD_CLASS -> {
          val recordPosition = reader.bytesRead
          val record = reader.readLoadClassRecord()
          listener.onHprofRecord(recordPosition, record)
        }
        STACK_FRAME -> {
          val recordPosition = reader.bytesRead
          val record = reader.readStackFrameRecord()
          listener.onHprofRecord(recordPosition, record)
        }
        STACK_TRACE -> {
          val recordPosition = reader.bytesRead
          val record = reader.readStackTraceRecord()
          listener.onHprofRecord(recordPosition, record)
        }
        ROOT_UNKNOWN -> {
          val recordPosition = reader.bytesRead
          val record = reader.readUnknownGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(record))
        }
        ROOT_JNI_GLOBAL -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readJniGlobalGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }
        ROOT_JNI_LOCAL -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readJniLocalGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }

        ROOT_JAVA_FRAME -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readJavaFrameGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }

        ROOT_NATIVE_STACK -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readNativeStackGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }

        ROOT_STICKY_CLASS -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readStickyClassGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }

        ROOT_THREAD_BLOCK -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readThreadBlockGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }

        ROOT_MONITOR_USED -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readMonitorUsedGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }

        ROOT_THREAD_OBJECT -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readThreadObjectGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }

        ROOT_INTERNED_STRING -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readInternedStringGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }

        ROOT_FINALIZING -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readFinalizingGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }

        ROOT_DEBUGGER -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readDebuggerGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }

        ROOT_REFERENCE_CLEANUP -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readReferenceCleanupGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }

        ROOT_VM_INTERNAL -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readVmInternalGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }

        ROOT_JNI_MONITOR -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readJniMonitorGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }

        ROOT_UNREACHABLE -> {
          val recordPosition = reader.bytesRead
          val gcRootRecord = reader.readUnreachableGcRootRecord()
          listener.onHprofRecord(recordPosition, GcRootRecord(gcRootRecord))
        }
        CLASS_DUMP -> {
          val recordPosition = reader.bytesRead
          val record = reader.readClassDumpRecord()
          listener.onHprofRecord(recordPosition, record)
        }
        INSTANCE_DUMP -> {
          val recordPosition = reader.bytesRead
          val record = reader.readInstanceDumpRecord()
          listener.onHprofRecord(recordPosition, record)
        }

        OBJECT_ARRAY_DUMP -> {
          val recordPosition = reader.bytesRead
          val arrayRecord = reader.readObjectArrayDumpRecord()
          listener.onHprofRecord(recordPosition, arrayRecord)
        }

        PRIMITIVE_ARRAY_DUMP -> {
          val recordPosition = reader.bytesRead
          val record = reader.readPrimitiveArrayDumpRecord()
          listener.onHprofRecord(recordPosition, record)
        }

        HEAP_DUMP_INFO -> {
          val recordPosition = reader.bytesRead
          val record = reader.readHeapDumpInfoRecord()
          listener.onHprofRecord(recordPosition, record)
        }
        HEAP_DUMP_END -> {
          val recordPosition = reader.bytesRead
          val record = HeapDumpEndRecord
          listener.onHprofRecord(recordPosition, record)
        }
        else -> error("Unexpected heap dump tag $tag at position ${reader.bytesRead}")
      }
    })
  }

  companion object {
    fun StreamingHprofReader.asStreamingRecordReader() = StreamingRecordReaderAdapter(this)

    fun Set<KClass<out HprofRecord>>.asHprofTags(): EnumSet<HprofRecordTag> {
      val recordTypes = this
      return if (HprofRecord::class in recordTypes) {
        EnumSet.allOf(HprofRecordTag::class.java)
      } else {
        EnumSet.noneOf(HprofRecordTag::class.java).apply {
          if (StringRecord::class in recordTypes) {
            add(STRING_IN_UTF8)
          }
          if (LoadClassRecord::class in recordTypes) {
            add(LOAD_CLASS)
          }
          if (HeapDumpEndRecord::class in recordTypes) {
            add(HEAP_DUMP_END)
          }
          if (StackFrameRecord::class in recordTypes) {
            add(STACK_FRAME)
          }
          if (StackTraceRecord::class in recordTypes) {
            add(STACK_TRACE)
          }
          if (HeapDumpInfoRecord::class in recordTypes) {
            add(HEAP_DUMP_INFO)
          }
          val readAllHeapDumpRecords = HeapDumpRecord::class in recordTypes
          if (readAllHeapDumpRecords || GcRootRecord::class in recordTypes) {
            addAll(HprofRecordTag.rootTags)
          }
          val readAllObjectRecords = readAllHeapDumpRecords || ObjectRecord::class in recordTypes
          if (readAllObjectRecords || ClassDumpRecord::class in recordTypes) {
            add(CLASS_DUMP)
          }
          if (readAllObjectRecords || InstanceDumpRecord::class in recordTypes) {
            add(INSTANCE_DUMP)
          }
          if (readAllObjectRecords || ObjectArrayDumpRecord::class in recordTypes) {
            add(OBJECT_ARRAY_DUMP)
          }
          if (readAllObjectRecords || PrimitiveArrayDumpRecord::class in recordTypes) {
            add(PRIMITIVE_ARRAY_DUMP)
          }
        }
      }
    }
  }
}

package shark

import okio.BufferedSource
import okio.Source

/**
 * Can open [Source] instances.
 */
fun interface StreamingSourceProvider {
  fun openStreamingSource(): BufferedSource
}

package shark

import shark.ValueHolder.ReferenceHolder

/**
 * A value in the heap dump, which can be a [ReferenceHolder] or
 * a primitive type.
 */
sealed class ValueHolder {
  data class ReferenceHolder(val value: Long) : ValueHolder() {
    val isNull
      get() = value == NULL_REFERENCE
  }

  data class BooleanHolder(val value: Boolean) : ValueHolder()
  data class CharHolder(val value: Char) : ValueHolder()
  data class FloatHolder(val value: Float) : ValueHolder()
  data class DoubleHolder(val value: Double) : ValueHolder()
  data class ByteHolder(val value: Byte) : ValueHolder()
  data class ShortHolder(val value: Short) : ValueHolder()
  data class IntHolder(val value: Int) : ValueHolder()
  data class LongHolder(val value: Long) : ValueHolder()

  companion object {
    const val NULL_REFERENCE = 0L
  }
}

package shark

/**
 * Central Logger for all Shark artifacts. Set [logger] to change where these logs go.
 */
object SharkLog {

  /**
   * @see SharkLog
   */
  interface Logger {

    /**
     * Logs a debug message formatted with the passed in arguments.
     */
    fun d(message: String)

    /**
     * Logs a [Throwable] and debug message formatted with the passed in arguments.
     */
    fun d(
      throwable: Throwable,
      message: String
    )
  }

  @Volatile var logger: Logger? = null

  /**
   * @see Logger.d
   */
  inline fun d(message: () -> String) {
    // Local variable to prevent the ref from becoming null after the null check.
    val logger = logger ?: return
    logger.d(message.invoke())
  }

  /**
   * @see Logger.d
   */
  inline fun d(
    throwable: Throwable,
    message: () -> String
  ) {
    // Local variable to prevent the ref from becoming null after the null check.
    val logger = logger ?: return
    logger.d(throwable, message.invoke())
  }
}

package shark

import shark.HeapObject.HeapInstance

/**
 * Inspector that automatically marks instances of the provided class names as not leaking
 * because they're app wide singletons.
 */
class AppSingletonInspector(private vararg val singletonClasses: String) : ObjectInspector {
  override fun inspect(
    reporter: ObjectReporter
  ) {
    if (reporter.heapObject is HeapInstance) {
      reporter.heapObject.instanceClass
        .classHierarchy
        .forEach { heapClass ->
          if (heapClass.name in singletonClasses) {
            reporter.notLeakingReasons += "${heapClass.name} is an app singleton"
          }
        }
    }
  }
}

package shark

/**
 * Finds the objects that are leaking by scanning all objects in the heap dump
 * and delegating the decision to a list of [FilteringLeakingObjectFinder.LeakingObjectFilter]
 */
class FilteringLeakingObjectFinder(private val filters: List<LeakingObjectFilter>) :
  LeakingObjectFinder {

  /**
   * Filter to be passed to the [FilteringLeakingObjectFinder] constructor.
   */
  interface LeakingObjectFilter {
    /**
     * Returns whether the passed in [heapObject] is leaking. This should only return true
     * when we're 100% sure the passed in [heapObject] should not be in memory anymore.
     */
    fun isLeakingObject(heapObject: HeapObject): Boolean
  }

  override fun findLeakingObjectIds(graph: HeapGraph): Set<Long> {
    return graph.objects
      .filter { heapObject ->
        filters.any { filter ->
          filter.isLeakingObject(heapObject)
        }
      }
      .map { it.objectId }
      .toSet()
  }
}

package shark

import shark.internal.createSHA1Hash
import java.io.File
import java.io.Serializable

/**
 * The result of an analysis performed by [HeapAnalyzer], either a [HeapAnalysisSuccess] or a
 * [HeapAnalysisFailure]. This class is serializable however there are no guarantees of forward
 * compatibility.
 */
sealed class HeapAnalysis : Serializable {
  /**
   * The hprof file that was analyzed.
   */
  abstract val heapDumpFile: File

  /**
   * The [System.currentTimeMillis] when this [HeapAnalysis] instance was created.
   */
  abstract val createdAtTimeMillis: Long

  /**
   * Total time spent dumping the heap.
   */
  abstract val dumpDurationMillis: Long

  /**
   * Total time spent analyzing the heap.
   */
  abstract val analysisDurationMillis: Long

  companion object {
    private const val serialVersionUID: Long = -8657286725869987172
    const val DUMP_DURATION_UNKNOWN: Long = -1
  }
}

/**
 * The analysis performed by [HeapAnalyzer] did not complete successfully.
 */
data class HeapAnalysisFailure(
  override val heapDumpFile: File,
  override val createdAtTimeMillis: Long,
  override val dumpDurationMillis: Long = DUMP_DURATION_UNKNOWN,
  override val analysisDurationMillis: Long,
  /**
   * An exception wrapping the actual exception that was thrown.
   */
  val exception: HeapAnalysisException
) : HeapAnalysis() {

  override fun toString(): String {
    return """====================================
HEAP ANALYSIS FAILED

You can report this failure at https://github.com/square/leakcanary/issues
Please provide the stacktrace, metadata and the heap dump file.
====================================
STACKTRACE

$exception====================================
METADATA

Build.VERSION.SDK_INT: ${androidSdkInt()}
Build.MANUFACTURER: ${androidManufacturer()}
LeakCanary version: ${leakCanaryVersion()}
Analysis duration: $analysisDurationMillis ms
Heap dump file path: ${heapDumpFile.absolutePath}
Heap dump timestamp: $createdAtTimeMillis
===================================="""
  }

  companion object {
    private const val serialVersionUID: Long = 8483254400637792414
  }
}

/**
 * The result of a successful heap analysis performed by [HeapAnalyzer].
 */
data class HeapAnalysisSuccess(
  override val heapDumpFile: File,
  override val createdAtTimeMillis: Long,
  override val dumpDurationMillis: Long = DUMP_DURATION_UNKNOWN,
  override val analysisDurationMillis: Long,
  val metadata: Map<String, String>,
  /**
   * The list of [ApplicationLeak] found in the heap dump by [HeapAnalyzer].
   */
  val applicationLeaks: List<ApplicationLeak>,
  /**
   * The list of [LibraryLeak] found in the heap dump by [HeapAnalyzer].
   */
  val libraryLeaks: List<LibraryLeak>,
  val unreachableObjects: List<LeakTraceObject>
) : HeapAnalysis() {
  /**
   * The list of [Leak] found in the heap dump by [HeapAnalyzer], ie all [applicationLeaks] and
   * all [libraryLeaks] in one list.
   */
  val allLeaks: Sequence<Leak>
    get() = applicationLeaks.asSequence() + libraryLeaks.asSequence()

  override fun toString(): String {
    return """====================================
HEAP ANALYSIS RESULT
====================================
${applicationLeaks.size} APPLICATION LEAKS

References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
${
      if (applicationLeaks.isNotEmpty()) "\n" + applicationLeaks.joinToString(
        "\n\n"
      ) + "\n" else ""
    }====================================
${libraryLeaks.size} LIBRARY LEAKS

A Library Leak is a leak caused by a known bug in 3rd party code that you do not have control over.
See https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks
${
      if (libraryLeaks.isNotEmpty()) "\n" + libraryLeaks.joinToString(
        "\n\n"
      ) + "\n" else ""
    }====================================
${unreachableObjects.size} UNREACHABLE OBJECTS

An unreachable object is still in memory but LeakCanary could not find a strong reference path
from GC roots.
${
      if (unreachableObjects.isNotEmpty()) "\n" + unreachableObjects.joinToString(
        "\n\n"
      ) + "\n" else ""
    }====================================
METADATA

Please include this in bug reports and Stack Overflow questions.
${
      if (metadata.isNotEmpty()) "\n" + metadata.map { "${it.key}: ${it.value}" }.joinToString(
        "\n"
      ) else ""
    }
Analysis duration: $analysisDurationMillis ms
Heap dump file path: ${heapDumpFile.absolutePath}
Heap dump timestamp: $createdAtTimeMillis
Heap dump duration: ${if (dumpDurationMillis != DUMP_DURATION_UNKNOWN) "$dumpDurationMillis ms" else "Unknown"}
===================================="""
  }

  companion object {
    private const val serialVersionUID: Long = 130453013437459642
  }
}

/**
 * A leak found by [HeapAnalyzer], either an [ApplicationLeak] or a [LibraryLeak].
 */
sealed class Leak : Serializable {

  /**
   * Group of leak traces which share the same leak signature.
   */
  abstract val leakTraces: List<LeakTrace>

  /**
   * Sum of [LeakTrace.retainedHeapByteSize] for all elements in [leakTraces].
   * Null if the retained heap size was not computed.
   */
  val totalRetainedHeapByteSize: Int?
    get() = if (leakTraces.first().retainedHeapByteSize == null) {
      null
    } else {
      leakTraces.sumBy { it.retainedHeapByteSize!! }
    }

  /**
   * Sum of [LeakTrace.retainedObjectCount] for all elements in [leakTraces].
   * Null if the retained heap size was not computed.
   */
  val totalRetainedObjectCount: Int?
    get() = if (leakTraces.first().retainedObjectCount == null) {
      null
    } else {
      leakTraces.sumBy { it.retainedObjectCount!! }
    }

  /**
   * A unique SHA1 hash that represents this group of leak traces.
   *
   * For [ApplicationLeak] this is based on [LeakTrace.signature] and for [LibraryLeak] this is
   * based on [LibraryLeak.pattern].
   */
  abstract val signature: String

  abstract val shortDescription: String

  override fun toString(): String {
    return (if (totalRetainedHeapByteSize != null) "$totalRetainedHeapByteSize bytes retained by leaking objects\n" else "") +
      (if (leakTraces.size > 1) "Displaying only 1 leak trace out of ${leakTraces.size} with the same signature\n" else "") +
      "Signature: $signature\n" +
      leakTraces.first()
  }

  companion object {
    private const val serialVersionUID: Long = -2287572510360910916
  }
}

/**
 * A leak found by [HeapAnalyzer], where the only path to the leaking object required going
 * through a reference matched by [pattern], as provided to a [LibraryLeakReferenceMatcher]
 * instance. This is a known leak in library code that is beyond your control.
 */
data class LibraryLeak(
  override val leakTraces: List<LeakTrace>,
  /**
   * The pattern that matched one of the references in each of [leakTraces], as provided to a
   * [LibraryLeakReferenceMatcher] instance.
   */
  val pattern: ReferencePattern,
  /**
   * A description that conveys what we know about this library leak.
   */
  val description: String
) : Leak() {
  override val signature: String
    get() = pattern.toString().createSHA1Hash()

  override val shortDescription: String
    get() = pattern.toString()

  override fun toString(): String {
    return """Leak pattern: $pattern
Description: $description
${super.toString()}
"""
  }

  companion object {
    private const val serialVersionUID: Long = 3943636164568681903
  }
}

/**
 * A leak found by [HeapAnalyzer] in your application.
 */
data class ApplicationLeak(
  override val leakTraces: List<LeakTrace>
) : Leak() {
  override val signature: String
    get() = leakTraces.first().signature

  override val shortDescription: String
    get() {
      val leakTrace = leakTraces.first()
      return leakTrace.suspectReferenceSubpath.firstOrNull()?.let { firstSuspectReferencePath ->
        val referenceName = firstSuspectReferencePath.referenceGenericName
        firstSuspectReferencePath.originObject.classSimpleName + "." + referenceName
      } ?: leakTrace.leakingObject.className
    }

  // Override required to avoid the default toString() from data classes
  override fun toString(): String {
    return super.toString()
  }

  companion object {
    private const val serialVersionUID: Long = 524928276700576863
  }
}

private fun androidSdkInt(): Int {
  return try {
    val versionClass = Class.forName("android.os.Build\$VERSION")
    val sdkIntField = versionClass.getDeclaredField("SDK_INT")
    sdkIntField.get(null) as Int
  } catch (e: Exception) {
    -1
  }
}

private fun androidManufacturer(): String {
  return try {
    val buildClass = Class.forName("android.os.Build")
    val manufacturerField = buildClass.getDeclaredField("MANUFACTURER")
    manufacturerField.get(null) as String
  } catch (e: Exception) {
    "Unknown"
  }
}

private fun leakCanaryVersion(): String {
  return try {
    val versionHolderClass = Class.forName("leakcanary.internal.InternalLeakCanary")
    val versionField = versionHolderClass.getDeclaredField("version")
    versionField.isAccessible = true
    versionField.get(null) as String
  } catch (e: Exception) {
    "Unknown"
  }
}

package shark

import java.io.PrintWriter
import java.io.StringWriter

class HeapAnalysisException(cause: Throwable) : RuntimeException(cause) {

  override fun toString(): String {
    val stringWriter = StringWriter()
    cause!!.printStackTrace(PrintWriter(stringWriter))
    return stringWriter.toString()
  }

  companion object {
    private const val serialVersionUID: Long = -2522323377375290608
  }
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package shark

import shark.HeapAnalyzer.TrieNode.LeafNode
import shark.HeapAnalyzer.TrieNode.ParentNode
import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import shark.HeapObject.HeapObjectArray
import shark.HeapObject.HeapPrimitiveArray
import shark.HprofHeapGraph.Companion.openHeapGraph
import shark.LeakTrace.GcRootType
import shark.LeakTraceObject.LeakingStatus
import shark.LeakTraceObject.LeakingStatus.LEAKING
import shark.LeakTraceObject.LeakingStatus.NOT_LEAKING
import shark.LeakTraceObject.LeakingStatus.UNKNOWN
import shark.LeakTraceObject.ObjectType.ARRAY
import shark.LeakTraceObject.ObjectType.CLASS
import shark.LeakTraceObject.ObjectType.INSTANCE
import shark.OnAnalysisProgressListener.Step.BUILDING_LEAK_TRACES
import shark.OnAnalysisProgressListener.Step.COMPUTING_NATIVE_RETAINED_SIZE
import shark.OnAnalysisProgressListener.Step.COMPUTING_RETAINED_SIZE
import shark.OnAnalysisProgressListener.Step.EXTRACTING_METADATA
import shark.OnAnalysisProgressListener.Step.FINDING_RETAINED_OBJECTS
import shark.OnAnalysisProgressListener.Step.INSPECTING_OBJECTS
import shark.OnAnalysisProgressListener.Step.PARSING_HEAP_DUMP
import shark.internal.AndroidNativeSizeMapper
import shark.internal.DominatorTree
import shark.internal.KeyedWeakReferenceMirror
import shark.internal.PathFinder
import shark.internal.PathFinder.PathFindingResults
import shark.internal.ReferencePathNode
import shark.internal.ReferencePathNode.ChildNode
import shark.internal.ReferencePathNode.LibraryLeakNode
import shark.internal.ReferencePathNode.RootNode
import shark.internal.ShallowSizeCalculator
import shark.internal.createSHA1Hash
import shark.internal.lastSegment
import java.io.File
import java.util.ArrayList
import java.util.concurrent.TimeUnit.NANOSECONDS

/**
 * Analyzes heap dumps to look for leaks.
 */
class HeapAnalyzer constructor(
  private val listener: OnAnalysisProgressListener
) {

  private class FindLeakInput(
    val graph: HeapGraph,
    val referenceMatchers: List<ReferenceMatcher>,
    val computeRetainedHeapSize: Boolean,
    val objectInspectors: List<ObjectInspector>
  )

  /**
   * Searches the heap dump for leaking instances and then computes the shortest strong reference
   * path from those instances to the GC roots.
   */
  fun analyze(
    heapDumpFile: File,
    leakingObjectFinder: LeakingObjectFinder,
    referenceMatchers: List<ReferenceMatcher> = emptyList(),
    computeRetainedHeapSize: Boolean = false,
    objectInspectors: List<ObjectInspector> = emptyList(),
    metadataExtractor: MetadataExtractor = MetadataExtractor.NO_OP,
    proguardMapping: ProguardMapping? = null
  ): HeapAnalysis {
    val analysisStartNanoTime = System.nanoTime()

    if (!heapDumpFile.exists()) {
      val exception = IllegalArgumentException("File does not exist: $heapDumpFile")
      return HeapAnalysisFailure(
        heapDumpFile = heapDumpFile,
        createdAtTimeMillis = System.currentTimeMillis(),
        analysisDurationMillis = since(analysisStartNanoTime),
        exception = HeapAnalysisException(exception)
      )
    }

    return try {
      listener.onAnalysisProgress(PARSING_HEAP_DUMP)
      val sourceProvider = ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(heapDumpFile))
      sourceProvider.openHeapGraph(proguardMapping).use { graph ->
        val helpers =
          FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)
        val result = helpers.analyzeGraph(
          metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime
        )
        val lruCacheStats = (graph as HprofHeapGraph).lruCacheStats()
        val randomAccessStats =
          "RandomAccess[" +
            "bytes=${sourceProvider.randomAccessByteReads}," +
            "reads=${sourceProvider.randomAccessReadCount}," +
            "travel=${sourceProvider.randomAccessByteTravel}," +
            "range=${sourceProvider.byteTravelRange}," +
            "size=${heapDumpFile.length()}" +
            "]"
        val stats = "$lruCacheStats $randomAccessStats"
        result.copy(metadata = result.metadata + ("Stats" to stats))
      }
    } catch (exception: Throwable) {
      HeapAnalysisFailure(
        heapDumpFile = heapDumpFile,
        createdAtTimeMillis = System.currentTimeMillis(),
        analysisDurationMillis = since(analysisStartNanoTime),
        exception = HeapAnalysisException(exception)
      )
    }
  }

  fun analyze(
    heapDumpFile: File,
    graph: HeapGraph,
    leakingObjectFinder: LeakingObjectFinder,
    referenceMatchers: List<ReferenceMatcher> = emptyList(),
    computeRetainedHeapSize: Boolean = false,
    objectInspectors: List<ObjectInspector> = emptyList(),
    metadataExtractor: MetadataExtractor = MetadataExtractor.NO_OP
  ): HeapAnalysis {
    val analysisStartNanoTime = System.nanoTime()
    return try {
      val helpers =
        FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)
      helpers.analyzeGraph(
        metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime
      )
    } catch (exception: Throwable) {
      HeapAnalysisFailure(
        heapDumpFile = heapDumpFile,
        createdAtTimeMillis = System.currentTimeMillis(),
        analysisDurationMillis = since(analysisStartNanoTime),
        exception = HeapAnalysisException(exception)
      )
    }
  }

  private fun FindLeakInput.analyzeGraph(
    metadataExtractor: MetadataExtractor,
    leakingObjectFinder: LeakingObjectFinder,
    heapDumpFile: File,
    analysisStartNanoTime: Long
  ): HeapAnalysisSuccess {
    listener.onAnalysisProgress(EXTRACTING_METADATA)
    val metadata = metadataExtractor.extractMetadata(graph)

    val retainedClearedWeakRefCount = KeyedWeakReferenceFinder.findKeyedWeakReferences(graph)
      .filter { it.isRetained && !it.hasReferent }.count()

    // This should rarely happens, as we generally remove all cleared weak refs right before a heap
    // dump.
    val metadataWithCount = if (retainedClearedWeakRefCount > 0) {
      metadata + ("Count of retained yet cleared" to "$retainedClearedWeakRefCount KeyedWeakReference instances")
    } else {
      metadata
    }

    listener.onAnalysisProgress(FINDING_RETAINED_OBJECTS)
    val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph)

    val (applicationLeaks, libraryLeaks, unreachableObjects) = findLeaks(leakingObjectIds)

    return HeapAnalysisSuccess(
      heapDumpFile = heapDumpFile,
      createdAtTimeMillis = System.currentTimeMillis(),
      analysisDurationMillis = since(analysisStartNanoTime),
      metadata = metadataWithCount,
      applicationLeaks = applicationLeaks,
      libraryLeaks = libraryLeaks,
      unreachableObjects = unreachableObjects
    )
  }

  private data class LeaksAndUnreachableObjects(
    val applicationLeaks: List<ApplicationLeak>,
    val libraryLeaks: List<LibraryLeak>,
    val unreachableObjects: List<LeakTraceObject>
  )

  private fun FindLeakInput.findLeaks(leakingObjectIds: Set<Long>): LeaksAndUnreachableObjects {
    val pathFinder = PathFinder(graph, listener, referenceMatchers)
    val pathFindingResults =
      pathFinder.findPathsFromGcRoots(leakingObjectIds, computeRetainedHeapSize)

    val unreachableObjects = findUnreachableObjects(pathFindingResults, leakingObjectIds)

    val shortestPaths =
      deduplicateShortestPaths(pathFindingResults.pathsToLeakingObjects)

    val inspectedObjectsByPath = inspectObjects(shortestPaths)

    val retainedSizes =
      if (pathFindingResults.dominatorTree != null) {
        computeRetainedSizes(inspectedObjectsByPath, pathFindingResults.dominatorTree)
      } else {
        null
      }
    val (applicationLeaks, libraryLeaks) = buildLeakTraces(
      shortestPaths, inspectedObjectsByPath, retainedSizes
    )
    return LeaksAndUnreachableObjects(applicationLeaks, libraryLeaks, unreachableObjects)
  }

  private fun FindLeakInput.findUnreachableObjects(
    pathFindingResults: PathFindingResults,
    leakingObjectIds: Set<Long>
  ): List<LeakTraceObject> {
    val reachableLeakingObjectIds =
      pathFindingResults.pathsToLeakingObjects.map { it.objectId }.toSet()

    val unreachableLeakingObjectIds = leakingObjectIds - reachableLeakingObjectIds

    val unreachableObjectReporters = unreachableLeakingObjectIds.map { objectId ->
      ObjectReporter(heapObject = graph.findObjectById(objectId))
    }

    objectInspectors.forEach { inspector ->
      unreachableObjectReporters.forEach { reporter ->
        inspector.inspect(reporter)
      }
    }

    val unreachableInspectedObjects = unreachableObjectReporters.map { reporter ->
      val reason = resolveStatus(reporter, leakingWins = true).let { (status, reason) ->
        when (status) {
          LEAKING -> reason
          UNKNOWN -> "This is a leaking object"
          NOT_LEAKING -> "This is a leaking object. Conflicts with $reason"
        }
      }
      InspectedObject(
        reporter.heapObject, LEAKING, reason, reporter.labels
      )
    }

    return buildLeakTraceObjects(unreachableInspectedObjects, null)
  }

  internal sealed class TrieNode {
    abstract val objectId: Long

    class ParentNode(override val objectId: Long) : TrieNode() {
      val children = mutableMapOf<Long, TrieNode>()
      override fun toString(): String {
        return "ParentNode(objectId=$objectId, children=$children)"
      }
    }

    class LeafNode(
      override val objectId: Long,
      val pathNode: ReferencePathNode
    ) : TrieNode()
  }

  private fun deduplicateShortestPaths(
    inputPathResults: List<ReferencePathNode>
  ): List<ShortestPath> {
    val rootTrieNode = ParentNode(0)

    inputPathResults.forEach { pathNode ->
      // Go through the linked list of nodes and build the reverse list of instances from
      // root to leaking.
      val path = mutableListOf<Long>()
      var leakNode: ReferencePathNode = pathNode
      while (leakNode is ChildNode) {
        path.add(0, leakNode.objectId)
        leakNode = leakNode.parent
      }
      path.add(0, leakNode.objectId)
      updateTrie(pathNode, path, 0, rootTrieNode)
    }

    val outputPathResults = mutableListOf<ReferencePathNode>()
    findResultsInTrie(rootTrieNode, outputPathResults)

    if (outputPathResults.size != inputPathResults.size) {
      SharkLog.d {
        "Found ${inputPathResults.size} paths to retained objects," +
          " down to ${outputPathResults.size} after removing duplicated paths"
      }
    } else {
      SharkLog.d { "Found ${outputPathResults.size} paths to retained objects" }
    }

    return outputPathResults.map { retainedObjectNode ->
      val shortestChildPath = mutableListOf<ChildNode>()
      var node = retainedObjectNode
      while (node is ChildNode) {
        shortestChildPath.add(0, node)
        node = node.parent
      }
      val rootNode = node as RootNode
      ShortestPath(rootNode, shortestChildPath)
    }
  }

  private fun updateTrie(
    pathNode: ReferencePathNode,
    path: List<Long>,
    pathIndex: Int,
    parentNode: ParentNode
  ) {
    val objectId = path[pathIndex]
    if (pathIndex == path.lastIndex) {
      parentNode.children[objectId] = LeafNode(objectId, pathNode)
    } else {
      val childNode = parentNode.children[objectId] ?: {
        val newChildNode = ParentNode(objectId)
        parentNode.children[objectId] = newChildNode
        newChildNode
      }()
      if (childNode is ParentNode) {
        updateTrie(pathNode, path, pathIndex + 1, childNode)
      }
    }
  }

  private fun findResultsInTrie(
    parentNode: ParentNode,
    outputPathResults: MutableList<ReferencePathNode>
  ) {
    parentNode.children.values.forEach { childNode ->
      when (childNode) {
        is ParentNode -> {
          findResultsInTrie(childNode, outputPathResults)
        }
        is LeafNode -> {
          outputPathResults += childNode.pathNode
        }
      }
    }
  }

  internal class ShortestPath(
    val root: RootNode,
    val childPath: List<ChildNode>
  ) {
    fun asList() = listOf(root) + childPath
  }

  private fun FindLeakInput.buildLeakTraces(
    shortestPaths: List<ShortestPath>,
    inspectedObjectsByPath: List<List<InspectedObject>>,
    retainedSizes: Map<Long, Pair<Int, Int>>?
  ): Pair<List<ApplicationLeak>, List<LibraryLeak>> {
    listener.onAnalysisProgress(BUILDING_LEAK_TRACES)

    val applicationLeaksMap = mutableMapOf<String, MutableList<LeakTrace>>()
    val libraryLeaksMap =
      mutableMapOf<String, Pair<LibraryLeakReferenceMatcher, MutableList<LeakTrace>>>()

    shortestPaths.forEachIndexed { pathIndex, shortestPath ->
      val inspectedObjects = inspectedObjectsByPath[pathIndex]

      val leakTraceObjects = buildLeakTraceObjects(inspectedObjects, retainedSizes)

      val referencePath = buildReferencePath(shortestPath.childPath, leakTraceObjects)

      val leakTrace = LeakTrace(
        gcRootType = GcRootType.fromGcRoot(shortestPath.root.gcRoot),
        referencePath = referencePath,
        leakingObject = leakTraceObjects.last()
      )

      val firstLibraryLeakNode = if (shortestPath.root is LibraryLeakNode) {
        shortestPath.root
      } else {
        shortestPath.childPath.firstOrNull { it is LibraryLeakNode } as LibraryLeakNode?
      }

      if (firstLibraryLeakNode != null) {
        val matcher = firstLibraryLeakNode.matcher
        val signature: String = matcher.pattern.toString()
          .createSHA1Hash()
        libraryLeaksMap.getOrPut(signature) { matcher to mutableListOf() }
          .second += leakTrace
      } else {
        applicationLeaksMap.getOrPut(leakTrace.signature) { mutableListOf() } += leakTrace
      }
    }
    val applicationLeaks = applicationLeaksMap.map { (_, leakTraces) ->
      ApplicationLeak(leakTraces)
    }
    val libraryLeaks = libraryLeaksMap.map { (_, pair) ->
      val (matcher, leakTraces) = pair
      LibraryLeak(leakTraces, matcher.pattern, matcher.description)
    }
    return applicationLeaks to libraryLeaks
  }

  private fun FindLeakInput.inspectObjects(shortestPaths: List<ShortestPath>): List<List<InspectedObject>> {
    listener.onAnalysisProgress(INSPECTING_OBJECTS)

    val leakReportersByPath = shortestPaths.map { path ->
      val pathList = path.asList()
      pathList
        .mapIndexed { index, node ->
          val reporter = ObjectReporter(heapObject = graph.findObjectById(node.objectId))
          val nextNode = if (index + 1 < pathList.size) pathList[index + 1] else null
          if (nextNode is LibraryLeakNode) {
            reporter.labels += "Library leak match: ${nextNode.matcher.pattern}"
          }
          reporter
        }
    }

    objectInspectors.forEach { inspector ->
      leakReportersByPath.forEach { leakReporters ->
        leakReporters.forEach { reporter ->
          inspector.inspect(reporter)
        }
      }
    }

    return leakReportersByPath.map { leakReporters ->
      computeLeakStatuses(leakReporters)
    }
  }

  private fun FindLeakInput.computeRetainedSizes(
    inspectedObjectsByPath: List<List<InspectedObject>>,
    dominatorTree: DominatorTree
  ): Map<Long, Pair<Int, Int>>? {
    val nodeObjectIds = inspectedObjectsByPath.flatMap { inspectedObjects ->
      inspectedObjects.filter { it.leakingStatus == UNKNOWN || it.leakingStatus == LEAKING }
        .map { it.heapObject.objectId }
    }.toSet()
    listener.onAnalysisProgress(COMPUTING_NATIVE_RETAINED_SIZE)
    val nativeSizeMapper = AndroidNativeSizeMapper(graph)
    val nativeSizes = nativeSizeMapper.mapNativeSizes()
    listener.onAnalysisProgress(COMPUTING_RETAINED_SIZE)
    val shallowSizeCalculator = ShallowSizeCalculator(graph)
    return dominatorTree.computeRetainedSizes(nodeObjectIds) { objectId ->
      val nativeSize = nativeSizes[objectId] ?: 0
      val shallowSize = shallowSizeCalculator.computeShallowSize(objectId)
      nativeSize + shallowSize
    }
  }

  private fun buildLeakTraceObjects(
    inspectedObjects: List<InspectedObject>,
    retainedSizes: Map<Long, Pair<Int, Int>>?
  ): List<LeakTraceObject> {
    return inspectedObjects.map { inspectedObject ->
      val heapObject = inspectedObject.heapObject
      val className = recordClassName(heapObject)

      val objectType = if (heapObject is HeapClass) {
        CLASS
      } else if (heapObject is HeapObjectArray || heapObject is HeapPrimitiveArray) {
        ARRAY
      } else {
        INSTANCE
      }

      val retainedSizeAndObjectCount = retainedSizes?.get(inspectedObject.heapObject.objectId)

      LeakTraceObject(
        type = objectType,
        className = className,
        labels = inspectedObject.labels,
        leakingStatus = inspectedObject.leakingStatus,
        leakingStatusReason = inspectedObject.leakingStatusReason,
        retainedHeapByteSize = retainedSizeAndObjectCount?.first,
        retainedObjectCount = retainedSizeAndObjectCount?.second
      )
    }
  }

  private fun FindLeakInput.buildReferencePath(
    shortestChildPath: List<ChildNode>,
    leakTraceObjects: List<LeakTraceObject>
  ): List<LeakTraceReference> {
    return shortestChildPath.mapIndexed { index, childNode ->
      LeakTraceReference(
        originObject = leakTraceObjects[index],
        referenceType = childNode.refFromParentType,
        owningClassName = if (childNode.owningClassId != 0L) {
          graph.findObjectById(childNode.owningClassId).asClass!!.name
        } else {
          leakTraceObjects[index].className
        },
        referenceName = childNode.refFromParentName
      )
    }
  }

  internal class InspectedObject(
    val heapObject: HeapObject,
    val leakingStatus: LeakingStatus,
    val leakingStatusReason: String,
    val labels: MutableSet<String>
  )

  private fun computeLeakStatuses(leakReporters: List<ObjectReporter>): List<InspectedObject> {
    val lastElementIndex = leakReporters.size - 1

    var lastNotLeakingElementIndex = -1
    var firstLeakingElementIndex = lastElementIndex

    val leakStatuses = ArrayList<Pair<LeakingStatus, String>>()

    for ((index, reporter) in leakReporters.withIndex()) {
      val resolvedStatusPair =
        resolveStatus(reporter, leakingWins = index == lastElementIndex).let { statusPair ->
          if (index == lastElementIndex) {
            // The last element should always be leaking.
            when (statusPair.first) {
              LEAKING -> statusPair
              UNKNOWN -> LEAKING to "This is the leaking object"
              NOT_LEAKING -> LEAKING to "This is the leaking object. Conflicts with ${statusPair.second}"
            }
          } else statusPair
        }

      leakStatuses.add(resolvedStatusPair)
      val (leakStatus, _) = resolvedStatusPair
      if (leakStatus == NOT_LEAKING) {
        lastNotLeakingElementIndex = index
        // Reset firstLeakingElementIndex so that we never have
        // firstLeakingElementIndex < lastNotLeakingElementIndex
        firstLeakingElementIndex = lastElementIndex
      } else if (leakStatus == LEAKING && firstLeakingElementIndex == lastElementIndex) {
        firstLeakingElementIndex = index
      }
    }

    val simpleClassNames = leakReporters.map { reporter ->
      recordClassName(reporter.heapObject).lastSegment('.')
    }

    for (i in 0 until lastNotLeakingElementIndex) {
      val (leakStatus, leakStatusReason) = leakStatuses[i]
      val nextNotLeakingIndex = generateSequence(i + 1) { index ->
        if (index < lastNotLeakingElementIndex) index + 1 else null
      }.first { index ->
        leakStatuses[index].first == NOT_LEAKING
      }

      // Element is forced to NOT_LEAKING
      val nextNotLeakingName = simpleClassNames[nextNotLeakingIndex]
      leakStatuses[i] = when (leakStatus) {
        UNKNOWN -> NOT_LEAKING to "$nextNotLeakingName↓ is not leaking"
        NOT_LEAKING -> NOT_LEAKING to "$nextNotLeakingName↓ is not leaking and $leakStatusReason"
        LEAKING -> NOT_LEAKING to "$nextNotLeakingName↓ is not leaking. Conflicts with $leakStatusReason"
      }
    }

    if (firstLeakingElementIndex < lastElementIndex - 1) {
      // We already know the status of firstLeakingElementIndex and lastElementIndex
      for (i in lastElementIndex - 1 downTo firstLeakingElementIndex + 1) {
        val (leakStatus, leakStatusReason) = leakStatuses[i]
        val previousLeakingIndex = generateSequence(i - 1) { index ->
          if (index > firstLeakingElementIndex) index - 1 else null
        }.first { index ->
          leakStatuses[index].first == LEAKING
        }

        // Element is forced to LEAKING
        val previousLeakingName = simpleClassNames[previousLeakingIndex]
        leakStatuses[i] = when (leakStatus) {
          UNKNOWN -> LEAKING to "$previousLeakingName↑ is leaking"
          LEAKING -> LEAKING to "$previousLeakingName↑ is leaking and $leakStatusReason"
          NOT_LEAKING -> throw IllegalStateException("Should never happen")
        }
      }
    }

    return leakReporters.mapIndexed { index, objectReporter ->
      val (leakingStatus, leakingStatusReason) = leakStatuses[index]
      InspectedObject(
        objectReporter.heapObject, leakingStatus, leakingStatusReason, objectReporter.labels
      )
    }
  }

  private fun resolveStatus(
    reporter: ObjectReporter,
    leakingWins: Boolean
  ): Pair<LeakingStatus, String> {
    var status = UNKNOWN
    var reason = ""
    if (reporter.notLeakingReasons.isNotEmpty()) {
      status = NOT_LEAKING
      reason = reporter.notLeakingReasons.joinToString(" and ")
    }
    val leakingReasons = reporter.leakingReasons
    if (leakingReasons.isNotEmpty()) {
      val winReasons = leakingReasons.joinToString(" and ")
      // Conflict
      if (status == NOT_LEAKING) {
        if (leakingWins) {
          status = LEAKING
          reason = "$winReasons. Conflicts with $reason"
        } else {
          reason += ". Conflicts with $winReasons"
        }
      } else {
        status = LEAKING
        reason = winReasons
      }
    }
    return status to reason
  }

  private fun recordClassName(
    heap: HeapObject
  ): String {
    return when (heap) {
      is HeapClass -> heap.name
      is HeapInstance -> heap.instanceClassName
      is HeapObjectArray -> heap.arrayClassName
      is HeapPrimitiveArray -> heap.arrayClassName
    }
  }

  private fun since(analysisStartNanoTime: Long): Long {
    return NANOSECONDS.toMillis(System.nanoTime() - analysisStartNanoTime)
  }
}

package shark

import shark.ObjectInspectors.KEYED_WEAK_REFERENCE
import shark.internal.KeyedWeakReferenceMirror

/**
 * Finds all objects tracked by a KeyedWeakReference, ie all objects that were passed to
 * ObjectWatcher.watch.
 */
object KeyedWeakReferenceFinder : LeakingObjectFinder {

  override fun findLeakingObjectIds(graph: HeapGraph): Set<Long> =
    findKeyedWeakReferences(graph)
      .filter { it.hasReferent && it.isRetained }
      .map { it.referent.value }
      .toSet()

  fun heapDumpUptimeMillis(graph: HeapGraph): Long? {
    return graph.context.getOrPut("heapDumpUptimeMillis") {
      val keyedWeakReferenceClass = graph.findClassByName("leakcanary.KeyedWeakReference")
      val heapDumpUptimeMillis = if (keyedWeakReferenceClass == null) {
        null
      } else {
        keyedWeakReferenceClass["heapDumpUptimeMillis"]?.value?.asLong
      }
      if (heapDumpUptimeMillis == null) {
        SharkLog.d {
          "leakcanary.KeyedWeakReference.heapDumpUptimeMillis field not found"
        }
      }
      heapDumpUptimeMillis
    }
  }

  internal fun findKeyedWeakReferences(graph: HeapGraph): List<KeyedWeakReferenceMirror> {
    return graph.context.getOrPut(KEYED_WEAK_REFERENCE.name) {
      val keyedWeakReferenceClass = graph.findClassByName("leakcanary.KeyedWeakReference")

      val keyedWeakReferenceClassId = keyedWeakReferenceClass?.objectId ?: 0
      val legacyKeyedWeakReferenceClassId =
        graph.findClassByName("com.squareup.leakcanary.KeyedWeakReference")?.objectId ?: 0

      val heapDumpUptimeMillis = heapDumpUptimeMillis(graph)

      val addedToContext: List<KeyedWeakReferenceMirror> = graph.instances
        .filter { instance ->
          instance.instanceClassId == keyedWeakReferenceClassId || instance.instanceClassId == legacyKeyedWeakReferenceClassId
        }
        .map {
          KeyedWeakReferenceMirror.fromInstance(
            it, heapDumpUptimeMillis
          )
        }
        .toList()
      graph.context[KEYED_WEAK_REFERENCE.name] = addedToContext
      addedToContext
    }
  }
}

package shark

/**
 * This class is kept to support backward compatible deserialization.
 */
internal enum class LeakNodeStatus {
  NOT_LEAKING,
  LEAKING,
  UNKNOWN;
}

package shark

import shark.LeakTrace.GcRootType.JAVA_FRAME
import shark.LeakTraceObject.LeakingStatus.LEAKING
import shark.LeakTraceObject.LeakingStatus.NOT_LEAKING
import shark.LeakTraceObject.LeakingStatus.UNKNOWN
import shark.LeakTraceReference.ReferenceType.STATIC_FIELD
import shark.internal.createSHA1Hash
import java.io.Serializable

/**
 * The best strong reference path from a GC root to the leaking object. "Best" here means the
 * shortest prioritized path. A large number of distinct paths can generally be found leading
 * to a leaking object. Shark prioritizes paths that don't go through known
 * [LibraryLeakReferenceMatcher] (because those are known to create leaks so it's more interesting
 * to find other paths causing leaks), then it prioritize paths that don't go through java local
 * gc roots (because those are harder to reason about). Taking those priorities into account,
 * finding the shortest path means there are less [LeakTraceReference] that can be suspected to
 * cause the leak.
 */
data class LeakTrace(
  /**
   * The Garbage Collection root that references the [LeakTraceReference.originObject] in
   * the first [LeakTraceReference] of [referencePath].
   */
  val gcRootType: GcRootType,
  val referencePath: List<LeakTraceReference>,
  val leakingObject: LeakTraceObject
) : Serializable {

  /**
   * The minimum number of bytes which would be freed if the leak was fixed.
   * Null if the retained heap size was not computed.
   */
  val retainedHeapByteSize: Int?
    get() {
      val allObjects = listOf(leakingObject) + referencePath.map { it.originObject }
      return allObjects.filter { it.leakingStatus == LEAKING }
        .mapNotNull { it.retainedHeapByteSize }
        // The minimum released is the max held by a leaking object.
        .max()
    }

  /**
   * The minimum number of objects which would be unreachable if the leak was fixed. Null if the
   * retained heap size was not computed.
   */
  val retainedObjectCount: Int?
    get() {
      val allObjects = listOf(leakingObject) + referencePath.map { it.originObject }
      return allObjects.filter { it.leakingStatus == LEAKING }
        .mapNotNull { it.retainedObjectCount }
        // The minimum released is the max held by a leaking object.
        .max()
    }

  /**
   * A part of [referencePath] that contains the references suspected to cause the leak.
   * Starts at the last non leaking object and ends before the first leaking object.
   */
  val suspectReferenceSubpath
    get() = referencePath.asSequence()
      .filterIndexed { index, _ ->
        referencePathElementIsSuspect(index)
      }

  /**
   * A SHA1 hash that represents this leak trace. This can be useful to group together similar
   * leak traces.
   *
   * The signature is a hash of [suspectReferenceSubpath].
   */
  val signature: String
    get() = suspectReferenceSubpath
      .joinToString(separator = "") { element ->
        element.originObject.className + element.referenceGenericName
      }
      .createSHA1Hash()

  /**
   * Returns true if the [referencePath] element at the provided [index] contains a reference
   * that is suspected to cause the leak, ie if [index] is greater than or equal to the index
   * of the [LeakTraceReference] of the last non leaking object and strictly lower than the index
   * of the [LeakTraceReference] of the first leaking object.
   */
  fun referencePathElementIsSuspect(index: Int): Boolean {
    return when (referencePath[index].originObject.leakingStatus) {
      UNKNOWN -> true
      NOT_LEAKING -> index == referencePath.lastIndex ||
        referencePath[index + 1].originObject.leakingStatus != NOT_LEAKING
      else -> false
    }
  }

  override fun toString(): String = leakTraceAsString(showLeakingStatus = true)

  fun toSimplePathString(): String = leakTraceAsString(showLeakingStatus = false)

  private fun leakTraceAsString(showLeakingStatus: Boolean): String {
    var result = """
        ┬───
        │ GC Root: ${gcRootType.description}
        │
      """.trimIndent()

    referencePath.forEachIndexed { index, element ->
      val originObject = element.originObject
      result += "\n"
      result += originObject.toString(
        firstLinePrefix = "├─ ",
        additionalLinesPrefix = "│    ",
        showLeakingStatus = showLeakingStatus,
        /**
         * When the GC Root is a Java Frame, Shark inserts the corresponding thread as an extra
         * element in the leaktrace.
         */
        typeName = if (index == 0 && gcRootType == JAVA_FRAME) {
          "thread"
        } else {
          originObject.typeName
        }
      )
      result += getNextElementString(this, element, index, showLeakingStatus)
    }

    result += "\n"
    result += leakingObject.toString(
      firstLinePrefix = "╰→ ",
      additionalLinesPrefix = "$ZERO_WIDTH_SPACE     ",
      showLeakingStatus = showLeakingStatus
    )
    return result
  }

  enum class GcRootType(val description: String) {
    JNI_GLOBAL("Global variable in native code"),
    JNI_LOCAL("Local variable in native code"),
    JAVA_FRAME("Java local variable"),
    NATIVE_STACK("Input or output parameters in native code"),
    STICKY_CLASS("System class"),
    THREAD_BLOCK("Thread block"),
    MONITOR_USED(
      "Monitor (anything that called the wait() or notify() methods, or that is synchronized.)"
    ),
    THREAD_OBJECT("Thread object"),
    JNI_MONITOR("Root JNI monitor"),
    ;

    companion object {
      fun fromGcRoot(gcRoot: GcRoot): GcRootType = when (gcRoot) {
        is GcRoot.JniGlobal -> JNI_GLOBAL
        is GcRoot.JniLocal -> JNI_LOCAL
        is GcRoot.JavaFrame -> JAVA_FRAME
        is GcRoot.NativeStack -> NATIVE_STACK
        is GcRoot.StickyClass -> STICKY_CLASS
        is GcRoot.ThreadBlock -> THREAD_BLOCK
        is GcRoot.MonitorUsed -> MONITOR_USED
        is GcRoot.ThreadObject -> THREAD_OBJECT
        is GcRoot.JniMonitor -> JNI_MONITOR
        else -> throw IllegalStateException("Unexpected gc root $gcRoot")
      }
    }
  }

  companion object {
    private fun getNextElementString(
      leakTrace: LeakTrace,
      reference: LeakTraceReference,
      index: Int,
      showLeakingStatus: Boolean
    ): String {
      val static = if (reference.referenceType == STATIC_FIELD) " static" else ""
      val referenceLine =
        "    ↓$static ${reference.owningClassSimpleName}.${reference.referenceDisplayName}"

      return if (showLeakingStatus && leakTrace.referencePathElementIsSuspect(index)) {
        val lengthBeforeReferenceName = referenceLine.lastIndexOf('.') + 1
        val referenceLength = referenceLine.length - lengthBeforeReferenceName

        val spaces = " ".repeat(lengthBeforeReferenceName)
        val underline = "~".repeat(referenceLength)
        "\n│$referenceLine\n│$spaces$underline"
      } else {
        "\n│$referenceLine"
      }
    }

    internal const val ZERO_WIDTH_SPACE = '\u200b'
    private const val serialVersionUID = -6315725584154386429
  }
}

package shark

import shark.LeakTrace.Companion.ZERO_WIDTH_SPACE
import shark.LeakTraceObject.LeakingStatus
import shark.LeakTraceObject.LeakingStatus.LEAKING
import shark.LeakTraceObject.LeakingStatus.NOT_LEAKING
import shark.LeakTraceObject.LeakingStatus.UNKNOWN
import shark.internal.lastSegment
import java.io.Serializable
import java.util.Locale
import kotlin.math.ln
import kotlin.math.pow

data class LeakTraceObject(
  val type: ObjectType,
  /**
   * Class name of the object.
   * The class name format is the same as what would be returned by [Class.getName].
   */
  val className: String,

  /**
   * Labels that were computed during analysis. A label provides extra information that helps
   * understand the state of the leak trace object.
   */
  val labels: Set<String>,
  val leakingStatus: LeakingStatus,
  val leakingStatusReason: String,
  /**
   * The minimum number of bytes which would be freed if all references to this object were
   * released. Not null only if the retained heap size was computed AND [leakingStatus] is
   * equal to [LeakingStatus.UNKNOWN] or [LeakingStatus.LEAKING].
   */
  val retainedHeapByteSize: Int?,
  /**
   * The minimum number of objects which would be unreachable if all references to this object were
   * released. Not null only if the retained heap size was computed AND [leakingStatus] is
   * equal to [LeakingStatus.UNKNOWN] or [LeakingStatus.LEAKING].
   */
  val retainedObjectCount: Int?
) : Serializable {

  /**
   * Returns {@link #className} without the package, ie stripped of any string content before the
   * last period (included).
   */
  val classSimpleName: String get() = className.lastSegment('.')

  val typeName
    get() = type.name.toLowerCase(Locale.US)

  override fun toString(): String {
    val firstLinePrefix = ""
    val additionalLinesPrefix = "$ZERO_WIDTH_SPACE  "
    return toString(firstLinePrefix, additionalLinesPrefix, true)
  }

  internal fun toString(
    firstLinePrefix: String,
    additionalLinesPrefix: String,
    showLeakingStatus: Boolean,
    typeName: String = this.typeName
  ): String {
    val leakStatus = when (leakingStatus) {
      UNKNOWN -> "UNKNOWN"
      NOT_LEAKING -> "NO ($leakingStatusReason)"
      LEAKING -> "YES ($leakingStatusReason)"
    }

    var result = ""
    result += "$firstLinePrefix$className $typeName"
    if (showLeakingStatus) {
      result += "\n${additionalLinesPrefix}Leaking: $leakStatus"
    }

    if (retainedHeapByteSize != null) {
      val humanReadableRetainedHeapSize =
        humanReadableByteCount(retainedHeapByteSize.toLong())
      result += "\n${additionalLinesPrefix}Retaining $humanReadableRetainedHeapSize in $retainedObjectCount objects"
    }
    for (label in labels) {
      result += "\n${additionalLinesPrefix}$label"
    }
    return result
  }

  enum class ObjectType {
    CLASS,
    ARRAY,
    INSTANCE
  }

  enum class LeakingStatus {
    /** The object was needed and therefore expected to be reachable. */
    NOT_LEAKING,

    /** The object was no longer needed and therefore expected to be unreachable. */
    LEAKING,

    /** No decision can be made about the provided object. */
    UNKNOWN;
  }

  companion object {
    private const val serialVersionUID = -3616216391305196341L

    // https://stackoverflow.com/a/3758880
    private fun humanReadableByteCount(bytes: Long): String {
      val unit = 1000
      if (bytes < unit) return "$bytes B"
      val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
      val pre = "kMGTPE"[exp - 1]
      return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
    }
  }
}

package shark

import shark.LeakTraceReference.ReferenceType.ARRAY_ENTRY
import shark.LeakTraceReference.ReferenceType.INSTANCE_FIELD
import shark.LeakTraceReference.ReferenceType.LOCAL
import shark.LeakTraceReference.ReferenceType.STATIC_FIELD
import shark.internal.lastSegment
import java.io.Serializable

/**
 * A [LeakTraceReference] represents and origin [LeakTraceObject] and either a reference from that
 * object to the [LeakTraceObject] in the next [LeakTraceReference] in [LeakTrace.referencePath],
 * or to [LeakTrace.leakingObject] if this is the last [LeakTraceReference] in
 * [LeakTrace.referencePath].
 */
data class LeakTraceReference(
  val originObject: LeakTraceObject,

  val referenceType: ReferenceType,

  val owningClassName: String,

  val referenceName: String

) : Serializable {

  enum class ReferenceType {
    INSTANCE_FIELD,
    STATIC_FIELD,
    LOCAL,
    ARRAY_ENTRY
  }

  /**
   * Returns {@link #className} without the package, ie stripped of any string content before the
   * last period (included).
   */
  val owningClassSimpleName: String get() = owningClassName.lastSegment('.')

  val referenceDisplayName: String
    get() {
      return when (referenceType) {
        ARRAY_ENTRY -> "[$referenceName]"
        STATIC_FIELD, INSTANCE_FIELD -> referenceName
        LOCAL -> "<Java Local>"
      }
    }

  val referenceGenericName: String
    get() {
      return when (referenceType) {
        // The specific array index in a leak rarely matters, this improves grouping.
        ARRAY_ENTRY -> "[x]"
        STATIC_FIELD, INSTANCE_FIELD -> referenceName
        LOCAL -> "<Java Local>"
      }
    }

  companion object {
    private const val serialVersionUID = 1L
  }
}

package shark

/**
 * Finds the objects that are leaking, for which Shark will compute
 * leak traces.
 *
 * This is a functional interface with which you can create a [LeakingObjectFinder] from a lambda.
 */
fun interface LeakingObjectFinder {

  /**
   * For a given heap graph, returns a set of object ids for the objects that are leaking.
   */
  fun findLeakingObjectIds(graph: HeapGraph): Set<Long>

  companion object {
    /**
     * Utility function to create a [LeakingObjectFinder] from the passed in [block] lambda
     * instead of using the anonymous `object : LeakingObjectFinder` syntax.
     *
     * Usage:
     *
     * ```kotlin
     * val listener = LeakingObjectFinder {
     *
     * }
     * ```
     */
    inline operator fun invoke(crossinline block: (HeapGraph) -> Set<Long>): LeakingObjectFinder =
      object : LeakingObjectFinder {
        override fun findLeakingObjectIds(graph: HeapGraph): Set<Long> = block(graph)
      }
  }
}

package shark

/**
 * Extracts metadata from a hprof to be reported in [HeapAnalysisSuccess.metadata].
 *
 * This is a functional interface with which you can create a [MetadataExtractor] from a lambda.
 */
fun interface MetadataExtractor {
  fun extractMetadata(graph: HeapGraph): Map<String, String>

  companion object {

    /**
     * A no-op [MetadataExtractor]
     */
    val NO_OP = MetadataExtractor { emptyMap() }

    /**
     * Utility function to create a [MetadataExtractor] from the passed in [block] lambda instead of
     * using the anonymous `object : MetadataExtractor` syntax.
     *
     * Usage:
     *
     * ```kotlin
     * val inspector = MetadataExtractor { graph ->
     *
     * }
     * ```
     */
    inline operator fun invoke(crossinline block: (HeapGraph) -> Map<String, String>): MetadataExtractor =
      object : MetadataExtractor {
        override fun extractMetadata(graph: HeapGraph): Map<String, String> = block(graph)
      }
  }
}

package shark

/**
 * Provides LeakCanary with insights about objects (classes, instances and arrays) found in the
 * heap. [inspect] will be called for each object that LeakCanary wants to know more about.
 * The implementation can then use the provided [ObjectReporter] to provide insights for that
 * object.
 *
 * This is a functional interface with which you can create a [ObjectInspector] from a lambda.
 */
fun interface ObjectInspector {

  /**
   * @see [ObjectInspector]
   */
  fun inspect(reporter: ObjectReporter)

  companion object {
    /**
     * Utility function to create a [ObjectInspector] from the passed in [block] lambda instead of
     * using the anonymous `object : OnHeapAnalyzedListener` syntax.
     *
     * Usage:
     *
     * ```kotlin
     * val inspector = ObjectInspector { reporter ->
     *
     * }
     * ```
     */
    inline operator fun invoke(crossinline block: (ObjectReporter) -> Unit): ObjectInspector =
      object : ObjectInspector {
        override fun inspect(
          reporter: ObjectReporter
        ) {
          block(reporter)
        }
      }
  }
}

/*
 * Copyright (C) 2018 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package shark

import shark.FilteringLeakingObjectFinder.LeakingObjectFilter
import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import java.util.EnumSet

/**
 * A set of default [ObjectInspector]s that knows about common JDK objects.
 */
enum class ObjectInspectors : ObjectInspector {

  KEYED_WEAK_REFERENCE {

    override val leakingObjectFilter = { heapObject: HeapObject ->
      KeyedWeakReferenceFinder.findKeyedWeakReferences(heapObject.graph)
        .filter { it.hasReferent && it.isRetained }
        .any { reference ->
          reference.referent.value == heapObject.objectId
        }
    }

    override fun inspect(
      reporter: ObjectReporter
    ) {
      val graph = reporter.heapObject.graph
      val references = KeyedWeakReferenceFinder.findKeyedWeakReferences(graph)

      val objectId = reporter.heapObject.objectId
      references.forEach { ref ->
        if (ref.referent.value == objectId) {
          reporter.leakingReasons += if (ref.description.isNotEmpty()) {
            "ObjectWatcher was watching this because ${ref.description}"
          } else {
            "ObjectWatcher was watching this"
          }
          reporter.labels += "key = ${ref.key}"
          if (ref.watchDurationMillis != null) {
            reporter.labels += "watchDurationMillis = ${ref.watchDurationMillis}"
          }
          if (ref.retainedDurationMillis != null) {
            reporter.labels += "retainedDurationMillis = ${ref.retainedDurationMillis}"
          }
        }
      }
    }
  },

  CLASSLOADER {
    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf(ClassLoader::class) {
        notLeakingReasons += "A ClassLoader is never leaking"
      }
    }
  },

  CLASS {
    override fun inspect(
      reporter: ObjectReporter
    ) {
      if (reporter.heapObject is HeapClass) {
        reporter.notLeakingReasons += "a class is never leaking"
      }
    }
  },

  ANONYMOUS_CLASS {
    override fun inspect(
      reporter: ObjectReporter
    ) {
      val heapObject = reporter.heapObject
      if (heapObject is HeapInstance) {
        val instanceClass = heapObject.instanceClass
        if (instanceClass.name.matches(ANONYMOUS_CLASS_NAME_PATTERN_REGEX)) {
          val parentClassRecord = instanceClass.superclass!!
          if (parentClassRecord.name == "java.lang.Object") {
            try {
              // This is an anonymous class implementing an interface. The API does not give access
              // to the interfaces implemented by the class. We check if it's in the class path and
              // use that instead.
              val actualClass = Class.forName(instanceClass.name)
              val interfaces = actualClass.interfaces
              reporter.labels += if (interfaces.isNotEmpty()) {
                val implementedInterface = interfaces[0]
                "Anonymous class implementing ${implementedInterface.name}"
              } else {
                "Anonymous subclass of java.lang.Object"
              }
            } catch (ignored: ClassNotFoundException) {
            }
          } else {
            // Makes it easier to figure out which anonymous class we're looking at.
            reporter.labels += "Anonymous subclass of ${parentClassRecord.name}"
          }
        }
      }
    }
  },

  THREAD {
    override fun inspect(
      reporter: ObjectReporter
    ) {
      reporter.whenInstanceOf(Thread::class) { instance ->
        val threadName = instance[Thread::class, "name"]!!.value.readAsJavaString()
        labels += "Thread name: '$threadName'"
      }
    }
  };

  internal open val leakingObjectFilter: ((heapObject: HeapObject) -> Boolean)? = null

  companion object {
    private const val ANONYMOUS_CLASS_NAME_PATTERN = "^.+\\$\\d+$"
    private val ANONYMOUS_CLASS_NAME_PATTERN_REGEX = ANONYMOUS_CLASS_NAME_PATTERN.toRegex()

    /** @see ObjectInspectors */
    val jdkDefaults: List<ObjectInspector>
      get() {
        return values().toList()
      }

    /**
     * Returns a list of [LeakingObjectFilter] suitable for common JDK projects.
     */
    val jdkLeakingObjectFilters: List<LeakingObjectFilter> =
      createLeakingObjectFilters(EnumSet.allOf(ObjectInspectors::class.java))

    /**
     * Creates a list of [LeakingObjectFilter] based on the passed in [ObjectInspectors].
     */
    fun createLeakingObjectFilters(inspectors: Set<ObjectInspectors>): List<LeakingObjectFilter> =
      inspectors.mapNotNull { it.leakingObjectFilter }
        .map { filter ->
          object : LeakingObjectFilter {
            override fun isLeakingObject(heapObject: HeapObject) = filter(heapObject)
          }
        }
  }
}

package shark

import shark.HeapObject.HeapInstance
import kotlin.reflect.KClass

/**
 * Enables [ObjectInspector] implementations to provide insights on [heapObject], which is
 * an object (class, instance or array) found in the heap.
 *
 * A given [ObjectReporter] only maps to one object in the heap, but is shared to many
 * [ObjectInspector] implementations and accumulates insights.
 */
class ObjectReporter constructor(val heapObject: HeapObject) {

  /**
   * Labels that will be visible on the corresponding [heapObject] in the leak trace.
   */
  val labels = linkedSetOf<String>()

  /**
   * Reasons for which this object is expected to be unreachable (ie it's leaking).
   */
  val leakingReasons = mutableSetOf<String>()

  /**
   * Deprecated, use leakingReasons instead.
   */
  @Deprecated(
    "Replace likelyLeakingReasons with leakingReasons",
    replaceWith = ReplaceWith(
      "leakingReasons"
    )
  )
  val likelyLeakingReasons
    get() = leakingReasons

  /**
   * Reasons for which this object is expected to be reachable (ie it's not leaking).
   */
  val notLeakingReasons = mutableSetOf<String>()

  /**
   * Runs [block] if [ObjectReporter.heapObject] is an instance of [expectedClass].
   */
  fun whenInstanceOf(
    expectedClass: KClass<out Any>,
    block: ObjectReporter.(HeapInstance) -> Unit
  ) {
    whenInstanceOf(expectedClass.java.name, block)
  }

  /**
   * Runs [block] if [ObjectReporter.heapObject] is an instance of [expectedClassName].
   */
  fun whenInstanceOf(
    expectedClassName: String,
    block: ObjectReporter.(HeapInstance) -> Unit
  ) {
    val heapObject = heapObject
    if (heapObject is HeapInstance && heapObject instanceOf expectedClassName) {
      block(heapObject)
    }
  }
}

package shark

/**
 * Reports progress from the [HeapAnalyzer] as they occur, as [Step] values.
 *
 * This is a functional interface with which you can create a [OnAnalysisProgressListener] from a lambda.
 */
fun interface OnAnalysisProgressListener {

  // These steps are defined in the order in which they occur.
  enum class Step {
    PARSING_HEAP_DUMP,
    EXTRACTING_METADATA,
    FINDING_RETAINED_OBJECTS,
    FINDING_PATHS_TO_RETAINED_OBJECTS,
    FINDING_DOMINATORS,
    INSPECTING_OBJECTS,
    COMPUTING_NATIVE_RETAINED_SIZE,
    COMPUTING_RETAINED_SIZE,
    BUILDING_LEAK_TRACES,
    REPORTING_HEAP_ANALYSIS
  }

  fun onAnalysisProgress(step: Step)

  companion object {

    /**
     * A no-op [OnAnalysisProgressListener]
     */
    val NO_OP = OnAnalysisProgressListener {}

    /**
     * Utility function to create a [OnAnalysisProgressListener] from the passed in [block] lambda
     * instead of using the anonymous `object : OnAnalysisProgressListener` syntax.
     *
     * Usage:
     *
     * ```kotlin
     * val listener = OnAnalysisProgressListener {
     *
     * }
     * ```
     */
    inline operator fun invoke(crossinline block: (Step) -> Unit): OnAnalysisProgressListener =
      object : OnAnalysisProgressListener {
        override fun onAnalysisProgress(step: Step) {
          block(step)
        }
      }
  }
}

package shark

/**
 * Used to pattern match known patterns of references in the heap, either to ignore them
 * ([IgnoredReferenceMatcher]) or to mark them as library leaks ([LibraryLeakReferenceMatcher]).
 */
sealed class ReferenceMatcher {

  /** The pattern that references will be matched against. */
  abstract val pattern: ReferencePattern
}

/**
 * [LibraryLeakReferenceMatcher] should be used to match references in library code that are
 * known to create leaks and are beyond your control. The shortest path finder will only go
 * through matching references after it has exhausted references that don't match, prioritizing
 * finding an application leak over a known library leak. Library leaks will be reported as
 * [LibraryLeak] instead of [ApplicationLeak].
 */
data class LibraryLeakReferenceMatcher(
  override val pattern: ReferencePattern,
  /**
   * A description that conveys what we know about this library leak.
   */
  val description: String = "",
  /**
   * Whether the identified leak may exist in the provided [HeapGraph]. Defaults to true. If
   * the heap dump comes from a VM that runs a different version of the library that doesn't
   * have the leak, then this should return false.
   */
  val patternApplies: (HeapGraph) -> Boolean = { true }
) : ReferenceMatcher() {
  override fun toString() = "library leak: $pattern"
}

/**
 * [IgnoredReferenceMatcher] should be used to match references that cannot ever create leaks. The
 * shortest path finder will never go through matching references.
 */
class IgnoredReferenceMatcher(override val pattern: ReferencePattern) : ReferenceMatcher() {
  override fun toString() = "ignored ref: $pattern"
}

package shark

import java.io.Serializable

/**
 * A pattern that will match references for a given [ReferenceMatcher].
 */
sealed class ReferencePattern : Serializable {

  /**
   * Matches local references held in the stack of frames of a given thread, identified by its name.
   */
  data class JavaLocalPattern(
    val threadName: String
  ) : ReferencePattern() {
    override fun toString() = "local variable on thread $threadName"

    companion object {
      private const val serialVersionUID: Long = -8985446122829543654
    }
  }

  /**
   * Matches static field references, identified by [className] and [fieldName].
   */
  data class StaticFieldPattern(
    val className: String,
    val fieldName: String
  ) : ReferencePattern() {
    override fun toString() = "static field $className#$fieldName"

    companion object {
      private const val serialVersionUID: Long = 7656908128775899611
    }
  }

  /**
   * Matches instances field references, identified by [className] and [fieldName].
   *
   * Note: If [fieldName] is declared in a superclass it will still match for subclasses.
   * This is to support overriding of rules for specific cases. If two [ReferenceMatcher] match for
   * the same [fieldName] but for different [className] in a class hierarchy, then the closest
   * class in the hierarchy wins.
   */
  data class InstanceFieldPattern(
    val className: String,
    val fieldName: String
  ) : ReferencePattern() {
    override fun toString() = "instance field $className#$fieldName"

    companion object {
      private const val serialVersionUID: Long = 6649791455204159802
    }
  }

  /**
   * Matches native global variables (also known as jni global gc roots) that reference
   * Java objects. The class name will match against classes, instances and object arrays with
   * a matching class name.
   */
  data class NativeGlobalVariablePattern(val className: String) : ReferencePattern() {
    override fun toString() = "native global variable referencing $className"

    companion object {
      private const val serialVersionUID: Long = -2651328076202244933
    }
  }

  companion object {
    private const val serialVersionUID: Long = -5113635523713591133
  }
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.Notification
import android.app.NotificationManager
import android.content.Context
import android.os.Debug
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import com.squareup.leakcanary.core.R
import leakcanary.internal.NotificationType.LEAKCANARY_LOW
import leakcanary.internal.friendly.mainHandler
import leakcanary.internal.friendly.measureDurationMillis
import shark.SharkLog
import java.util.concurrent.TimeUnit.SECONDS

internal class AndroidHeapDumper(
  context: Context,
  private val leakDirectoryProvider: LeakDirectoryProvider
) : HeapDumper {

  private val context: Context = context.applicationContext

  override fun dumpHeap(): DumpHeapResult {
    val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return NoHeapDump

    val waitingForToast = FutureResult<Toast?>()
    showToast(waitingForToast)

    if (!waitingForToast.wait(5, SECONDS)) {
      SharkLog.d { "Did not dump heap, too much time waiting for Toast." }
      return NoHeapDump
    }

    val notificationManager =
      context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    if (Notifications.canShowNotification) {
      val dumpingHeap = context.getString(R.string.leak_canary_notification_dumping)
      val builder = Notification.Builder(context)
        .setContentTitle(dumpingHeap)
      val notification = Notifications.buildNotification(context, builder, LEAKCANARY_LOW)
      notificationManager.notify(R.id.leak_canary_notification_dumping_heap, notification)
    }

    val toast = waitingForToast.get()

    return try {
      val durationMillis = measureDurationMillis {
        Debug.dumpHprofData(heapDumpFile.absolutePath)
      }
      if (heapDumpFile.length() == 0L) {
        SharkLog.d { "Dumped heap file is 0 byte length" }
        NoHeapDump
      } else {
        HeapDump(file = heapDumpFile, durationMillis = durationMillis)
      }
    } catch (e: Exception) {
      SharkLog.d(e) { "Could not dump heap" }
      // Abort heap dump
      NoHeapDump
    } finally {
      cancelToast(toast)
      notificationManager.cancel(R.id.leak_canary_notification_dumping_heap)
    }
  }

  private fun showToast(waitingForToast: FutureResult<Toast?>) {
    mainHandler.post(Runnable {
      val resumedActivity = InternalLeakCanary.resumedActivity
      if (resumedActivity == null) {
        waitingForToast.set(null)
        return@Runnable
      }
      val toast = Toast(resumedActivity)
      // Resources from application context: https://github.com/square/leakcanary/issues/2023
      val iconSize = context.resources.getDimensionPixelSize(
        R.dimen.leak_canary_toast_icon_size
      )
      toast.setGravity(Gravity.CENTER_VERTICAL, 0, -iconSize)
      toast.duration = Toast.LENGTH_LONG
      // Inflating with application context: https://github.com/square/leakcanary/issues/1385
      val inflater = LayoutInflater.from(context)
      toast.view = inflater.inflate(R.layout.leak_canary_heap_dump_toast, null)
      toast.show()

      val toastIcon = toast.view.findViewById<View>(R.id.leak_canary_toast_icon)
      toastIcon.translationY = -iconSize.toFloat()
      toastIcon
        .animate()
        .translationY(0f)
        .setListener(object : AnimatorListenerAdapter() {
          override fun onAnimationEnd(animation: Animator) {
            waitingForToast.set(toast)
          }
        })
    })
  }

  private fun cancelToast(toast: Toast?) {
    if (toast == null) {
      return
    }
    mainHandler.post { toast.cancel() }
  }
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.os.Debug

/**
 * Gives the opportunity to skip checking if a reference is gone when the debugger is connected.
 * An attached debugger might retain references and create false positives.
 */
internal object DebuggerControl {

  val isDebuggerAttached: Boolean
    get() = Debug.isDebuggerConnected()
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.content.Context
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
import com.squareup.leakcanary.core.R
import leakcanary.internal.DisplayLeakConnectorView.Type
import leakcanary.internal.DisplayLeakConnectorView.Type.END
import leakcanary.internal.DisplayLeakConnectorView.Type.END_FIRST_UNREACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.GC_ROOT
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_FIRST_UNREACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_LAST_REACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_REACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_UNKNOWN
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_UNREACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.START
import leakcanary.internal.DisplayLeakConnectorView.Type.START_LAST_REACHABLE
import leakcanary.internal.navigation.getColorCompat
import leakcanary.internal.navigation.inflate
import leakcanary.internal.utils.humanReadableByteCount
import shark.LeakTrace
import shark.LeakTrace.GcRootType.JAVA_FRAME
import shark.LeakTraceObject
import shark.LeakTraceObject.LeakingStatus.LEAKING
import shark.LeakTraceObject.LeakingStatus.NOT_LEAKING
import shark.LeakTraceObject.LeakingStatus.UNKNOWN
import shark.LeakTraceReference
import shark.LeakTraceReference.ReferenceType.STATIC_FIELD

@Suppress("DEPRECATION")
internal class DisplayLeakAdapter constructor(
  context: Context,
  private val leakTrace: LeakTrace,
  private val header: CharSequence
) : BaseAdapter() {

  private val highlightColorHexString: String
  private val leakColorHexString: String
  private val referenceColorHexString: String
  private val extraColorHexString: String
  private val helpColorHexString: String

  init {
    highlightColorHexString = hexStringColor(context, R.color.leak_canary_class_name)
    leakColorHexString = hexStringColor(context, R.color.leak_canary_leak)
    referenceColorHexString = hexStringColor(context, R.color.leak_canary_reference)
    extraColorHexString = hexStringColor(context, R.color.leak_canary_extra)
    helpColorHexString = hexStringColor(context, R.color.leak_canary_help)
  }

  override fun getView(
    position: Int,
    convertView: View?,
    parent: ViewGroup
  ): View {
    return when (getItemViewType(position)) {
      HEADER_ROW -> {
        val view = convertView ?: parent.inflate(R.layout.leak_canary_leak_header)
        bindHeaderRow(view)
        view
      }
      CONNECTOR_ROW -> {
        val view = convertView ?: parent.inflate(R.layout.leak_canary_ref_row)
        bindConnectorRow(view, position)
        view
      }
      else -> {
        throw IllegalStateException("Unexpected type ${getItemViewType(position)}")
      }
    }
  }

  private fun bindHeaderRow(
    view: View
  ) {
    view.findViewById<TextView>(R.id.leak_canary_header_text)
      .apply {
        movementMethod = LinkMovementMethod.getInstance()
        text = header
      }
  }

  private fun bindConnectorRow(
    view: View,
    position: Int
  ) {
    val titleView = view.findViewById<TextView>(R.id.leak_canary_row_title)
    val connector = view.findViewById<DisplayLeakConnectorView>(R.id.leak_canary_row_connector)

    connector.setType(getConnectorType(position))

    titleView.text = when {
      position == 1 -> {
        "GC Root: ${leakTrace.gcRootType.description}"
      }
      position < count - 1 -> {
        val referencePathIndex = elementIndex(position)
        val referencePath = leakTrace.referencePath[referencePathIndex]
        val isSuspect = leakTrace.referencePathElementIsSuspect(referencePathIndex)

        val leakTraceObject = referencePath.originObject

        val typeName =
          if (position == 2 && leakTrace.gcRootType == JAVA_FRAME) "thread" else leakTraceObject.typeName

        var referenceName = referencePath.referenceDisplayName

        referenceName = referenceName.replace("<".toRegex(), "<")
          .replace(">".toRegex(), ">")

        referenceName = if (isSuspect) {
          "$referenceName"
        } else {
          "$referenceName"
        }

        if (referencePath.referenceType == STATIC_FIELD) {
          referenceName = "$referenceName"
        }

        if (isSuspect) {
          referenceName = "$referenceName"
        }

        val staticPrefix = if (referencePath.referenceType == STATIC_FIELD) "static " else ""

        val htmlString = leakTraceObject.asHtmlString(typeName) +
          "$INDENTATION$staticPrefix${referencePath.styledOwningClassSimpleName()}.$referenceName"

        val builder = Html.fromHtml(htmlString) as SpannableStringBuilder
        if (isSuspect) {
          SquigglySpan.replaceUnderlineSpans(builder, view.context)
        }
        builder
      }
      else -> {
        Html.fromHtml(leakTrace.leakingObject.asHtmlString(leakTrace.leakingObject.typeName))
      }
    }
  }

  private fun LeakTraceObject.asHtmlString(typeName: String): String {
    val packageEnd = className.lastIndexOf('.')

    val extra: (String) -> String = { "$it" }

    val styledClassName = styledClassSimpleName()
    var htmlString =
      if (packageEnd != -1) "${
        extra(
          className.substring(
            0, packageEnd
          )
        )
      }.$styledClassName" else styledClassName
    htmlString += " ${extra(typeName)}
"

    val reachabilityString = when (leakingStatus) {
      UNKNOWN -> extra("UNKNOWN")
      NOT_LEAKING -> "NO" + extra(" (${leakingStatusReason})")
      LEAKING -> "YES" + extra(" (${leakingStatusReason})")
    }

    htmlString += "$INDENTATION${extra("Leaking: ")}$reachabilityString
"

    retainedHeapByteSize?.let {
      val humanReadableRetainedHeapSize = humanReadableByteCount(it.toLong(), si = true)
      htmlString += "${INDENTATION}${extra("Retaining ")}$humanReadableRetainedHeapSize${
        extra(
          " in "
        )
      }$retainedObjectCount${extra(" objects")}
"
    }

    labels.forEach { label ->
      htmlString += "$INDENTATION${extra(label)}
"
    }
    return htmlString
  }

  private fun LeakTraceObject.styledClassSimpleName(): String {
    val simpleName = classSimpleName.replace("[]", "[ ]")
    return "$simpleName"
  }

  private fun LeakTraceReference.styledOwningClassSimpleName(): String {
    val simpleName = owningClassSimpleName.replace("[]", "[ ]")
    return "$simpleName"
  }

  @Suppress("ReturnCount")
  private fun getConnectorType(position: Int): Type {
    if (position == 1) {
      return GC_ROOT
    } else if (position == 2) {
      return when (leakTrace.referencePath.size) {
        0 -> END_FIRST_UNREACHABLE
        1 -> START_LAST_REACHABLE
        else -> {
          val nextReachability = leakTrace.referencePath[1].originObject
          if (nextReachability.leakingStatus != NOT_LEAKING) {
            START_LAST_REACHABLE
          } else START
        }
      }
    } else {
      val isLeakingInstance = position == count - 1
      if (isLeakingInstance) {
        val previousReachability = leakTrace.referencePath.last()
          .originObject
        return if (previousReachability.leakingStatus != LEAKING) {
          END_FIRST_UNREACHABLE
        } else END
      } else {
        val reachability = leakTrace.referencePath[elementIndex(position)].originObject
        when (reachability.leakingStatus) {
          UNKNOWN -> return NODE_UNKNOWN
          NOT_LEAKING -> {
            val nextReachability =
              if (position + 1 == count - 1) leakTrace.leakingObject else leakTrace.referencePath[elementIndex(
                position + 1
              )].originObject
            return if (nextReachability.leakingStatus != NOT_LEAKING) {
              NODE_LAST_REACHABLE
            } else {
              NODE_REACHABLE
            }
          }
          LEAKING -> {
            val previousReachability =
              leakTrace.referencePath[elementIndex(position - 1)].originObject
            return if (previousReachability.leakingStatus != LEAKING) {
              NODE_FIRST_UNREACHABLE
            } else {
              NODE_UNREACHABLE
            }
          }
          else -> throw IllegalStateException(
            "Unknown value: " + reachability.leakingStatus
          )
        }
      }
    }
  }

  override fun isEnabled(position: Int) = false

  override fun getCount() = leakTrace.referencePath.size + 3

  override fun getItem(position: Int) = when {
    position == 0 || position == 1 -> null
    position == count - 1 -> leakTrace.leakingObject
    else -> leakTrace.referencePath[elementIndex(position)]
  }

  private fun elementIndex(position: Int) = position - 2

  override fun getViewTypeCount() = 2

  override fun getItemViewType(position: Int) = if (position == 0) HEADER_ROW else CONNECTOR_ROW

  override fun getItemId(position: Int) = position.toLong()

  companion object {

    const val HEADER_ROW = 0
    const val CONNECTOR_ROW = 1
    val INDENTATION = " ".repeat(4)

    // https://stackoverflow.com/a/6540378/703646
    private fun hexStringColor(
      context: Context,
      colorResId: Int
    ): String {
      return String.format("#%06X", 0xFFFFFF and context.getColorCompat(colorResId))
    }
  }
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Bitmap.Config.ARGB_8888
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.graphics.PorterDuff.Mode.CLEAR
import android.graphics.PorterDuffXfermode
import android.util.AttributeSet
import android.view.View
import com.squareup.leakcanary.core.R
import leakcanary.internal.DisplayLeakConnectorView.Type.END
import leakcanary.internal.DisplayLeakConnectorView.Type.END_FIRST_UNREACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.GC_ROOT
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_FIRST_UNREACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_LAST_REACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_REACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_UNKNOWN
import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_UNREACHABLE
import leakcanary.internal.DisplayLeakConnectorView.Type.START
import leakcanary.internal.DisplayLeakConnectorView.Type.START_LAST_REACHABLE
import leakcanary.internal.navigation.getColorCompat
import kotlin.math.sqrt

internal class DisplayLeakConnectorView(
  context: Context,
  attrs: AttributeSet
) : View(context, attrs) {

  private val classNamePaint: Paint
  private val leakPaint: Paint
  private val clearPaint: Paint
  private val referencePaint: Paint
  private val strokeSize: Float
  private val circleY: Float

  private var type: Type? = null
  private var cache: Bitmap? = null

  enum class Type {
    GC_ROOT,
    START,
    START_LAST_REACHABLE,
    NODE_UNKNOWN,
    NODE_FIRST_UNREACHABLE,
    NODE_UNREACHABLE,
    NODE_REACHABLE,
    NODE_LAST_REACHABLE,
    END,
    END_FIRST_UNREACHABLE
  }

  init {

    val resources = resources

    type = NODE_UNKNOWN
    circleY = resources.getDimensionPixelSize(R.dimen.leak_canary_connector_center_y)
      .toFloat()
    strokeSize = resources.getDimensionPixelSize(R.dimen.leak_canary_connector_stroke_size)
      .toFloat()

    classNamePaint = Paint(Paint.ANTI_ALIAS_FLAG)
    classNamePaint.color = context.getColorCompat(R.color.leak_canary_class_name)
    classNamePaint.strokeWidth = strokeSize

    leakPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    leakPaint.color = context.getColorCompat(R.color.leak_canary_leak)
    leakPaint.style = Paint.Style.STROKE
    leakPaint.strokeWidth = strokeSize

    val pathLines = resources.getDimensionPixelSize(R.dimen.leak_canary_connector_leak_dash_line)
      .toFloat()

    val pathGaps = resources.getDimensionPixelSize(R.dimen.leak_canary_connector_leak_dash_gap)
      .toFloat()
    leakPaint.pathEffect = DashPathEffect(floatArrayOf(pathLines, pathGaps), 0f)

    clearPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    clearPaint.color = Color.TRANSPARENT
    clearPaint.xfermode = CLEAR_XFER_MODE

    referencePaint = Paint(Paint.ANTI_ALIAS_FLAG)
    referencePaint.color = context.getColorCompat(R.color.leak_canary_reference)
    referencePaint.strokeWidth = strokeSize
  }

  override fun onDraw(canvas: Canvas) {
    val width = measuredWidth
    val height = measuredHeight

    if (cache != null && (cache!!.width != width || cache!!.height != height)) {
      cache!!.recycle()
      cache = null
    }

    if (cache == null) {
      cache = Bitmap.createBitmap(width, height, ARGB_8888)

      val cacheCanvas = Canvas(cache!!)

      when (type) {
        NODE_UNKNOWN -> drawItems(cacheCanvas, leakPaint, leakPaint)
        NODE_UNREACHABLE, NODE_REACHABLE -> drawItems(
          cacheCanvas, referencePaint, referencePaint
        )
        NODE_FIRST_UNREACHABLE -> drawItems(
          cacheCanvas, leakPaint, referencePaint
        )
        NODE_LAST_REACHABLE -> drawItems(
          cacheCanvas, referencePaint, leakPaint
        )
        START -> {
          drawStartLine(cacheCanvas)
          drawItems(cacheCanvas, null, referencePaint)
        }
        START_LAST_REACHABLE -> {
          drawStartLine(cacheCanvas)
          drawItems(cacheCanvas, null, leakPaint)
        }
        END -> drawItems(cacheCanvas, referencePaint, null)
        END_FIRST_UNREACHABLE -> drawItems(
          cacheCanvas, leakPaint, null
        )
        GC_ROOT -> drawGcRoot(cacheCanvas)
        else -> throw UnsupportedOperationException("Unknown type " + type!!)
      }
    }
    canvas.drawBitmap(cache!!, 0f, 0f, null)
  }

  private fun drawStartLine(cacheCanvas: Canvas) {
    val width = measuredWidth
    val halfWidth = width / 2f
    cacheCanvas.drawLine(halfWidth, 0f, halfWidth, circleY, classNamePaint)
  }

  private fun drawGcRoot(
    cacheCanvas: Canvas
  ) {
    val width = measuredWidth
    val height = measuredHeight
    val halfWidth = width / 2f
    cacheCanvas.drawLine(halfWidth, 0f, halfWidth, height.toFloat(), classNamePaint)
  }

  private fun drawItems(
    cacheCanvas: Canvas,
    arrowHeadPaint: Paint?,
    nextArrowPaint: Paint?
  ) {
    if (arrowHeadPaint != null) {
      drawArrowHead(cacheCanvas, arrowHeadPaint)
    }
    if (nextArrowPaint != null) {
      drawNextArrowLine(cacheCanvas, nextArrowPaint)
    }
    drawInstanceCircle(cacheCanvas)
  }

  private fun drawArrowHead(
    cacheCanvas: Canvas,
    paint: Paint
  ) {
    // Circle center is at half height
    val width = measuredWidth
    val halfWidth = width / 2f
    val circleRadius = width / 3f
// Splitting the arrow head in two makes an isosceles right triangle.
    // It's hypotenuse is side * sqrt(2)
    val arrowHeight = halfWidth / 2 * SQRT_TWO
    val halfStrokeSize = strokeSize / 2
    val translateY = circleY - arrowHeight - circleRadius * 2 - strokeSize

    val lineYEnd = circleY - circleRadius - strokeSize / 2
    cacheCanvas.drawLine(halfWidth, 0f, halfWidth, lineYEnd, paint)
    cacheCanvas.translate(halfWidth, translateY)
    cacheCanvas.rotate(45f)
    cacheCanvas.drawLine(
      0f, halfWidth, halfWidth + halfStrokeSize, halfWidth,
      paint
    )
    cacheCanvas.drawLine(halfWidth, 0f, halfWidth, halfWidth, paint)
    cacheCanvas.rotate(-45f)
    cacheCanvas.translate(-halfWidth, -translateY)
  }

  private fun drawNextArrowLine(
    cacheCanvas: Canvas,
    paint: Paint
  ) {
    val height = measuredHeight
    val width = measuredWidth
    val centerX = width / 2f
    cacheCanvas.drawLine(centerX, circleY, centerX, height.toFloat(), paint)
  }

  private fun drawInstanceCircle(cacheCanvas: Canvas) {
    val width = measuredWidth
    val circleX = width / 2f
    val circleRadius = width / 3f
    cacheCanvas.drawCircle(circleX, circleY, circleRadius, classNamePaint)
  }

  fun setType(type: Type) {
    if (type != this.type) {
      this.type = type
      if (cache != null) {
        cache!!.recycle()
        cache = null
      }
      invalidate()
    }
  }

  companion object {

    private val SQRT_TWO = sqrt(2.0)
      .toFloat()
    private val CLEAR_XFER_MODE = PorterDuffXfermode(CLEAR)
  }
}

/*
 * Copyright (C) 2018 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.app.IntentService
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import com.squareup.leakcanary.core.R
import leakcanary.internal.NotificationType.LEAKCANARY_LOW

internal abstract class ForegroundService(
  name: String,
  private val notificationContentTitleResId: Int,
  private val notificationId: Int
) : IntentService(name) {

  override fun onCreate() {
    super.onCreate()
    showForegroundNotification(
      max = 100, progress = 0, indeterminate = true,
      contentText = getString(R.string.leak_canary_notification_foreground_text)
    )
  }

  protected fun showForegroundNotification(
    max: Int,
    progress: Int,
    indeterminate: Boolean,
    contentText: String
  ) {
    val builder = Notification.Builder(this)
      .setContentTitle(getString(notificationContentTitleResId))
      .setContentText(contentText)
      .setProgress(max, progress, indeterminate)
    val notification =
      Notifications.buildNotification(this, builder, LEAKCANARY_LOW)
    startForeground(notificationId, notification)
  }

  override fun onHandleIntent(intent: Intent?) {
    onHandleIntentInForeground(intent)
  }

  protected abstract fun onHandleIntentInForeground(intent: Intent?)

  override fun onDestroy() {
    super.onDestroy()
    stopForeground(true)
  }

  override fun onBind(intent: Intent): IBinder? {
    return null
  }
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference

internal class FutureResult<T> {

  private val resultHolder: AtomicReference<T> = AtomicReference()
  private val latch: CountDownLatch = CountDownLatch(1)

  fun wait(
    timeout: Long,
    unit: TimeUnit
  ): Boolean {
    try {
      return latch.await(timeout, unit)
    } catch (e: InterruptedException) {
      throw RuntimeException("Did not expect thread to be interrupted", e)
    }
  }

  fun get(): T {
    if (latch.count > 0) {
      throw IllegalStateException("Call wait() and check its result")
    }
    return resultHolder.get()
  }

  fun set(result: T) {
    resultHolder.set(result)
    latch.countDown()
  }
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.content.Context
import android.content.Intent
import android.os.Build.VERSION.SDK_INT
import android.os.Process
import com.squareup.leakcanary.core.R
import leakcanary.LeakCanary
import leakcanary.LeakCanary.Config
import shark.HeapAnalysis
import shark.HeapAnalysisException
import shark.HeapAnalysisFailure
import shark.HeapAnalysisSuccess
import shark.HeapAnalyzer
import shark.OnAnalysisProgressListener
import shark.OnAnalysisProgressListener.Step.REPORTING_HEAP_ANALYSIS
import shark.ProguardMappingReader
import shark.SharkLog
import java.io.File
import java.io.IOException
import java.util.Locale

/**
 * This service runs in a main app process.
 */
internal class HeapAnalyzerService : ForegroundService(
  HeapAnalyzerService::class.java.simpleName,
  R.string.leak_canary_notification_analysing,
  R.id.leak_canary_notification_analyzing_heap
), OnAnalysisProgressListener {

  override fun onHandleIntentInForeground(intent: Intent?) {
    if (intent == null || !intent.hasExtra(HEAPDUMP_FILE_EXTRA)) {
      SharkLog.d { "HeapAnalyzerService received a null or empty intent, ignoring." }
      return
    }

    // Since we're running in the main process we should be careful not to impact it.
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
    val heapDumpFile = intent.getSerializableExtra(HEAPDUMP_FILE_EXTRA) as File
    val heapDumpReason = intent.getStringExtra(HEAPDUMP_REASON_EXTRA)
    val heapDumpDurationMillis = intent.getLongExtra(HEAPDUMP_DURATION_MILLIS_EXTRA, -1)

    val config = LeakCanary.config
    val heapAnalysis = if (heapDumpFile.exists()) {
      analyzeHeap(heapDumpFile, config)
    } else {
      missingFileFailure(heapDumpFile)
    }
    val fullHeapAnalysis = when (heapAnalysis) {
      is HeapAnalysisSuccess -> heapAnalysis.copy(
        dumpDurationMillis = heapDumpDurationMillis,
        metadata = heapAnalysis.metadata + ("Heap dump reason" to heapDumpReason)
      )
      is HeapAnalysisFailure -> heapAnalysis.copy(dumpDurationMillis = heapDumpDurationMillis)
    }
    onAnalysisProgress(REPORTING_HEAP_ANALYSIS)
    config.onHeapAnalyzedListener.onHeapAnalyzed(fullHeapAnalysis)
  }

  private fun analyzeHeap(
    heapDumpFile: File,
    config: Config
  ): HeapAnalysis {
    val heapAnalyzer = HeapAnalyzer(this)

    val proguardMappingReader = try {
      ProguardMappingReader(assets.open(PROGUARD_MAPPING_FILE_NAME))
    } catch (e: IOException) {
      null
    }
    return heapAnalyzer.analyze(
      heapDumpFile = heapDumpFile,
      leakingObjectFinder = config.leakingObjectFinder,
      referenceMatchers = config.referenceMatchers,
      computeRetainedHeapSize = config.computeRetainedHeapSize,
      objectInspectors = config.objectInspectors,
      metadataExtractor = config.metadataExtractor,
      proguardMapping = proguardMappingReader?.readProguardMapping()
    )
  }

  private fun missingFileFailure(
    heapDumpFile: File
  ): HeapAnalysisFailure {
    val deletedReason = LeakDirectoryProvider.hprofDeleteReason(heapDumpFile)
    val exception = IllegalStateException(
      "Hprof file $heapDumpFile missing, deleted because: $deletedReason"
    )
    return HeapAnalysisFailure(
      heapDumpFile = heapDumpFile,
      createdAtTimeMillis = System.currentTimeMillis(),
      analysisDurationMillis = 0,
      exception = HeapAnalysisException(exception)
    )
  }

  override fun onAnalysisProgress(step: OnAnalysisProgressListener.Step) {
    val percent =
      (100f * step.ordinal / OnAnalysisProgressListener.Step.values().size).toInt()
    SharkLog.d { "Analysis in progress, working on: ${step.name}" }
    val lowercase = step.name.replace("_", " ")
      .toLowerCase(Locale.US)
    val message = lowercase.substring(0, 1).toUpperCase(Locale.US) + lowercase.substring(1)
    showForegroundNotification(100, percent, false, message)
  }

  companion object {
    private const val HEAPDUMP_FILE_EXTRA = "HEAPDUMP_FILE_EXTRA"
    private const val HEAPDUMP_DURATION_MILLIS_EXTRA = "HEAPDUMP_DURATION_MILLIS_EXTRA"
    private const val HEAPDUMP_REASON_EXTRA = "HEAPDUMP_REASON_EXTRA"
    private const val PROGUARD_MAPPING_FILE_NAME = "leakCanaryObfuscationMapping.txt"

    fun runAnalysis(
      context: Context,
      heapDumpFile: File,
      heapDumpDurationMillis: Long? = null,
      heapDumpReason: String = "Unknown"
    ) {
      val intent = Intent(context, HeapAnalyzerService::class.java)
      intent.putExtra(HEAPDUMP_FILE_EXTRA, heapDumpFile)
      intent.putExtra(HEAPDUMP_REASON_EXTRA, heapDumpReason)
      heapDumpDurationMillis?.let {
        intent.putExtra(HEAPDUMP_DURATION_MILLIS_EXTRA, heapDumpDurationMillis)
      }
      startForegroundService(context, intent)
    }

    private fun startForegroundService(
      context: Context,
      intent: Intent
    ) {
      if (SDK_INT >= 26) {
        context.startForegroundService(intent)
      } else {
        // Pre-O behavior.
        context.startService(intent)
      }
    }
  }
}

package leakcanary.internal

import android.app.Application
import android.os.Handler
import android.os.Looper
import com.squareup.leakcanary.core.R
import leakcanary.AppWatcher
import leakcanary.LeakCanary
import leakcanary.internal.HeapDumpControl.ICanHazHeap.Nope
import leakcanary.internal.HeapDumpControl.ICanHazHeap.NotifyingNope
import leakcanary.internal.HeapDumpControl.ICanHazHeap.SilentNope
import leakcanary.internal.HeapDumpControl.ICanHazHeap.Yup
import leakcanary.internal.friendly.mainHandler

internal object HeapDumpControl {

  sealed class ICanHazHeap {
    object Yup : ICanHazHeap()
    abstract class Nope(val reason: () -> String) : ICanHazHeap()
    class SilentNope(reason: () -> String) : Nope(reason)

    /**
     * Allows manual dumping via a notification
     */
    class NotifyingNope(reason: () -> String) : Nope(reason)
  }

  @Volatile
  private lateinit var latest: ICanHazHeap

  private val app: Application
    get() = InternalLeakCanary.application

  private val testClassName by lazy {
    InternalLeakCanary.application.getString(R.string.leak_canary_test_class_name)
  }

  private val hasTestClass by lazy {
    try {
      Class.forName(testClassName)
      true
    } catch (e: Exception) {
      false
    }
  }

  fun updateICanHasHeap() {
    iCanHasHeap()
  }

  fun iCanHasHeap(): ICanHazHeap {
    val config = LeakCanary.config
    val dumpHeap = if (!AppWatcher.isInstalled) {
      // Can't use a resource, we don't have an Application instance when not installed
      SilentNope { "AppWatcher is not installed." }
    } else if (!InternalLeakCanary.dumpEnabledInAboutScreen) {
      NotifyingNope {
        app.getString(R.string.leak_canary_heap_dump_disabled_from_ui)
      }
    } else if (!config.dumpHeap) {
      SilentNope { app.getString(R.string.leak_canary_heap_dump_disabled_by_app) }
    } else if (hasTestClass) {
      SilentNope {
        app.getString(R.string.leak_canary_heap_dump_disabled_running_tests, testClassName)
      }
    } else if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
      mainHandler.postDelayed({
        updateICanHasHeap()
      }, 20_000L)
      NotifyingNope { app.getString(R.string.leak_canary_notification_retained_debugger_attached) }
    } else Yup

    synchronized(this) {
      if (::latest.isInitialized && dumpHeap is Yup && latest is Nope) {
        InternalLeakCanary.scheduleRetainedObjectCheck()
      }
      latest = dumpHeap
    }

    return dumpHeap
  }
}

package leakcanary.internal

import android.app.Application
import android.app.Notification
import android.app.NotificationManager
import android.content.Context
import android.content.res.Resources.NotFoundException
import android.os.Handler
import android.os.SystemClock
import com.squareup.leakcanary.core.R
import leakcanary.AppWatcher
import leakcanary.GcTrigger
import leakcanary.KeyedWeakReference
import leakcanary.LeakCanary.Config
import leakcanary.ObjectWatcher
import leakcanary.internal.HeapDumpControl.ICanHazHeap.Nope
import leakcanary.internal.HeapDumpControl.ICanHazHeap.NotifyingNope
import leakcanary.internal.InternalLeakCanary.onRetainInstanceListener
import leakcanary.internal.NotificationReceiver.Action.CANCEL_NOTIFICATION
import leakcanary.internal.NotificationReceiver.Action.DUMP_HEAP
import leakcanary.internal.NotificationType.LEAKCANARY_LOW
import leakcanary.internal.RetainInstanceEvent.CountChanged.BelowThreshold
import leakcanary.internal.RetainInstanceEvent.CountChanged.DumpHappenedRecently
import leakcanary.internal.RetainInstanceEvent.CountChanged.DumpingDisabled
import leakcanary.internal.RetainInstanceEvent.NoMoreObjects
import shark.AndroidResourceIdNames
import shark.SharkLog

internal class HeapDumpTrigger(
  private val application: Application,
  private val backgroundHandler: Handler,
  private val objectWatcher: ObjectWatcher,
  private val gcTrigger: GcTrigger,
  private val heapDumper: HeapDumper,
  private val configProvider: () -> Config
) {

  private val notificationManager
    get() =
      application.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

  private val applicationVisible
    get() = applicationInvisibleAt == -1L

  @Volatile
  private var checkScheduledAt: Long = 0L

  private var lastDisplayedRetainedObjectCount = 0

  private var lastHeapDumpUptimeMillis = 0L

  private val scheduleDismissRetainedCountNotification = {
    dismissRetainedCountNotification()
  }

  private val scheduleDismissNoRetainedOnTapNotification = {
    dismissNoRetainedOnTapNotification()
  }

  /**
   * When the app becomes invisible, we don't dump the heap immediately. Instead we wait in case
   * the app came back to the foreground, but also to wait for new leaks that typically occur on
   * back press (activity destroy).
   */
  private val applicationInvisibleLessThanWatchPeriod: Boolean
    get() {
      val applicationInvisibleAt = applicationInvisibleAt
      return applicationInvisibleAt != -1L && SystemClock.uptimeMillis() - applicationInvisibleAt < AppWatcher.config.watchDurationMillis
    }

  @Volatile
  private var applicationInvisibleAt = -1L

  fun onApplicationVisibilityChanged(applicationVisible: Boolean) {
    if (applicationVisible) {
      applicationInvisibleAt = -1L
    } else {
      applicationInvisibleAt = SystemClock.uptimeMillis()
      // Scheduling for after watchDuration so that any destroyed activity has time to become
      // watch and be part of this analysis.
      scheduleRetainedObjectCheck(
        delayMillis = AppWatcher.config.watchDurationMillis
      )
    }
  }

  private fun checkRetainedObjects() {
    val iCanHasHeap = HeapDumpControl.iCanHasHeap()

    val config = configProvider()

    if (iCanHasHeap is Nope) {
      if (iCanHasHeap is NotifyingNope) {
        // Before notifying that we can't dump heap, let's check if we still have retained object.
        var retainedReferenceCount = objectWatcher.retainedObjectCount

        if (retainedReferenceCount > 0) {
          gcTrigger.runGc()
          retainedReferenceCount = objectWatcher.retainedObjectCount
        }

        val nopeReason = iCanHasHeap.reason()
        val wouldDump = !checkRetainedCount(
          retainedReferenceCount, config.retainedVisibleThreshold, nopeReason
        )

        if (wouldDump) {
          val uppercaseReason = nopeReason[0].toUpperCase() + nopeReason.substring(1)
          onRetainInstanceListener.onEvent(DumpingDisabled(uppercaseReason))
          showRetainedCountNotification(
            objectCount = retainedReferenceCount,
            contentText = uppercaseReason
          )
        }
      } else {
        SharkLog.d {
          application.getString(
            R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
          )
        }
      }
      return
    }

    var retainedReferenceCount = objectWatcher.retainedObjectCount

    if (retainedReferenceCount > 0) {
      gcTrigger.runGc()
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }

    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

    val now = SystemClock.uptimeMillis()
    val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
    if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
      onRetainInstanceListener.onEvent(DumpHappenedRecently)
      showRetainedCountNotification(
        objectCount = retainedReferenceCount,
        contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)
      )
      scheduleRetainedObjectCheck(
        delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
      )
      return
    }

    dismissRetainedCountNotification()
    val visibility = if (applicationVisible) "visible" else "not visible"
    dumpHeap(
      retainedReferenceCount = retainedReferenceCount,
      retry = true,
      reason = "$retainedReferenceCount retained objects, app is $visibility"
    )
  }

  private fun dumpHeap(
    retainedReferenceCount: Int,
    retry: Boolean,
    reason: String
  ) {
    saveResourceIdNamesToMemory()
    val heapDumpUptimeMillis = SystemClock.uptimeMillis()
    KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
    when (val heapDumpResult = heapDumper.dumpHeap()) {
      is NoHeapDump -> {
        if (retry) {
          SharkLog.d { "Failed to dump heap, will retry in $WAIT_AFTER_DUMP_FAILED_MILLIS ms" }
          scheduleRetainedObjectCheck(
            delayMillis = WAIT_AFTER_DUMP_FAILED_MILLIS
          )
        } else {
          SharkLog.d { "Failed to dump heap, will not automatically retry" }
        }
        showRetainedCountNotification(
          objectCount = retainedReferenceCount,
          contentText = application.getString(
            R.string.leak_canary_notification_retained_dump_failed
          )
        )
      }
      is HeapDump -> {
        lastDisplayedRetainedObjectCount = 0
        lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
        objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
        HeapAnalyzerService.runAnalysis(
          context = application,
          heapDumpFile = heapDumpResult.file,
          heapDumpDurationMillis = heapDumpResult.durationMillis,
          heapDumpReason = reason
        )
      }
    }
  }

  private fun saveResourceIdNamesToMemory() {
    val resources = application.resources
    AndroidResourceIdNames.saveToMemory(
      getResourceTypeName = { id ->
        try {
          resources.getResourceTypeName(id)
        } catch (e: NotFoundException) {
          null
        }
      },
      getResourceEntryName = { id ->
        try {
          resources.getResourceEntryName(id)
        } catch (e: NotFoundException) {
          null
        }
      })
  }

  fun onDumpHeapReceived(forceDump: Boolean) {
    backgroundHandler.post {
      dismissNoRetainedOnTapNotification()
      gcTrigger.runGc()
      val retainedReferenceCount = objectWatcher.retainedObjectCount
      if (!forceDump && retainedReferenceCount == 0) {
        SharkLog.d { "Ignoring user request to dump heap: no retained objects remaining after GC" }
        @Suppress("DEPRECATION")
        val builder = Notification.Builder(application)
          .setContentTitle(
            application.getString(R.string.leak_canary_notification_no_retained_object_title)
          )
          .setContentText(
            application.getString(
              R.string.leak_canary_notification_no_retained_object_content
            )
          )
          .setAutoCancel(true)
          .setContentIntent(NotificationReceiver.pendingIntent(application, CANCEL_NOTIFICATION))
        val notification =
          Notifications.buildNotification(application, builder, LEAKCANARY_LOW)
        notificationManager.notify(
          R.id.leak_canary_notification_no_retained_object_on_tap, notification
        )
        backgroundHandler.postDelayed(
          scheduleDismissNoRetainedOnTapNotification,
          DISMISS_NO_RETAINED_OBJECT_NOTIFICATION_MILLIS
        )
        lastDisplayedRetainedObjectCount = 0
        return@post
      }

      SharkLog.d { "Dumping the heap because user requested it" }
      dumpHeap(retainedReferenceCount, retry = false, "user request")
    }
  }

  private fun checkRetainedCount(
    retainedKeysCount: Int,
    retainedVisibleThreshold: Int,
    nopeReason: String? = null
  ): Boolean {
    val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
    lastDisplayedRetainedObjectCount = retainedKeysCount
    if (retainedKeysCount == 0) {
      if (countChanged) {
        SharkLog.d { "All retained objects have been garbage collected" }
        onRetainInstanceListener.onEvent(NoMoreObjects)
        showNoMoreRetainedObjectNotification()
      }
      return true
    }

    val applicationVisible = applicationVisible
    val applicationInvisibleLessThanWatchPeriod = applicationInvisibleLessThanWatchPeriod

    if (countChanged) {
      val whatsNext = if (applicationVisible) {
        if (retainedKeysCount < retainedVisibleThreshold) {
          "not dumping heap yet (app is visible & < $retainedVisibleThreshold threshold)"
        } else {
          if (nopeReason != null) {
            "would dump heap now (app is visible & >=$retainedVisibleThreshold threshold) but $nopeReason"
          } else {
            "dumping heap now (app is visible & >=$retainedVisibleThreshold threshold)"
          }
        }
      } else if (applicationInvisibleLessThanWatchPeriod) {
        val wait =
          AppWatcher.config.watchDurationMillis - (SystemClock.uptimeMillis() - applicationInvisibleAt)
        if (nopeReason != null) {
          "would dump heap in $wait ms (app just became invisible) but $nopeReason"
        } else {
          "dumping heap in $wait ms (app just became invisible)"
        }
      } else {
        if (nopeReason != null) {
          "would dump heap now (app is invisible) but $nopeReason"
        } else {
          "dumping heap now (app is invisible)"
        }
      }

      SharkLog.d {
        val s = if (retainedKeysCount > 1) "s" else ""
        "Found $retainedKeysCount object$s retained, $whatsNext"
      }
    }

    if (retainedKeysCount < retainedVisibleThreshold) {
      if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
        if (countChanged) {
          onRetainInstanceListener.onEvent(BelowThreshold(retainedKeysCount))
        }
        showRetainedCountNotification(
          objectCount = retainedKeysCount,
          contentText = application.getString(
            R.string.leak_canary_notification_retained_visible, retainedVisibleThreshold
          )
        )
        scheduleRetainedObjectCheck(
          delayMillis = WAIT_FOR_OBJECT_THRESHOLD_MILLIS
        )
        return true
      }
    }
    return false
  }

  fun scheduleRetainedObjectCheck(
    delayMillis: Long = 0L
  ) {
    val checkCurrentlyScheduledAt = checkScheduledAt
    if (checkCurrentlyScheduledAt > 0) {
      return
    }
    checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
    backgroundHandler.postDelayed({
      checkScheduledAt = 0
      checkRetainedObjects()
    }, delayMillis)
  }

  private fun showNoMoreRetainedObjectNotification() {
    backgroundHandler.removeCallbacks(scheduleDismissRetainedCountNotification)
    if (!Notifications.canShowNotification) {
      return
    }
    val builder = Notification.Builder(application)
      .setContentTitle(
        application.getString(R.string.leak_canary_notification_no_retained_object_title)
      )
      .setContentText(
        application.getString(
          R.string.leak_canary_notification_no_retained_object_content
        )
      )
      .setAutoCancel(true)
      .setContentIntent(NotificationReceiver.pendingIntent(application, CANCEL_NOTIFICATION))
    val notification =
      Notifications.buildNotification(application, builder, LEAKCANARY_LOW)
    notificationManager.notify(R.id.leak_canary_notification_retained_objects, notification)
    backgroundHandler.postDelayed(
      scheduleDismissRetainedCountNotification, DISMISS_NO_RETAINED_OBJECT_NOTIFICATION_MILLIS
    )
  }

  private fun showRetainedCountNotification(
    objectCount: Int,
    contentText: String
  ) {
    backgroundHandler.removeCallbacks(scheduleDismissRetainedCountNotification)
    if (!Notifications.canShowNotification) {
      return
    }
    @Suppress("DEPRECATION")
    val builder = Notification.Builder(application)
      .setContentTitle(
        application.getString(R.string.leak_canary_notification_retained_title, objectCount)
      )
      .setContentText(contentText)
      .setAutoCancel(true)
      .setContentIntent(NotificationReceiver.pendingIntent(application, DUMP_HEAP))
    val notification =
      Notifications.buildNotification(application, builder, LEAKCANARY_LOW)
    notificationManager.notify(R.id.leak_canary_notification_retained_objects, notification)
  }

  private fun dismissRetainedCountNotification() {
    backgroundHandler.removeCallbacks(scheduleDismissRetainedCountNotification)
    notificationManager.cancel(R.id.leak_canary_notification_retained_objects)
  }

  private fun dismissNoRetainedOnTapNotification() {
    backgroundHandler.removeCallbacks(scheduleDismissNoRetainedOnTapNotification)
    notificationManager.cancel(R.id.leak_canary_notification_no_retained_object_on_tap)
  }

  companion object {
    private const val WAIT_AFTER_DUMP_FAILED_MILLIS = 5_000L
    private const val WAIT_FOR_OBJECT_THRESHOLD_MILLIS = 2_000L
    private const val DISMISS_NO_RETAINED_OBJECT_NOTIFICATION_MILLIS = 30_000L
    private const val WAIT_BETWEEN_HEAP_DUMPS_MILLIS = 60_000L
  }
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import java.io.File

/** Dumps the heap into a file.  */
internal interface HeapDumper {

  /**
   * @return a [File] referencing the dumped heap, or [.RETRY_LATER] if the heap could
   * not be dumped.
   */
  fun dumpHeap(): DumpHeapResult
}

/** Dump heap result holding the file and the dump heap duration */
internal sealed class DumpHeapResult

internal data class HeapDump(
  val file: File,
  val durationMillis: Long
) : DumpHeapResult()

internal object NoHeapDump : DumpHeapResult()

package leakcanary.internal

import android.app.Activity
import android.app.Application
import android.app.Application.ActivityLifecycleCallbacks
import android.app.UiModeManager
import android.content.ComponentName
import android.content.Context
import android.content.Context.UI_MODE_SERVICE
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
import android.content.pm.PackageManager.DONT_KILL_APP
import android.content.pm.ShortcutInfo.Builder
import android.content.pm.ShortcutManager
import android.content.res.Configuration
import android.graphics.drawable.Icon
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Handler
import android.os.HandlerThread
import com.squareup.leakcanary.core.BuildConfig
import com.squareup.leakcanary.core.R
import leakcanary.AppWatcher
import leakcanary.GcTrigger
import leakcanary.LeakCanary
import leakcanary.OnObjectRetainedListener
import leakcanary.internal.HeapDumpControl.ICanHazHeap.Nope
import leakcanary.internal.HeapDumpControl.ICanHazHeap.Yup
import leakcanary.internal.InternalLeakCanary.FormFactor.MOBILE
import leakcanary.internal.InternalLeakCanary.FormFactor.TV
import leakcanary.internal.InternalLeakCanary.FormFactor.WATCH
import leakcanary.internal.friendly.noOpDelegate
import leakcanary.internal.tv.TvOnRetainInstanceListener
import leakcanary.internal.friendly.mainHandler
import shark.SharkLog

internal object InternalLeakCanary : (Application) -> Unit, OnObjectRetainedListener {

  private const val DYNAMIC_SHORTCUT_ID = "com.squareup.leakcanary.dynamic_shortcut"

  private lateinit var heapDumpTrigger: HeapDumpTrigger

  // You're wrong https://discuss.kotlinlang.org/t/object-or-top-level-property-name-warning/6621/7
  @Suppress("ObjectPropertyName")
  private var _application: Application? = null

  val application: Application
    get() {
      check(_application != null) {
        "LeakCanary not installed, see AppWatcher.manualInstall()"
      }
      return _application!!
    }

  // BuildConfig.LIBRARY_VERSION is stripped so this static var is how we keep it around to find
  // it later when parsing the heap dump.
  @Suppress("unused")
  @JvmStatic
  private var version = BuildConfig.LIBRARY_VERSION

  @Volatile
  var applicationVisible = false
    private set

  private val isDebuggableBuild by lazy {
    (application.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
  }

  fun createLeakDirectoryProvider(context: Context): LeakDirectoryProvider {
    val appContext = context.applicationContext
    return LeakDirectoryProvider(appContext, {
      LeakCanary.config.maxStoredHeapDumps
    }, {
      LeakCanary.config.requestWriteExternalStoragePermission
    })
  }

  internal enum class FormFactor {
    MOBILE,
    TV,
    WATCH,
  }

  val formFactor by lazy {
    val currentModeType =
      (application.getSystemService(UI_MODE_SERVICE) as UiModeManager).currentModeType
    return@lazy when (currentModeType) {
      Configuration.UI_MODE_TYPE_TELEVISION -> TV
      Configuration.UI_MODE_TYPE_WATCH -> WATCH
      else -> MOBILE
    }
  }

  val isInstantApp by lazy {
    VERSION.SDK_INT >= VERSION_CODES.O && application.packageManager.isInstantApp
  }

  val onRetainInstanceListener by lazy {
    when (formFactor) {
      TV -> TvOnRetainInstanceListener(application)
      else -> DefaultOnRetainInstanceListener()
    }
  }

  var resumedActivity: Activity? = null

  private val heapDumpPrefs by lazy {
    application.getSharedPreferences("LeakCanaryHeapDumpPrefs", Context.MODE_PRIVATE)
  }

  internal var dumpEnabledInAboutScreen: Boolean
    get() {
      return heapDumpPrefs
        .getBoolean("AboutScreenDumpEnabled", true)
    }
    set(value) {
      heapDumpPrefs
        .edit()
        .putBoolean("AboutScreenDumpEnabled", value)
        .apply()
    }

  override fun invoke(application: Application) {
    _application = application

    checkRunningInDebuggableBuild()

    AppWatcher.objectWatcher.addOnObjectRetainedListener(this)

    val heapDumper = AndroidHeapDumper(application, createLeakDirectoryProvider(application))

    val gcTrigger = GcTrigger.Default

    val configProvider = { LeakCanary.config }

    val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
    handlerThread.start()
    val backgroundHandler = Handler(handlerThread.looper)

    heapDumpTrigger = HeapDumpTrigger(
      application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, heapDumper,
      configProvider
    )
    application.registerVisibilityListener { applicationVisible ->
      this.applicationVisible = applicationVisible
      heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
    }
    registerResumedActivityListener(application)
    addDynamicShortcut(application)

    // We post so that the log happens after Application.onCreate()
    mainHandler.post {
      // https://github.com/square/leakcanary/issues/1981
      // We post to a background handler because HeapDumpControl.iCanHasHeap() checks a shared pref
      // which blocks until loaded and that creates a StrictMode violation.
      backgroundHandler.post {
        SharkLog.d {
          when (val iCanHasHeap = HeapDumpControl.iCanHasHeap()) {
            is Yup -> application.getString(R.string.leak_canary_heap_dump_enabled_text)
            is Nope -> application.getString(
              R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
            )
          }
        }
      }
    }
  }

  private fun checkRunningInDebuggableBuild() {
    if (isDebuggableBuild) {
      return
    }

    if (!application.resources.getBoolean(R.bool.leak_canary_allow_in_non_debuggable_build)) {
      throw Error(
        """
        LeakCanary in non-debuggable build

        LeakCanary should only be used in debug builds, but this APK is not debuggable.
        Please follow the instructions on the "Getting started" page to only include LeakCanary in
        debug builds: https://square.github.io/leakcanary/getting_started/

        If you're sure you want to include LeakCanary in a non-debuggable build, follow the
        instructions here: https://square.github.io/leakcanary/recipes/#leakcanary-in-release-builds
      """.trimIndent()
      )
    }
  }

  private fun registerResumedActivityListener(application: Application) {
    application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityResumed(activity: Activity) {
        resumedActivity = activity
      }

      override fun onActivityPaused(activity: Activity) {
        if (resumedActivity === activity) {
          resumedActivity = null
        }
      }
    })
  }

  @Suppress("ReturnCount")
  private fun addDynamicShortcut(application: Application) {
    if (VERSION.SDK_INT < VERSION_CODES.N_MR1) {
      return
    }
    if (!application.resources.getBoolean(R.bool.leak_canary_add_dynamic_shortcut)) {
      return
    }
    if (isInstantApp) {
      // Instant Apps don't have access to ShortcutManager
      return
    }

    val shortcutManager = application.getSystemService(ShortcutManager::class.java)!!
    val dynamicShortcuts = shortcutManager.dynamicShortcuts

    val shortcutInstalled =
      dynamicShortcuts.any { shortcut -> shortcut.id == DYNAMIC_SHORTCUT_ID }

    if (shortcutInstalled) {
      return
    }

    val mainIntent = Intent(Intent.ACTION_MAIN, null)
    mainIntent.addCategory(Intent.CATEGORY_LAUNCHER)
    mainIntent.setPackage(application.packageName)
    val activities = application.packageManager.queryIntentActivities(mainIntent, 0)
      .filter {
        it.activityInfo.name != "leakcanary.internal.activity.LeakLauncherActivity"
      }

    if (activities.isEmpty()) {
      return
    }

    val firstMainActivity = activities.first()
      .activityInfo

    // Displayed on long tap on app icon
    val longLabel: String
    // Label when dropping shortcut to launcher
    val shortLabel: String

    val leakActivityLabel = application.getString(R.string.leak_canary_shortcut_label)

    if (activities.isEmpty()) {
      longLabel = leakActivityLabel
      shortLabel = leakActivityLabel
    } else {
      val firstLauncherActivityLabel = if (firstMainActivity.labelRes != 0) {
        application.getString(firstMainActivity.labelRes)
      } else {
        application.packageManager.getApplicationLabel(application.applicationInfo)
      }
      val fullLengthLabel = "$firstLauncherActivityLabel $leakActivityLabel"
      // short label should be under 10 and long label under 25
      if (fullLengthLabel.length > 10) {
        if (fullLengthLabel.length <= 25) {
          longLabel = fullLengthLabel
          shortLabel = leakActivityLabel
        } else {
          longLabel = leakActivityLabel
          shortLabel = leakActivityLabel
        }
      } else {
        longLabel = fullLengthLabel
        shortLabel = fullLengthLabel
      }
    }

    val componentName = ComponentName(firstMainActivity.packageName, firstMainActivity.name)

    val shortcutCount = dynamicShortcuts.count { shortcutInfo ->
      shortcutInfo.activity == componentName
    } + shortcutManager.manifestShortcuts.count { shortcutInfo ->
      shortcutInfo.activity == componentName
    }

    if (shortcutCount >= shortcutManager.maxShortcutCountPerActivity) {
      return
    }

    val intent = LeakCanary.newLeakDisplayActivityIntent()
    intent.action = "Dummy Action because Android is stupid"
    val shortcut = Builder(application, DYNAMIC_SHORTCUT_ID)
      .setLongLabel(longLabel)
      .setShortLabel(shortLabel)
      .setActivity(componentName)
      .setIcon(Icon.createWithResource(application, R.mipmap.leak_canary_icon))
      .setIntent(intent)
      .build()

    try {
      shortcutManager.addDynamicShortcuts(listOf(shortcut))
    } catch (ignored: Throwable) {
      SharkLog.d(ignored) {
        "Could not add dynamic shortcut. " +
          "shortcutCount=$shortcutCount, " +
          "maxShortcutCountPerActivity=${shortcutManager.maxShortcutCountPerActivity}"
      }
    }
  }

  override fun onObjectRetained() = scheduleRetainedObjectCheck()

  fun scheduleRetainedObjectCheck() {
    if (this::heapDumpTrigger.isInitialized) {
      heapDumpTrigger.scheduleRetainedObjectCheck()
    }
  }

  fun onDumpHeapReceived(forceDump: Boolean) {
    if (this::heapDumpTrigger.isInitialized) {
      heapDumpTrigger.onDumpHeapReceived(forceDump)
    }
  }

  fun setEnabledBlocking(
    componentClassName: String,
    enabled: Boolean
  ) {
    val component = ComponentName(application, componentClassName)
    val newState =
      if (enabled) COMPONENT_ENABLED_STATE_ENABLED else COMPONENT_ENABLED_STATE_DISABLED
    // Blocks on IPC.
    application.packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP)
  }

  private const val LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump"
}

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ProviderInfo
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.ParcelFileDescriptor
import android.provider.OpenableColumns
import android.text.TextUtils
import android.webkit.MimeTypeMap
import org.xmlpull.v1.XmlPullParser.END_DOCUMENT
import org.xmlpull.v1.XmlPullParser.START_TAG
import org.xmlpull.v1.XmlPullParserException
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.util.HashMap

/**
 * Copy of androidx.core.content.FileProvider, converted to Kotlin.
 */
internal class LeakCanaryFileProvider : ContentProvider() {

  private var mStrategy: PathStrategy? = null

  /**
   * The default FileProvider implementation does not need to be initialized. If you want to
   * override this method, you must provide your own subclass of FileProvider.
   */
  override fun onCreate(): Boolean {
    return true
  }

  /**
   * After the FileProvider is instantiated, this method is called to provide the system with
   * information about the provider.
   *
   * @param context A [Context] for the current component.
   * @param info A [ProviderInfo] for the new provider.
   */
  override fun attachInfo(
    context: Context,
    info: ProviderInfo
  ) {
    super.attachInfo(context, info)

    // Sanity check our security
    if (info.exported) {
      throw SecurityException("Provider must not be exported")
    }
    if (!info.grantUriPermissions) {
      throw SecurityException("Provider must grant uri permissions")
    }

    mStrategy = getPathStrategy(context, info.authority)
  }

  /**
   * Use a content URI returned by
   * [getUriForFile()][.getUriForFile] to get information about a file
   * managed by the FileProvider.
   * FileProvider reports the column names defined in [android.provider.OpenableColumns]:
   *
   *  * [android.provider.OpenableColumns.DISPLAY_NAME]
   *  * [android.provider.OpenableColumns.SIZE]
   *
   * For more information, see
   * [ ContentProvider.query()][ContentProvider.query].
   *
   * @param uri A content URI returned by [.getUriForFile].
   * @param projection The list of columns to put into the [Cursor]. If null all columns are
   * included.
   * @param selection Selection criteria to apply. If null then all data that matches the content
   * URI is returned.
   * @param selectionArgs An array of [java.lang.String], containing arguments to bind to
   * the *selection* parameter. The *query* method scans *selection* from left to
   * right and iterates through *selectionArgs*, replacing the current "?" character in
   * *selection* with the value at the current position in *selectionArgs*. The
   * values are bound to *selection* as [java.lang.String] values.
   * @param sortOrder A [java.lang.String] containing the column name(s) on which to sort
   * the resulting [Cursor].
   * @return A [Cursor] containing the results of the query.
   */
  override fun query(
    uri: Uri,
    projectionArg: Array<String>?,
    selection: String?,
    selectionArgs: Array<String>?,
    sortOrder: String?
  ): Cursor? {
    val projection = projectionArg ?: COLUMNS
    // ContentProvider has already checked granted permissions
    val file = mStrategy!!.getFileForUri(uri)

    var cols = arrayOfNulls<String>(projection.size)
    var values = arrayOfNulls<Any>(projection.size)
    var i = 0
    for (col in projection) {
      if (OpenableColumns.DISPLAY_NAME == col) {
        cols[i] = OpenableColumns.DISPLAY_NAME
        values[i++] = file.name
      } else if (OpenableColumns.SIZE == col) {
        cols[i] = OpenableColumns.SIZE
        values[i++] = file.length()
      }
    }

    cols = copyOfStringArray(cols, i)
    values = copyOfAnyArray(values, i)

    val cursor = MatrixCursor(cols, 1)
    cursor.addRow(values)
    return cursor
  }

  /**
   * Returns the MIME type of a content URI returned by
   * [getUriForFile()][.getUriForFile].
   *
   * @param uri A content URI returned by
   * [getUriForFile()][.getUriForFile].
   * @return If the associated file has an extension, the MIME type associated with that
   * extension; otherwise `application/octet-stream`.
   */
  override fun getType(uri: Uri): String? {
    // ContentProvider has already checked granted permissions
    val file = mStrategy!!.getFileForUri(uri)

    val lastDot = file.name.lastIndexOf('.')
    if (lastDot >= 0) {
      val extension = file.name.substring(lastDot + 1)
      val mime = MimeTypeMap.getSingleton()
        .getMimeTypeFromExtension(extension)
      if (mime != null) {
        return mime
      }
    }

    return "application/octet-stream"
  }

  /**
   * By default, this method throws an [java.lang.UnsupportedOperationException]. You must
   * subclass FileProvider if you want to provide different functionality.
   */
  override fun insert(
    uri: Uri,
    values: ContentValues?
  ): Uri? {
    throw UnsupportedOperationException("No external inserts")
  }

  /**
   * By default, this method throws an [java.lang.UnsupportedOperationException]. You must
   * subclass FileProvider if you want to provide different functionality.
   */
  override fun update(
    uri: Uri,
    values: ContentValues?,
    selection: String?,
    selectionArgs: Array<String>?
  ): Int {
    throw UnsupportedOperationException("No external updates")
  }

  /**
   * Deletes the file associated with the specified content URI, as
   * returned by [getUriForFile()][.getUriForFile]. Notice that this
   * method does **not** throw an [java.io.IOException]; you must check its return value.
   *
   * @param uri A content URI for a file, as returned by
   * [getUriForFile()][.getUriForFile].
   * @param selection Ignored. Set to `null`.
   * @param selectionArgs Ignored. Set to `null`.
   * @return 1 if the delete succeeds; otherwise, 0.
   */
  override fun delete(
    uri: Uri,
    selection: String?,
    selectionArgs: Array<String>?
  ): Int {
    // ContentProvider has already checked granted permissions
    val file = mStrategy!!.getFileForUri(uri)
    return if (file.delete()) 1 else 0
  }

  /**
   * By default, FileProvider automatically returns the
   * [ParcelFileDescriptor] for a file associated with a `content://`
   * [Uri]. To get the [ParcelFileDescriptor], call
   * [ ContentResolver.openFileDescriptor][android.content.ContentResolver.openFileDescriptor].
   *
   * To override this method, you must provide your own subclass of FileProvider.
   *
   * @param uri A content URI associated with a file, as returned by
   * [getUriForFile()][.getUriForFile].
   * @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and
   * write access, or "rwt" for read and write access that truncates any existing file.
   * @return A new [ParcelFileDescriptor] with which you can access the file.
   */
  @Throws(FileNotFoundException::class)
  override fun openFile(
    uri: Uri,
    mode: String
  ): ParcelFileDescriptor? {
    // ContentProvider has already checked granted permissions
    val file = mStrategy!!.getFileForUri(uri)
    val fileMode = modeToMode(mode)
    return ParcelFileDescriptor.open(file, fileMode)
  }

  /**
   * Strategy for mapping between [File] and [Uri].
   *
   *
   * Strategies must be symmetric so that mapping a [File] to a
   * [Uri] and then back to a [File] points at the original
   * target.
   *
   *
   * Strategies must remain consistent across app launches, and not rely on
   * dynamic state. This ensures that any generated [Uri] can still be
   * resolved if your process is killed and later restarted.
   *
   * @see SimplePathStrategy
   */
  internal interface PathStrategy {
    /**
     * Return a [Uri] that represents the given [File].
     */
    fun getUriForFile(file: File): Uri

    /**
     * Return a [File] that represents the given [Uri].
     */
    fun getFileForUri(uri: Uri): File
  }

  /**
   * Strategy that provides access to files living under a narrow allowlist of
   * filesystem roots. It will throw [SecurityException] if callers try
   * accessing files outside the configured roots.
   *
   *
   * For example, if configured with
   * `addRoot("myfiles", context.getFilesDir())`, then
   * `context.getFileStreamPath("foo.txt")` would map to
   * `content://myauthority/myfiles/foo.txt`.
   */
  internal class SimplePathStrategy(private val mAuthority: String) : PathStrategy {
    private val mRoots = HashMap<String, File>()

    /**
     * Add a mapping from a name to a filesystem root. The provider only offers
     * access to files that live under configured roots.
     */
    fun addRoot(
      name: String,
      root: File
    ) {

      if (TextUtils.isEmpty(name)) {
        throw IllegalArgumentException("Name must not be empty")
      }

      mRoots[name] = try {
        // Resolve to canonical path to keep path checking fast
        root.canonicalFile
      } catch (e: IOException) {
        throw IllegalArgumentException(
          "Failed to resolve canonical path for $root", e
        )
      }
    }

    override fun getUriForFile(file: File): Uri {
      var path: String
      try {
        path = file.canonicalPath
      } catch (e: IOException) {
        throw IllegalArgumentException("Failed to resolve canonical path for $file")
      }

      // Find the most-specific root path
      var mostSpecific: MutableMap.MutableEntry<String, File>? = null
      for (root in mRoots.entries) {
        val rootPath = root.value.path
        if (path.startsWith(
            rootPath
          ) && (mostSpecific == null || rootPath.length > mostSpecific.value.path.length)
        ) {
          mostSpecific = root
        }
      }

      if (mostSpecific == null) {
        throw IllegalArgumentException(
          "Failed to find configured root that contains $path"
        )
      }

      // Start at first char of path under root
      val rootPath = mostSpecific.value.path
      if (rootPath.endsWith("/")) {
        path = path.substring(rootPath.length)
      } else {
        path = path.substring(rootPath.length + 1)
      }

      // Encode the tag and path separately
      path = Uri.encode(mostSpecific.key) + '/'.toString() + Uri.encode(path, "/")
      return Uri.Builder()
        .scheme("content")
        .authority(mAuthority)
        .encodedPath(path)
        .build()
    }

    override fun getFileForUri(uri: Uri): File {
      var path = uri.encodedPath

      val splitIndex = path!!.indexOf('/', 1)
      val tag = Uri.decode(path.substring(1, splitIndex))
      path = Uri.decode(path.substring(splitIndex + 1))

      val root =
        mRoots[tag] ?: throw IllegalArgumentException("Unable to find configured root for $uri")

      var file = File(root, path!!)
      try {
        file = file.canonicalFile
      } catch (e: IOException) {
        throw IllegalArgumentException("Failed to resolve canonical path for $file")
      }

      if (!file.path.startsWith(root.path)) {
        throw SecurityException("Resolved path jumped beyond configured root")
      }

      return file
    }
  }

  companion object {
    private val COLUMNS = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)

    private val META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS"

    private val TAG_ROOT_PATH = "root-path"
    private val TAG_FILES_PATH = "files-path"
    private val TAG_CACHE_PATH = "cache-path"
    private val TAG_EXTERNAL = "external-path"
    private val TAG_EXTERNAL_FILES = "external-files-path"
    private val TAG_EXTERNAL_CACHE = "external-cache-path"
    private val TAG_EXTERNAL_MEDIA = "external-media-path"

    private val ATTR_NAME = "name"
    private val ATTR_PATH = "path"

    private val DEVICE_ROOT = File("/")

    private val sCache = HashMap<String, PathStrategy>()

    /**
     * Return a content URI for a given [File]. Specific temporary
     * permissions for the content URI can be set with
     * [Context.grantUriPermission], or added
     * to an [Intent] by calling [setData()][Intent.setData] and then
     * [setFlags()][Intent.setFlags]; in both cases, the applicable flags are
     * [Intent.FLAG_GRANT_READ_URI_PERMISSION] and
     * [Intent.FLAG_GRANT_WRITE_URI_PERMISSION]. A FileProvider can only return a
     * `content` [Uri] for file paths defined in their `<paths>`
     * meta-data element. See the Class Overview for more information.
     *
     * @param context A [Context] for the current component.
     * @param authority The authority of a [FileProvider] defined in a
     * `<provider>` element in your app's manifest.
     * @param file A [File] pointing to the filename for which you want a
     * `content` [Uri].
     * @return A content URI for the file.
     * @throws IllegalArgumentException When the given [File] is outside
     * the paths supported by the provider.
     */
    fun getUriForFile(
      context: Context,
      authority: String,
      file: File
    ): Uri {
      val strategy = getPathStrategy(context, authority)
      return strategy!!.getUriForFile(file)
    }

    /**
     * Return [PathStrategy] for given authority, either by parsing or
     * returning from cache.
     */
    private fun getPathStrategy(
      context: Context,
      authority: String
    ): PathStrategy? {
      var strat: PathStrategy?
      synchronized(sCache) {
        strat = sCache[authority]
        if (strat == null) {
          try {
            strat = parsePathStrategy(context, authority)
          } catch (e: IOException) {
            throw IllegalArgumentException(
              "Failed to parse $META_DATA_FILE_PROVIDER_PATHS meta-data", e
            )
          } catch (e: XmlPullParserException) {
            throw IllegalArgumentException(
              "Failed to parse $META_DATA_FILE_PROVIDER_PATHS meta-data", e
            )
          }
          sCache[authority] = strat!!
        }
      }
      return strat
    }

    /**
     * Parse and return [PathStrategy] for given authority as defined in
     * [.META_DATA_FILE_PROVIDER_PATHS] `<meta-data>`.
     *
     * @see .getPathStrategy
     */
    @Throws(IOException::class, XmlPullParserException::class)
    private fun parsePathStrategy(
      context: Context,
      authority: String
    ): PathStrategy {
      val strat = SimplePathStrategy(authority)

      val info = context.packageManager
        .resolveContentProvider(authority, PackageManager.GET_META_DATA)
        ?: throw IllegalArgumentException(
          "Couldn't find meta-data for provider with authority $authority"
        )
      val resourceParser = info.loadXmlMetaData(
        context.packageManager, META_DATA_FILE_PROVIDER_PATHS
      ) ?: throw IllegalArgumentException(
        "Missing $META_DATA_FILE_PROVIDER_PATHS meta-data"
      )

      var type = 0
      while (({ type = resourceParser.next(); type }()) != END_DOCUMENT) {
        if (type == START_TAG) {
          val tag = resourceParser.name

          val name = resourceParser.getAttributeValue(null, ATTR_NAME)
          val path = resourceParser.getAttributeValue(null, ATTR_PATH)

          var target: File? = null
          if (TAG_ROOT_PATH == tag) {
            target = DEVICE_ROOT
          } else if (TAG_FILES_PATH == tag) {
            target = context.filesDir
          } else if (TAG_CACHE_PATH == tag) {
            target = context.cacheDir
          } else if (TAG_EXTERNAL == tag) {
            target = Environment.getExternalStorageDirectory()
          } else if (TAG_EXTERNAL_FILES == tag) {
            val externalFilesDirs = getExternalFilesDirs(context, null)
            if (externalFilesDirs.isNotEmpty()) {
              target = externalFilesDirs[0]
            }
          } else if (TAG_EXTERNAL_CACHE == tag) {
            val externalCacheDirs = getExternalCacheDirs(context)
            if (externalCacheDirs.isNotEmpty()) {
              target = externalCacheDirs[0]
            }
          } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && TAG_EXTERNAL_MEDIA == tag) {
            val externalMediaDirs = context.externalMediaDirs
            if (externalMediaDirs.isNotEmpty()) {
              target = externalMediaDirs[0]
            }
          }

          if (target != null) {
            strat.addRoot(name, buildPath(target, path))
          }
        }
      }

      return strat
    }

    private fun getExternalFilesDirs(
      context: Context,
      type: String?
    ): Array<File> {
      return if (Build.VERSION.SDK_INT >= 19) {
        context.getExternalFilesDirs(type)
      } else {
        arrayOf<File>(context.getExternalFilesDir(type)!!)
      }
    }

    private fun getExternalCacheDirs(context: Context): Array<File> {
      return if (Build.VERSION.SDK_INT >= 19) {
        context.externalCacheDirs
      } else {
        arrayOf(context.externalCacheDir!!)
      }
    }

    /**
     * Copied from ContentResolver.java
     */
    private fun modeToMode(mode: String): Int {
      val modeBits: Int
      if ("r" == mode) {
        modeBits = ParcelFileDescriptor.MODE_READ_ONLY
      } else if ("w" == mode || "wt" == mode) {
        modeBits = (ParcelFileDescriptor.MODE_WRITE_ONLY
          or ParcelFileDescriptor.MODE_CREATE
          or ParcelFileDescriptor.MODE_TRUNCATE)
      } else if ("wa" == mode) {
        modeBits = (ParcelFileDescriptor.MODE_WRITE_ONLY
          or ParcelFileDescriptor.MODE_CREATE
          or ParcelFileDescriptor.MODE_APPEND)
      } else if ("rw" == mode) {
        modeBits = ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE
      } else if ("rwt" == mode) {
        modeBits = (ParcelFileDescriptor.MODE_READ_WRITE
          or ParcelFileDescriptor.MODE_CREATE
          or ParcelFileDescriptor.MODE_TRUNCATE)
      } else {
        throw IllegalArgumentException("Invalid mode: $mode")
      }
      return modeBits
    }

    private fun buildPath(
      base: File,
      vararg segments: String
    ): File {
      var cur = base
      for (segment in segments) {
        cur = File(cur, segment)
      }
      return cur
    }

    private fun copyOfStringArray(
      original: Array<String?>,
      newLength: Int
    ): Array<String?> {
      val result = arrayOfNulls<String>(newLength)
      System.arraycopy(original, 0, result, 0, newLength)
      return result
    }

    private fun copyOfAnyArray(
      original: Array<Any?>,
      newLength: Int
    ): Array<Any?> {
      val result = arrayOfNulls<Any>(newLength)
      System.arraycopy(original, 0, result, 0, newLength)
      return result
    }
  }
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import java.util.concurrent.ThreadFactory

/**
 * This is intended to only be used with a single thread executor.
 */
internal class LeakCanarySingleThreadFactory(threadName: String) : ThreadFactory {

  private val threadName: String = "LeakCanary-$threadName"

  override fun newThread(runnable: Runnable): Thread {
    return Thread(runnable, threadName)
  }
}

/*
 * Copyright (C) 2016 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.annotation.TargetApi
import android.content.Context
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.M
import android.os.Environment
import android.os.Environment.DIRECTORY_DOWNLOADS
import com.squareup.leakcanary.core.R
import leakcanary.internal.NotificationType.LEAKCANARY_LOW
import shark.SharkLog
import java.io.File
import java.io.FilenameFilter
import java.text.SimpleDateFormat
import java.util.ArrayList
import java.util.Date
import java.util.Locale

/**
 * Provides access to where heap dumps and analysis results will be stored.
 */
internal class LeakDirectoryProvider constructor(
  context: Context,
  private val maxStoredHeapDumps: () -> Int,
  private val requestExternalStoragePermission: () -> Boolean
) {
  private val context: Context = context.applicationContext

  fun listFiles(filter: FilenameFilter): MutableList<File> {
    if (!hasStoragePermission() && requestExternalStoragePermission()) {
      requestWritePermissionNotification()
    }
    val files = ArrayList<File>()

    val externalFiles = externalStorageDirectory().listFiles(filter)
    if (externalFiles != null) {
      files.addAll(externalFiles)
    }

    val appFiles = appStorageDirectory().listFiles(filter)
    if (appFiles != null) {
      files.addAll(appFiles)
    }
    return files
  }

  fun newHeapDumpFile(): File? {
    cleanupOldHeapDumps()

    var storageDirectory = externalStorageDirectory()
    if (!directoryWritableAfterMkdirs(storageDirectory)) {
      if (!hasStoragePermission()) {
        if (requestExternalStoragePermission()) {
          SharkLog.d { "WRITE_EXTERNAL_STORAGE permission not granted, requesting" }
          requestWritePermissionNotification()
        } else {
          SharkLog.d { "WRITE_EXTERNAL_STORAGE permission not granted, ignoring" }
        }
      } else {
        val state = Environment.getExternalStorageState()
        if (Environment.MEDIA_MOUNTED != state) {
          SharkLog.d { "External storage not mounted, state: $state" }
        } else {
          SharkLog.d {
            "Could not create heap dump directory in external storage: [${storageDirectory.absolutePath}]"
          }
        }
      }
      // Fallback to app storage.
      storageDirectory = appStorageDirectory()
      if (!directoryWritableAfterMkdirs(storageDirectory)) {
        SharkLog.d {
          "Could not create heap dump directory in app storage: [${storageDirectory.absolutePath}]"
        }
        return null
      }
    }

    val fileName = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS'.hprof'", Locale.US).format(Date())
    return File(storageDirectory, fileName)
  }

  @TargetApi(M) fun hasStoragePermission(): Boolean {
    if (SDK_INT < M) {
      return true
    }
    // Once true, this won't change for the life of the process so we can cache it.
    if (writeExternalStorageGranted) {
      return true
    }
    writeExternalStorageGranted =
      context.checkSelfPermission(WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED
    return writeExternalStorageGranted
  }

  fun requestWritePermissionNotification() {
    if (permissionNotificationDisplayed) {
      return
    }
    permissionNotificationDisplayed = true

    val pendingIntent = RequestStoragePermissionActivity.createPendingIntent(context)
    val contentTitle = context.getString(
      R.string.leak_canary_permission_notification_title
    )
    val packageName = context.packageName
    val contentText =
      context.getString(R.string.leak_canary_permission_notification_text, packageName)

    Notifications.showNotification(
      context, contentTitle, contentText, pendingIntent,
      R.id.leak_canary_notification_write_permission, LEAKCANARY_LOW
    )
  }

  @Suppress("DEPRECATION")
  private fun externalStorageDirectory(): File {
    val downloadsDirectory = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS)
    return File(downloadsDirectory, "leakcanary-" + context.packageName)
  }

  private fun appStorageDirectory(): File {
    val appFilesDirectory = context.cacheDir
    return File(appFilesDirectory, "leakcanary")
  }

  private fun directoryWritableAfterMkdirs(directory: File): Boolean {
    val success = directory.mkdirs()
    return (success || directory.exists()) && directory.canWrite()
  }

  private fun cleanupOldHeapDumps() {
    val hprofFiles = listFiles(FilenameFilter { _, name ->
      name.endsWith(
        HPROF_SUFFIX
      )
    })
    val maxStoredHeapDumps = maxStoredHeapDumps()
    if (maxStoredHeapDumps < 1) {
      throw IllegalArgumentException("maxStoredHeapDumps must be at least 1")
    }

    val filesToRemove = hprofFiles.size - maxStoredHeapDumps
    if (filesToRemove > 0) {
      SharkLog.d { "Removing $filesToRemove heap dumps" }
      // Sort with oldest modified first.
      hprofFiles.sortWith(Comparator { lhs, rhs ->
        java.lang.Long.valueOf(lhs.lastModified())
          .compareTo(rhs.lastModified())
      })
      for (i in 0 until filesToRemove) {
        val path = hprofFiles[i].absolutePath
        val deleted = hprofFiles[i].delete()
        if (deleted) {
          filesDeletedTooOld += path
        } else {
          SharkLog.d { "Could not delete old hprof file ${hprofFiles[i].path}" }
        }
      }
    }
  }

  companion object {
    @Volatile private var writeExternalStorageGranted: Boolean = false
    @Volatile private var permissionNotificationDisplayed: Boolean = false

    private val filesDeletedTooOld = mutableListOf<String>()
    val filesDeletedRemoveLeak = mutableListOf<String>()

    private const val HPROF_SUFFIX = ".hprof"

    fun hprofDeleteReason(file: File): String {
      val path = file.absolutePath
      return when {
        filesDeletedTooOld.contains(path) -> "older than all other hprof files"
        filesDeletedRemoveLeak.contains(path) -> "leak manually removed"
        else -> "unknown"
      }
    }
  }
}

package leakcanary.internal

import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import leakcanary.internal.NotificationReceiver.Action.CANCEL_NOTIFICATION
import leakcanary.internal.NotificationReceiver.Action.DUMP_HEAP
import shark.SharkLog

internal class NotificationReceiver : BroadcastReceiver() {

  enum class Action {
    DUMP_HEAP,
    CANCEL_NOTIFICATION
  }

  override fun onReceive(
    context: Context,
    intent: Intent
  ) {
    when (intent.action) {
      DUMP_HEAP.name -> {
        InternalLeakCanary.onDumpHeapReceived(forceDump = false)
      }
      CANCEL_NOTIFICATION.name -> {
        // Do nothing, the notification has auto cancel true.
      }
      else -> {
        SharkLog.d { "NotificationReceiver received unknown intent action for $intent" }
      }
    }
  }

  companion object {
    fun pendingIntent(
      context: Context,
      action: Action
    ): PendingIntent {
      val broadcastIntent = Intent(context, NotificationReceiver::class.java)
      broadcastIntent.action = action.name
      val flags = if (Build.VERSION.SDK_INT >= 23) {
        PendingIntent.FLAG_IMMUTABLE
      } else {
        0
      }
      return PendingIntent.getBroadcast(context, 0, broadcastIntent, flags)
    }
  }
}

package leakcanary.internal

import com.squareup.leakcanary.core.R

internal enum class NotificationType(
  val nameResId: Int,
  val importance: Int
) {
  LEAKCANARY_LOW(
    R.string.leak_canary_notification_channel_low, IMPORTANCE_LOW
  ),
  LEAKCANARY_MAX(
    R.string.leak_canary_notification_channel_result, IMPORTANCE_MAX
  );
}

private const val IMPORTANCE_LOW = 2
private const val IMPORTANCE_MAX = 5

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.JELLY_BEAN
import android.os.Build.VERSION_CODES.O
import com.squareup.leakcanary.core.R
import leakcanary.internal.InternalLeakCanary.FormFactor.MOBILE

internal object Notifications {

  val canShowNotification: Boolean
    get() = canShowBackgroundNotifications || InternalLeakCanary.applicationVisible

  // Instant apps cannot show background notifications
  // See https://github.com/square/leakcanary/issues/1197
  // TV devices also can't do notifications
  private val canShowBackgroundNotifications =
    InternalLeakCanary.formFactor == MOBILE && !InternalLeakCanary.isInstantApp

  @Suppress("LongParameterList")
  fun showNotification(
    context: Context,
    contentTitle: CharSequence,
    contentText: CharSequence,
    pendingIntent: PendingIntent?,
    notificationId: Int,
    type: NotificationType
  ) {
    if (!canShowNotification) {
      return
    }

    val builder = if (SDK_INT >= O) {
      Notification.Builder(context, type.name)
    } else Notification.Builder(context)

    builder
      .setContentText(contentText)
      .setContentTitle(contentTitle)
      .setAutoCancel(true)
      .setContentIntent(pendingIntent)

    val notification =
      buildNotification(context, builder, type)
    val notificationManager =
      context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    notificationManager.notify(notificationId, notification)
  }

  fun buildNotification(
    context: Context,
    builder: Notification.Builder,
    type: NotificationType
  ): Notification {
    builder.setSmallIcon(R.drawable.leak_canary_leak)
      .setWhen(System.currentTimeMillis())

    if (SDK_INT >= O) {
      val notificationManager =
        context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
      var notificationChannel: NotificationChannel? =
        notificationManager.getNotificationChannel(type.name)
      if (notificationChannel == null) {
        val channelName = context.getString(type.nameResId)
        notificationChannel =
          NotificationChannel(type.name, channelName, type.importance)
        notificationManager.createNotificationChannel(notificationChannel)
      }
      builder.setChannelId(type.name)
      builder.setGroup(type.name)
    }

    return if (SDK_INT < JELLY_BEAN) {
      @Suppress("DEPRECATION")
      builder.notification
    } else {
      builder.build()
    }
  }
}

package leakcanary.internal

internal sealed class RetainInstanceEvent {
  object NoMoreObjects : RetainInstanceEvent()
  sealed class CountChanged : RetainInstanceEvent() {
    class BelowThreshold(val retainedCount: Int) : RetainInstanceEvent()
    class DumpingDisabled(val reason: String) : RetainInstanceEvent()
    object DumpHappenedRecently : RetainInstanceEvent()
  }
}

/**
 * Called by LeakCanary when the number of retained instances updates .
 */
internal interface OnRetainInstanceListener {

  /**
   * Called when there's a change to the Retained Instances. See [RetainInstanceEvent] for
   * possible events.
   */
  fun onEvent(event: RetainInstanceEvent)
}

internal class DefaultOnRetainInstanceListener : OnRetainInstanceListener {

  override fun onEvent(event: RetainInstanceEvent) {}
}

/*
 * Copyright (C) 2016 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.annotation.TargetApi
import android.app.Activity
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Bundle
import android.os.Build
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import com.squareup.leakcanary.core.R

@TargetApi(Build.VERSION_CODES.M) //
internal class RequestStoragePermissionActivity : Activity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    if (savedInstanceState == null) {
      if (hasStoragePermission()) {
        finish()
        return
      }
      val permissions = arrayOf(WRITE_EXTERNAL_STORAGE)
      requestPermissions(permissions, 42)
    }
  }

  override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<String>,
    grantResults: IntArray
  ) {
    if (!hasStoragePermission()) {
      Toast.makeText(application, R.string.leak_canary_permission_not_granted, LENGTH_LONG)
        .show()
    }
    finish()
  }

  override fun finish() {
    // Reset the animation to avoid flickering.
    overridePendingTransition(0, 0)
    super.finish()
  }

  private fun hasStoragePermission(): Boolean {
    return checkSelfPermission(WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED
  }

  companion object {

    fun createPendingIntent(context: Context): PendingIntent {
      val intent = Intent(context, RequestStoragePermissionActivity::class.java)
      intent.flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TOP
      val flags = if (Build.VERSION.SDK_INT >= 23) {
        FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
      } else {
        FLAG_UPDATE_CURRENT
      }
      return PendingIntent.getActivity(context, 1, intent, flags)
    }
  }
}

/*
 * Copyright (C) 2018 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import com.squareup.leakcanary.core.R

internal class RowElementLayout(
  context: Context,
  attrs: AttributeSet
) : ViewGroup(context, attrs) {

  private val connectorWidth: Int
  private val rowMargins: Int
  private val moreSize: Int
  private val minHeight: Int
  private val titleMarginTop: Int
  private val moreMarginTop: Int

  private var connector: View? = null
  private var title: View? = null

  init {
    val resources = resources
    connectorWidth = resources.getDimensionPixelSize(R.dimen.leak_canary_connector_width)
    rowMargins = resources.getDimensionPixelSize(R.dimen.leak_canary_row_margins)
    moreSize = resources.getDimensionPixelSize(R.dimen.leak_canary_more_size)
    minHeight = resources.getDimensionPixelSize(R.dimen.leak_canary_row_min)
    titleMarginTop = resources.getDimensionPixelSize(R.dimen.leak_canary_row_title_margin_top)
    moreMarginTop = resources.getDimensionPixelSize(R.dimen.leak_canary_more_margin_top)
  }

  override fun onFinishInflate() {
    super.onFinishInflate()
    connector = findViewById(R.id.leak_canary_row_connector)
    title = findViewById(R.id.leak_canary_row_title)
  }

  override fun onMeasure(
    widthMeasureSpec: Int,
    heightMeasureSpec: Int
  ) {
    val availableWidth = View.MeasureSpec.getSize(widthMeasureSpec)
    val titleWidth = availableWidth - connectorWidth - moreSize - 4 * rowMargins
    val titleWidthSpec = View.MeasureSpec.makeMeasureSpec(titleWidth, View.MeasureSpec.AT_MOST)
    val titleHeightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
    title!!.measure(titleWidthSpec, titleHeightSpec)

    var totalHeight = titleMarginTop + title!!.measuredHeight
    totalHeight = Math.max(totalHeight, minHeight)

    val connectorWidthSpec =
      View.MeasureSpec.makeMeasureSpec(connectorWidth, View.MeasureSpec.EXACTLY)
    val connectorHeightSpec =
      View.MeasureSpec.makeMeasureSpec(totalHeight, View.MeasureSpec.EXACTLY)

    connector!!.measure(connectorWidthSpec, connectorHeightSpec)
    setMeasuredDimension(availableWidth, totalHeight)
  }

  override fun onLayout(
    changed: Boolean,
    l: Int,
    t: Int,
    r: Int,
    b: Int
  ) {
    val connectorRight = rowMargins + connector!!.measuredWidth
    connector!!.layout(rowMargins, 0, connectorRight, connector!!.measuredHeight)

    val titleLeft = connectorRight + rowMargins
    val titleBottom = titleMarginTop + title!!.measuredHeight
    title!!.layout(titleLeft, titleMarginTop, titleLeft + title!!.measuredWidth, titleBottom)
  }
}

package leakcanary.internal

import shark.SharkLog
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable

internal fun Serializable.toByteArray(): ByteArray {
  val outputStream = ByteArrayOutputStream()
  ObjectOutputStream(outputStream).writeObject(this)
  return outputStream.toByteArray()
}

internal object Serializables {

  inline fun <reified T> fromByteArray(byteArray: ByteArray): T? {
    val inputStream = ByteArrayInputStream(byteArray)
    return try {
      ObjectInputStream(inputStream).readObject() as? T
    } catch (ignored: Throwable) {
      SharkLog.d(ignored) { "Could not deserialize bytes, ignoring" }
      null
    }
  }
}

/*
 * Copyright (C) 2018 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.text.SpannableStringBuilder
import android.text.style.ReplacementSpan
import android.text.style.UnderlineSpan
import com.squareup.leakcanary.core.R
import leakcanary.internal.navigation.getColorCompat

/**
 * Inspired from https://github.com/flavienlaurent/spans and
 * https://github.com/andyxialm/WavyLineView
 */
internal class SquigglySpan(context: Context) : ReplacementSpan() {

  private val squigglyPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
  private val path: Path
  private val referenceColor: Int
  private val halfStrokeWidth: Float
  private val amplitude: Float
  private val halfWaveHeight: Float
  private val periodDegrees: Float

  private var width: Int = 0

  init {
    val resources = context.resources
    squigglyPaint.style = Paint.Style.STROKE
    squigglyPaint.color = context.getColorCompat(R.color.leak_canary_leak)
    val strokeWidth =
      resources.getDimensionPixelSize(R.dimen.leak_canary_squiggly_span_stroke_width)
        .toFloat()
    squigglyPaint.strokeWidth = strokeWidth

    halfStrokeWidth = strokeWidth / 2
    amplitude = resources.getDimensionPixelSize(R.dimen.leak_canary_squiggly_span_amplitude)
      .toFloat()
    periodDegrees =
      resources.getDimensionPixelSize(R.dimen.leak_canary_squiggly_span_period_degrees)
        .toFloat()
    path = Path()
    val waveHeight = 2 * amplitude + strokeWidth
    halfWaveHeight = waveHeight / 2
    referenceColor = context.getColorCompat(R.color.leak_canary_reference)
  }

  override fun getSize(
    paint: Paint,
    text: CharSequence,
    start: Int,
    end: Int,
    fm: Paint.FontMetricsInt?
  ): Int {
    width = paint.measureText(text, start, end)
      .toInt()
    return width
  }

  override fun draw(
    canvas: Canvas,
    text: CharSequence,
    start: Int,
    end: Int,
    x: Float,
    top: Int,
    y: Int,
    bottom: Int,
    paint: Paint
  ) {
    squigglyHorizontalPath(
      path,
      x + halfStrokeWidth,
      x + width - halfStrokeWidth,
      bottom - halfWaveHeight,
      amplitude, periodDegrees
    )
    canvas.drawPath(path, squigglyPaint)

    paint.color = referenceColor
    canvas.drawText(text, start, end, x, y.toFloat(), paint)
  }

  companion object {

    fun replaceUnderlineSpans(
      builder: SpannableStringBuilder,
      context: Context
    ) {
      val underlineSpans = builder.getSpans(0, builder.length, UnderlineSpan::class.java)
      for (span in underlineSpans) {
        val start = builder.getSpanStart(span)
        val end = builder.getSpanEnd(span)
        builder.removeSpan(span)
        builder.setSpan(SquigglySpan(context), start, end, 0)
      }
    }

    @Suppress("LongParameterList")
    private fun squigglyHorizontalPath(
      path: Path,
      left: Float,
      right: Float,
      centerY: Float,
      amplitude: Float,
      periodDegrees: Float
    ) {
      path.reset()

      var y: Float
      path.moveTo(left, centerY)
      val period = (2 * Math.PI / periodDegrees).toFloat()

      var x = 0f
      while (x <= right - left) {
        y = (amplitude * Math.sin((40 + period * x).toDouble()) + centerY).toFloat()
        path.lineTo(left + x, y)
        x += 1f
      }
    }
  }
}

package leakcanary.internal

import android.app.Activity
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_SCREEN_OFF
import android.content.Intent.ACTION_SCREEN_ON
import android.content.IntentFilter
import leakcanary.internal.friendly.noOpDelegate

internal class VisibilityTracker(
  private val listener: (Boolean) -> Unit
) : Application.ActivityLifecycleCallbacks by noOpDelegate(), BroadcastReceiver() {

  private var startedActivityCount = 0

  /**
   * Visible activities are any activity started but not stopped yet. An activity can be paused
   * yet visible: this will happen when another activity shows on top with a transparent background
   * and the activity behind won't get touch inputs but still need to render / animate.
   */
  private var hasVisibleActivities: Boolean = false

  /**
   * Assuming screen on by default.
   */
  private var screenOn: Boolean = true

  private var lastUpdate: Boolean = false

  override fun onActivityStarted(activity: Activity) {
    startedActivityCount++
    if (!hasVisibleActivities && startedActivityCount == 1) {
      hasVisibleActivities = true
      updateVisible()
    }
  }

  override fun onActivityStopped(activity: Activity) {
    // This could happen if the callbacks were registered after some activities were already
    // started. In that case we effectively considers those past activities as not visible.
    if (startedActivityCount > 0) {
      startedActivityCount--
    }
    if (hasVisibleActivities && startedActivityCount == 0 && !activity.isChangingConfigurations) {
      hasVisibleActivities = false
      updateVisible()
    }
  }

  override fun onReceive(
    context: Context,
    intent: Intent
  ) {
    screenOn = intent.action != ACTION_SCREEN_OFF
    updateVisible()
  }

  private fun updateVisible() {
    val visible = screenOn && hasVisibleActivities
    if (visible != lastUpdate) {
      lastUpdate = visible
      listener.invoke(visible)
    }
  }
}

internal fun Application.registerVisibilityListener(listener: (Boolean) -> Unit) {
  val visibilityTracker = VisibilityTracker(listener)
  registerActivityLifecycleCallbacks(visibilityTracker)
  registerReceiver(visibilityTracker, IntentFilter().apply {
    addAction(ACTION_SCREEN_ON)
    addAction(ACTION_SCREEN_OFF)
  })
}

package leakcanary.internal

import android.app.Instrumentation.REPORT_KEY_IDENTIFIER
import android.os.Bundle
import androidx.test.internal.runner.listener.InstrumentationResultPrinter.REPORT_KEY_NAME_CLASS
import androidx.test.internal.runner.listener.InstrumentationResultPrinter.REPORT_KEY_NAME_TEST
import androidx.test.internal.runner.listener.InstrumentationResultPrinter.REPORT_KEY_STACK
import androidx.test.internal.runner.listener.InstrumentationResultPrinter.REPORT_VALUE_RESULT_FAILURE
import androidx.test.platform.app.InstrumentationRegistry
import leakcanary.FailTestOnLeakRunListener
import org.junit.runner.Description

internal class InstrumentationTestResultPublisher : TestResultPublisher {
  override fun publishTestFinished() {
  }

  override fun publishTestFailure(
    description: Description,
    trace: String
  ) {
    val bundle = Bundle()
    bundle.putString(REPORT_KEY_IDENTIFIER, FailTestOnLeakRunListener::class.java.name)
    bundle.putString(REPORT_KEY_NAME_CLASS, description.className)
    bundle.putString(REPORT_KEY_NAME_TEST, description.methodName)
    bundle.putString(REPORT_KEY_STACK, trace)

    val instrumentation = InstrumentationRegistry.getInstrumentation()
    instrumentation.sendStatus(REPORT_VALUE_RESULT_FAILURE, bundle)
  }
}

package leakcanary.internal

import android.os.Bundle
import androidx.test.orchestrator.instrumentationlistener.OrchestratedInstrumentationListener
import androidx.test.orchestrator.instrumentationlistener.delegateSendTestNotification
import androidx.test.orchestrator.junit.ParcelableDescription
import androidx.test.orchestrator.junit.ParcelableFailure
import androidx.test.orchestrator.junit.ParcelableResult
import androidx.test.orchestrator.listeners.OrchestrationListenerManager.KEY_TEST_EVENT
import androidx.test.orchestrator.listeners.OrchestrationListenerManager.TestEvent.TEST_FAILURE
import androidx.test.orchestrator.listeners.OrchestrationListenerManager.TestEvent.TEST_FINISHED
import androidx.test.orchestrator.listeners.OrchestrationListenerManager.TestEvent.TEST_RUN_FINISHED
import org.junit.runner.Description

internal class OrchestratorTestResultPublisher(listener: OrchestratedInstrumentationListener) :
  TestResultPublisher {

  private var sendTestFinished: (() -> Unit)? = null

  private var failureBundle: Bundle? = null

  private var receivedTestFinished: Boolean = false

  init {
    val failures = mutableListOf<ParcelableFailure>()
    listener.delegateSendTestNotification { testEventBundle, sendTestNotification ->

      when (testEventBundle.getString(KEY_TEST_EVENT)) {
        TEST_FINISHED.toString() -> {
          sendTestFinished = {
            failureBundle?.let { failureBundle ->
              failures += failureBundle.get("failure") as ParcelableFailure
              sendTestNotification(failureBundle)
            }
            sendTestNotification(testEventBundle)
            // reset for next test if any.
            sendTestFinished = null
            failureBundle = null
            receivedTestFinished = false
          }
          if (receivedTestFinished) {
            sendTestFinished!!.invoke()
          }
        }
        TEST_RUN_FINISHED.toString() -> {
          if (failures.isNotEmpty()) {
            val result = testEventBundle.get("result") as ParcelableResult
            result.failures += failures
          }
          sendTestNotification(testEventBundle)
        }
        else -> sendTestNotification(testEventBundle)
      }
    }
  }

  override fun publishTestFinished() {
    receivedTestFinished = true
    sendTestFinished?.invoke()
  }

  override fun publishTestFailure(
    description: Description,
    trace: String
  ) {
    val result = Bundle()
    val failure = ParcelableFailure(
      ParcelableDescription(description),
      RuntimeException(trace)
    )
    result.putParcelable("failure", failure)
    result.putString(KEY_TEST_EVENT, TEST_FAILURE.toString())
    this.failureBundle = result
  }
}

package leakcanary.internal

import androidx.test.orchestrator.instrumentationlistener.OrchestratedInstrumentationListener
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnitRunner
import org.junit.runner.Description
import shark.SharkLog

internal interface TestResultPublisher {

  fun publishTestFinished()

  fun publishTestFailure(
    description: Description,
    trace: String
  )

  companion object {
    fun install(): TestResultPublisher {
      val instrumentation = InstrumentationRegistry.getInstrumentation()
      val orchestratorListener = if (instrumentation is AndroidJUnitRunner) {
        AndroidJUnitRunner::class.java.getDeclaredField("orchestratorListener")
          .run {
            isAccessible = true
            get(instrumentation) as OrchestratedInstrumentationListener?
          }
      } else null
      return if (orchestratorListener != null) {
        SharkLog.d { "Android Test Orchestrator detected, failures will be sent via binder callback" }
        OrchestratorTestResultPublisher(orchestratorListener)
      } else {
        SharkLog.d { "Failures will be sent via Instrumentation.sendStatus()" }
        InstrumentationTestResultPublisher()
      }
    }
  }
}

package leakcanary.internal

import android.app.Activity
import android.app.Application
import android.app.Application.ActivityLifecycleCallbacks
import leakcanary.ProcessInfo
import leakcanary.internal.friendly.mainHandler
import leakcanary.internal.friendly.noOpDelegate

/**
 * Tracks whether the app is in background, based on the app's importance.
 */
internal class BackgroundListener(
  private val processInfo: ProcessInfo,
  private val callback: (Boolean) -> Unit
) : ActivityLifecycleCallbacks by noOpDelegate() {

  private val checkAppInBackground: Runnable = object : Runnable {
    override fun run() {
      val appInBackgroundNow = processInfo.isImportanceBackground
      updateBackgroundState(appInBackgroundNow)
      if (!appInBackgroundNow) {
        mainHandler.removeCallbacks(this)
        mainHandler.postDelayed(this, BACKGROUND_REPEAT_DELAY_MS)
      }
    }
  }

  private fun updateBackgroundState(appInBackgroundNow: Boolean) {
    if (appInBackground != appInBackgroundNow) {
      appInBackground = appInBackgroundNow
      callback.invoke(appInBackgroundNow)
    }
  }

  private var appInBackground = false

  fun install(application: Application) {
    application.registerActivityLifecycleCallbacks(this)
    updateBackgroundState(appInBackgroundNow = false)
    checkAppInBackground.run()
  }

  fun uninstall(application: Application) {
    application.unregisterActivityLifecycleCallbacks(this)
    updateBackgroundState(appInBackgroundNow = false)
  }

  override fun onActivityPaused(activity: Activity) {
    mainHandler.removeCallbacks(checkAppInBackground)
    mainHandler.postDelayed(checkAppInBackground, BACKGROUND_DELAY_MS)
  }

  override fun onActivityResumed(activity: Activity) {
    updateBackgroundState(appInBackgroundNow = false)
    mainHandler.removeCallbacks(checkAppInBackground)
  }

  companion object {
    private const val BACKGROUND_DELAY_MS = 1000L
    private const val BACKGROUND_REPEAT_DELAY_MS = 5000L
  }
}

package leakcanary.internal

import android.os.Debug
import android.os.SystemClock
import leakcanary.HeapAnalysisConfig
import leakcanary.HeapAnalysisInterceptor
import leakcanary.HeapAnalysisJob
import leakcanary.HeapAnalysisJob.Result
import leakcanary.HeapAnalysisJob.Result.Canceled
import leakcanary.HeapAnalysisJob.Result.Done
import leakcanary.JobContext
import okio.buffer
import okio.sink
import shark.CloseableHeapGraph
import shark.ConstantMemoryMetricsDualSourceProvider
import shark.DualSourceProvider
import shark.HeapAnalysis
import shark.HeapAnalysisException
import shark.HeapAnalysisFailure
import shark.HeapAnalysisSuccess
import shark.HeapAnalyzer
import shark.HprofHeapGraph
import shark.HprofHeapGraph.Companion.openHeapGraph
import shark.HprofPrimitiveArrayStripper
import shark.OnAnalysisProgressListener
import shark.RandomAccessSource
import shark.SharkLog
import shark.StreamingSourceProvider
import java.io.File
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference

internal class RealHeapAnalysisJob(
  private val heapDumpDirectoryProvider: () -> File,
  private val config: HeapAnalysisConfig,
  private val interceptors: List<HeapAnalysisInterceptor>,
  override val context: JobContext
) : HeapAnalysisJob, HeapAnalysisInterceptor.Chain {

  private val heapDumpDirectory by lazy {
    heapDumpDirectoryProvider()
  }

  private val _canceled = AtomicReference<Canceled?>()

  private val _executed = AtomicBoolean(false)

  private lateinit var executionThread: Thread

  private var interceptorIndex = 0

  private var analysisStep: OnAnalysisProgressListener.Step? = null

  override val executed
    get() = _executed.get()

  override val canceled
    get() = _canceled.get() != null

  override val job: HeapAnalysisJob
    get() = this

  override fun execute(): Result {
    check(_executed.compareAndSet(false, true)) { "HeapAnalysisJob can only be executed once" }
    SharkLog.d { "Starting heap analysis job" }
    executionThread = Thread.currentThread()
    return proceed()
  }

  override fun cancel(cancelReason: String) {
    // If cancel is called several times, we use the first cancel reason.
    _canceled.compareAndSet(null, Canceled(cancelReason))
  }

  override fun proceed(): Result {
    check(Thread.currentThread() == executionThread) {
      "Interceptor.Chain.proceed() called from unexpected thread ${Thread.currentThread()} instead of $executionThread"
    }
    check(interceptorIndex <= interceptors.size) {
      "Interceptor.Chain.proceed() should be called max once per interceptor"
    }
    _canceled.get()?.let {
      interceptorIndex = interceptors.size + 1
      return it
    }
    if (interceptorIndex < interceptors.size) {
      val currentInterceptor = interceptors[interceptorIndex]
      interceptorIndex++
      return currentInterceptor.intercept(this)
    } else {
      interceptorIndex++
      val result = dumpAndAnalyzeHeap()
      val analysis = result.analysis
      analysis.heapDumpFile.delete()
      if (analysis is HeapAnalysisFailure) {
        val cause = analysis.exception.cause
        if (cause is StopAnalysis) {
          return _canceled.get()!!.run {
            copy(cancelReason = "$cancelReason (stopped at ${cause.step})")
          }
        }
      }
      return result
    }
  }

  private fun dumpAndAnalyzeHeap(): Done {
    val filesDir = heapDumpDirectory
    filesDir.mkdirs()
    val fileNameBase = "$HPROF_PREFIX${UUID.randomUUID()}"
    val sensitiveHeapDumpFile = File(filesDir, "$fileNameBase$HPROF_SUFFIX").apply {
      // Any call to System.exit(0) will run shutdown hooks that will attempt to remove this
      // file. Note that this is best effort, and won't delete if the VM is killed by the system.
      deleteOnExit()
    }

    val heapDumpStart = SystemClock.uptimeMillis()
    saveHeapDumpTime(heapDumpStart)

    var dumpDurationMillis = -1L
    var analysisDurationMillis = -1L
    var heapDumpFile = sensitiveHeapDumpFile

    try {
      runGc()
      dumpHeap(sensitiveHeapDumpFile)
      dumpDurationMillis = SystemClock.uptimeMillis() - heapDumpStart

      val stripDurationMillis =
        if (config.stripHeapDump) {
          leakcanary.internal.friendly.measureDurationMillis {
            val strippedHeapDumpFile = File(filesDir, "$fileNameBase-stripped$HPROF_SUFFIX").apply {
              deleteOnExit()
            }
            heapDumpFile = strippedHeapDumpFile
            try {
              stripHeapDump(sensitiveHeapDumpFile, strippedHeapDumpFile)
            } finally {
              sensitiveHeapDumpFile.delete()
            }
          }
        } else null

      return analyzeHeapWithStats(heapDumpFile).let { (heapAnalysis, stats) ->
        when (heapAnalysis) {
          is HeapAnalysisSuccess -> {
            val metadata = heapAnalysis.metadata.toMutableMap()
            metadata["Stats"] = stats
            if (config.stripHeapDump) {
              metadata["Hprof stripping duration"] = "$stripDurationMillis ms"
            }
            Done(
              heapAnalysis.copy(
                dumpDurationMillis = dumpDurationMillis,
                metadata = metadata
              ), stripDurationMillis
            )
          }
          is HeapAnalysisFailure -> Done(
            heapAnalysis.copy(
              dumpDurationMillis = dumpDurationMillis,
              analysisDurationMillis = (SystemClock.uptimeMillis() - heapDumpStart) - dumpDurationMillis
            ), stripDurationMillis
          )
        }
      }
    } catch (throwable: Throwable) {
      if (dumpDurationMillis == -1L) {
        dumpDurationMillis = SystemClock.uptimeMillis() - heapDumpStart
      }
      if (analysisDurationMillis == -1L) {
        analysisDurationMillis = (SystemClock.uptimeMillis() - heapDumpStart) - dumpDurationMillis
      }
      return Done(
        HeapAnalysisFailure(
          heapDumpFile = heapDumpFile,
          createdAtTimeMillis = System.currentTimeMillis(),
          dumpDurationMillis = dumpDurationMillis,
          analysisDurationMillis = analysisDurationMillis,
          exception = HeapAnalysisException(throwable)
        )
      )
    }
  }

  private fun runGc() {
    // Code taken from AOSP FinalizationTest:
    // https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
    // java/lang/ref/FinalizationTester.java
    // System.gc() does not garbage collect every time. Runtime.gc() is
    // more likely to perform a gc.
    Runtime.getRuntime()
      .gc()
    enqueueReferences()
    System.runFinalization()
  }

  private fun enqueueReferences() {
    // Hack. We don't have a programmatic way to wait for the reference queue daemon to move
    // references to the appropriate queues.
    try {
      Thread.sleep(100)
    } catch (e: InterruptedException) {
      throw AssertionError()
    }
  }

  private fun saveHeapDumpTime(heapDumpUptimeMillis: Long) {
    try {
      Class.forName("leakcanary.KeyedWeakReference")
        .getDeclaredField("heapDumpUptimeMillis")
        .apply { isAccessible = true }
        .set(null, heapDumpUptimeMillis)
    } catch (ignored: Throwable) {
      SharkLog.d(ignored) { "KeyedWeakReference.heapDumpUptimeMillis not updated" }
    }
  }

  private fun dumpHeap(heapDumpFile: File) {
    Debug.dumpHprofData(heapDumpFile.absolutePath)

    check(heapDumpFile.exists()) {
      "File does not exist after dump"
    }

    check(heapDumpFile.length() > 0L) {
      "File has length ${heapDumpFile.length()} after dump"
    }
  }

  private fun stripHeapDump(
    sourceHeapDumpFile: File,
    strippedHeapDumpFile: File
  ) {
    val sensitiveSourceProvider =
      StoppableFileSourceProvider(sourceHeapDumpFile) {
        checkStopAnalysis("stripping heap dump")
      }

    var openCalls = 0
    val deletingFileSourceProvider = StreamingSourceProvider {
      openCalls++
      sensitiveSourceProvider.openStreamingSource().apply {
        if (openCalls == 2) {
          // Using the Unix trick of deleting the file as soon as all readers have opened it.
          // No new readers/writers will be able to access the file, but all existing
          // ones will still have access until the last one closes the file.
          SharkLog.d { "Deleting $sourceHeapDumpFile eagerly" }
          sourceHeapDumpFile.delete()
        }
      }
    }

    val strippedHprofSink = strippedHeapDumpFile.outputStream().sink().buffer()
    val stripper = HprofPrimitiveArrayStripper()

    stripper.stripPrimitiveArrays(deletingFileSourceProvider, strippedHprofSink)
  }

  private fun analyzeHeapWithStats(heapDumpFile: File): Pair<HeapAnalysis, String> {
    val fileLength = heapDumpFile.length()
    val analysisSourceProvider = ConstantMemoryMetricsDualSourceProvider(
      StoppableFileSourceProvider(heapDumpFile) {
        checkStopAnalysis(analysisStep?.name ?: "Reading heap dump")
      })

    val deletingFileSourceProvider = object : DualSourceProvider {
      override fun openStreamingSource() = analysisSourceProvider.openStreamingSource()

      override fun openRandomAccessSource(): RandomAccessSource {
        SharkLog.d { "Deleting $heapDumpFile eagerly" }
        return analysisSourceProvider.openRandomAccessSource().apply {
          // Using the Unix trick of deleting the file as soon as all readers have opened it.
          // No new readers/writers will be able to access the file, but all existing
          // ones will still have access until the last one closes the file.
          heapDumpFile.delete()
        }
      }
    }

    return deletingFileSourceProvider.openHeapGraph().use { graph ->
      val heapAnalysis = analyzeHeap(heapDumpFile, graph)
      val lruCacheStats = (graph as HprofHeapGraph).lruCacheStats()
      val randomAccessStats =
        "RandomAccess[" +
          "bytes=${analysisSourceProvider.randomAccessByteReads}," +
          "reads=${analysisSourceProvider.randomAccessReadCount}," +
          "travel=${analysisSourceProvider.randomAccessByteTravel}," +
          "range=${analysisSourceProvider.byteTravelRange}," +
          "size=$fileLength" +
          "]"
      val stats = "$lruCacheStats $randomAccessStats"
      (heapAnalysis to stats)
    }
  }

  private fun analyzeHeap(
    analyzedHeapDumpFile: File,
    graph: CloseableHeapGraph
  ): HeapAnalysis {
    val stepListener = OnAnalysisProgressListener { step ->
      analysisStep = step
      checkStopAnalysis(step.name)
      SharkLog.d { "Analysis in progress, working on: ${step.name}" }
    }

    val heapAnalyzer = HeapAnalyzer(stepListener)
    return heapAnalyzer.analyze(
      heapDumpFile = analyzedHeapDumpFile,
      graph = graph,
      leakingObjectFinder = config.leakingObjectFinder,
      referenceMatchers = config.referenceMatchers,
      computeRetainedHeapSize = config.computeRetainedHeapSize,
      objectInspectors = config.objectInspectors,
      metadataExtractor = config.metadataExtractor
    )
  }

  private fun checkStopAnalysis(step: String) {
    if (_canceled.get() != null) {
      throw StopAnalysis(step)
    }
  }

  class StopAnalysis(val step: String) : Exception() {
    override fun fillInStackTrace(): Throwable {
      // Skip filling in stacktrace.
      return this
    }
  }

  companion object {
    const val HPROF_PREFIX = "heap-"
    const val HPROF_SUFFIX = ".hprof"
  }
}

package leakcanary.internal

import okio.Buffer
import okio.BufferedSource
import okio.Source
import okio.buffer
import okio.source
import shark.DualSourceProvider
import shark.RandomAccessSource
import java.io.File

internal class StoppableFileSourceProvider(
  private val file: File,
  private val throwIfStop: () -> Unit
) : DualSourceProvider {

  override fun openStreamingSource(): BufferedSource {
    val realSource = file.inputStream().source()
    return object : Source by realSource {
      override fun read(
        sink: Buffer,
        byteCount: Long
      ): Long {
        throwIfStop()
        return realSource.read(sink, byteCount)
      }
    }.buffer()
  }

  override fun openRandomAccessSource(): RandomAccessSource {
    val channel = file.inputStream().channel
    return object : RandomAccessSource {
      override fun read(
        sink: Buffer,
        position: Long,
        byteCount: Long
      ): Long {
        throwIfStop()
        return channel.transferTo(position, byteCount, sink)
      }

      override fun close() = channel.close()
    }
  }
}

package leakcanary.internal

import android.os.Handler
import android.os.Looper

internal val mainHandler by lazy { Handler(Looper.getMainLooper()) }

internal fun checkMainThread() {
  check(Looper.getMainLooper().thread === Thread.currentThread()) {
    "Should be called from the main thread, not ${Thread.currentThread()}"
  }
}

internal fun checkNotMainThread() {
  check(Looper.getMainLooper().thread !== Thread.currentThread()) {
    "Should not be called from the main thread"
  }
}

package leakcanary.internal

import java.lang.reflect.InvocationHandler
import java.lang.reflect.Proxy

internal inline fun <reified T : Any> noOpDelegate(): T {
  val javaClass = T::class.java
  return Proxy.newProxyInstance(
    javaClass.classLoader, arrayOf(javaClass), NO_OP_HANDLER
  ) as T
}

private val NO_OP_HANDLER = InvocationHandler { _, _, _ ->
  // no op
}

package leakcanary.internal

import android.os.SystemClock

/**
 * Executes the given [block] and returns elapsed time in milliseconds using [SystemClock.uptimeMillis]
 */
internal inline fun measureDurationMillis(block: () -> Unit): Long {
  val start = SystemClock.uptimeMillis()
  block()
  return SystemClock.uptimeMillis() - start
}

/*
 * Copyright (C) 2018 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.app.Activity
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import leakcanary.ReachabilityWatcher

internal class AndroidXFragmentDestroyWatcher(
  private val reachabilityWatcher: ReachabilityWatcher
) : (Activity) -> Unit {

  private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

    override fun onFragmentCreated(
      fm: FragmentManager,
      fragment: Fragment,
      savedInstanceState: Bundle?
    ) {
      ViewModelClearedWatcher.install(fragment, reachabilityWatcher)
    }

    override fun onFragmentViewDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      val view = fragment.view
      if (view != null) {
        reachabilityWatcher.expectWeaklyReachable(
          view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
          "(references to its views should be cleared to prevent leaks)"
        )
      }
    }

    override fun onFragmentDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      reachabilityWatcher.expectWeaklyReachable(
        fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
      )
    }
  }

  override fun invoke(activity: Activity) {
    if (activity is FragmentActivity) {
      val supportFragmentManager = activity.supportFragmentManager
      supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
      ViewModelClearedWatcher.install(activity, reachabilityWatcher)
    }
  }
}

package leakcanary.internal

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.Factory
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import leakcanary.ReachabilityWatcher
import leakcanary.internal.ViewModelClearedWatcher.Companion.install

/**
 * [AndroidXFragmentDestroyWatcher] calls [install] to add a spy [ViewModel] in every
 * [ViewModelStoreOwner] instance (i.e. FragmentActivity and Fragment). [ViewModelClearedWatcher]
 * holds on to the map of [ViewModel]s backing its store. When [ViewModelClearedWatcher] receives
 * the [onCleared] callback, it adds each live [ViewModel] from the store to the [ReachabilityWatcher].
 */
internal class ViewModelClearedWatcher(
  storeOwner: ViewModelStoreOwner,
  private val reachabilityWatcher: ReachabilityWatcher
) : ViewModel() {

  private val viewModelMap: Map<String, ViewModel>?

  init {
    // We could call ViewModelStore#keys with a package spy in androidx.lifecycle instead,
    // however that was added in 2.1.0 and we support AndroidX first stable release. viewmodel-2.0.0
    // does not have ViewModelStore#keys. All versions currently have the mMap field.
    viewModelMap = try {
      val mMapField = ViewModelStore::class.java.getDeclaredField("mMap")
      mMapField.isAccessible = true
      @Suppress("UNCHECKED_CAST")
      mMapField[storeOwner.viewModelStore] as Map<String, ViewModel>
    } catch (ignored: Exception) {
      null
    }
  }

  override fun onCleared() {
    viewModelMap?.values?.forEach { viewModel ->
      reachabilityWatcher.expectWeaklyReachable(
        viewModel, "${viewModel::class.java.name} received ViewModel#onCleared() callback"
      )
    }
  }

  companion object {
    fun install(
      storeOwner: ViewModelStoreOwner,
      reachabilityWatcher: ReachabilityWatcher
    ) {
      val provider = ViewModelProvider(storeOwner, object : Factory {
        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel?> create(modelClass: Class<T>): T =
          ViewModelClearedWatcher(storeOwner, reachabilityWatcher) as T
      })
      provider.get(ViewModelClearedWatcher::class.java)
    }
  }
}

/*
 * Copyright (C) 2019 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.app.Activity
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentActivity
import android.support.v4.app.FragmentManager
import leakcanary.ReachabilityWatcher

internal class AndroidSupportFragmentDestroyWatcher(
  private val reachabilityWatcher: ReachabilityWatcher
) : (Activity) -> Unit {

  private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

    override fun onFragmentViewDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      val view = fragment.view
      if (view != null) {
        reachabilityWatcher.expectWeaklyReachable(
          view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
          "(references to its views should be cleared to prevent leaks)"
        )
      }
    }

    override fun onFragmentDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      reachabilityWatcher.expectWeaklyReachable(
        fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
      )
    }
  }

  override fun invoke(activity: Activity) {
    if (activity is FragmentActivity) {
      val supportFragmentManager = activity.supportFragmentManager
      supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
    }
  }
}

/*
 * Copyright (C) 2018 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
@file:Suppress("DEPRECATION")

package leakcanary.internal

import android.annotation.SuppressLint
import android.app.Activity
import android.app.Fragment
import android.app.FragmentManager
import leakcanary.ReachabilityWatcher

@SuppressLint("NewApi")
internal class AndroidOFragmentDestroyWatcher(
  private val reachabilityWatcher: ReachabilityWatcher
) : (Activity) -> Unit {
  private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

    override fun onFragmentViewDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      val view = fragment.view
      if (view != null) {
        reachabilityWatcher.expectWeaklyReachable(
          view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
          "(references to its views should be cleared to prevent leaks)"
        )
      }
    }

    override fun onFragmentDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      reachabilityWatcher.expectWeaklyReachable(
        fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
      )
    }
  }

  override fun invoke(activity: Activity) {
    val fragmentManager = activity.fragmentManager
    fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
  }
}

package leakcanary.internal

import android.app.Application
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import leakcanary.AppWatcher

/**
 * Content providers are loaded before the application class is created. [AppWatcherInstaller] is
 * used to install [leakcanary.AppWatcher] on application start.
 */
internal sealed class AppWatcherInstaller : ContentProvider() {

  /**
   * [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
   */
  internal class MainProcess : AppWatcherInstaller()

  /**
   * When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
   * [LeakCanaryProcess] automatically sets up the LeakCanary code
   */
  internal class LeakCanaryProcess : AppWatcherInstaller()

  override fun onCreate(): Boolean {
    val application = context!!.applicationContext as Application
    AppWatcher.manualInstall(application)
    return true
  }

  override fun query(
    uri: Uri,
    strings: Array<String>?,
    s: String?,
    strings1: Array<String>?,
    s1: String?
  ): Cursor? {
    return null
  }

  override fun getType(uri: Uri): String? {
    return null
  }

  override fun insert(
    uri: Uri,
    contentValues: ContentValues?
  ): Uri? {
    return null
  }

  override fun delete(
    uri: Uri,
    s: String?,
    strings: Array<String>?
  ): Int {
    return 0
  }

  override fun update(
    uri: Uri,
    contentValues: ContentValues?,
    s: String?,
    strings: Array<String>?
  ): Int {
    return 0
  }
}

package leakcanary.internal

import android.app.Application
import android.content.pm.ApplicationInfo

internal val Application.isDebuggableBuild: Boolean
  get() = (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0

package leakcanary.internal

import android.app.Application
import leakcanary.OnObjectRetainedListener

internal object LeakCanaryDelegate {

  @Suppress("UNCHECKED_CAST")
  val loadLeakCanary by lazy {
    try {
      val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary")
      leakCanaryListener.getDeclaredField("INSTANCE")
        .get(null) as (Application) -> Unit
    } catch (ignored: Throwable) {
      NoLeakCanary
    }
  }

  object NoLeakCanary : (Application) -> Unit, OnObjectRetainedListener {
    override fun invoke(application: Application) {
    }

    override fun onObjectRetained() {
    }
  }
}

package leakcanary.internal

import android.app.Activity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager

private val hasAndroidXFragmentActivity: Boolean by lazy {
  try {
    Class.forName("androidx.fragment.app.FragmentActivity")
    true
  } catch (ignored: Throwable) {
    false
  }
}

internal fun Activity.onAndroidXFragmentViewDestroyed(block: () -> Unit) {
  if (!hasAndroidXFragmentActivity) {
    return
  }
  if (this is FragmentActivity) {
    supportFragmentManager.registerFragmentLifecycleCallbacks(
      object : FragmentManager.FragmentLifecycleCallbacks() {
        override fun onFragmentViewDestroyed(
          fm: FragmentManager,
          fragment: Fragment
        ) {
          block()
        }
      }, true
    )
  }
}

package leakcanary.internal

import android.app.Application
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import leakcanary.AndroidLeakFixes

/**
 * Content providers are loaded before the application class is created. [PlumberInstaller] is
 * used to install [leakcanary.AndroidLeakFixes] fixes on application start.
 */
internal class PlumberInstaller : ContentProvider() {

  override fun onCreate(): Boolean {
    val application = context!!.applicationContext as Application
    AndroidLeakFixes.applyFixes(application)
    return true
  }

  override fun query(
    uri: Uri,
    strings: Array<String>?,
    s: String?,
    strings1: Array<String>?,
    s1: String?
  ): Cursor? {
    return null
  }

  override fun getType(uri: Uri): String? {
    return null
  }

  override fun insert(
    uri: Uri,
    contentValues: ContentValues?
  ): Uri? {
    return null
  }

  override fun delete(
    uri: Uri,
    s: String?,
    strings: Array<String>?
  ): Int {
    return 0
  }

  override fun update(
    uri: Uri,
    contentValues: ContentValues?,
    s: String?,
    strings: Array<String>?
  ): Int {
    return 0
  }
}

package leakcanary.internal

import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.ContextWrapper
import android.os.Looper
import android.os.MessageQueue.IdleHandler
import android.view.View
import android.view.View.OnAttachStateChangeListener
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener
import android.view.inputmethod.InputMethodManager
import shark.SharkLog
import java.lang.reflect.Field
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method

internal class ReferenceCleaner(
  private val inputMethodManager: InputMethodManager,
  private val mHField: Field,
  private val mServedViewField: Field,
  private val finishInputLockedMethod: Method
) : IdleHandler,
  OnAttachStateChangeListener,
  OnGlobalFocusChangeListener {
  override fun onGlobalFocusChanged(
    oldFocus: View?,
    newFocus: View?
  ) {
    if (newFocus == null) {
      return
    }
    oldFocus?.removeOnAttachStateChangeListener(this)
    Looper.myQueue()
      .removeIdleHandler(this)
    newFocus.addOnAttachStateChangeListener(this)
  }

  override fun onViewAttachedToWindow(v: View) {}
  override fun onViewDetachedFromWindow(v: View) {
    v.removeOnAttachStateChangeListener(this)
    Looper.myQueue()
      .removeIdleHandler(this)
    Looper.myQueue()
      .addIdleHandler(this)
  }

  override fun queueIdle(): Boolean {
    clearInputMethodManagerLeak()
    return false
  }

  private fun clearInputMethodManagerLeak() {
    try {
      val lock = mHField[inputMethodManager]
      if (lock == null) {
        SharkLog.d { "InputMethodManager.mH was null, could not fix leak." }
        return
      }
      // This is highly dependent on the InputMethodManager implementation.
      synchronized(lock) {
        val servedView =
          mServedViewField[inputMethodManager] as View?
        if (servedView != null) {
          val servedViewAttached =
            servedView.windowVisibility != View.GONE
          if (servedViewAttached) {
            // The view held by the IMM was replaced without a global focus change. Let's make
            // sure we get notified when that view detaches.
            // Avoid double registration.
            servedView.removeOnAttachStateChangeListener(this)
            servedView.addOnAttachStateChangeListener(this)
          } else { // servedView is not attached. InputMethodManager is being stupid!
            val activity = extractActivity(servedView.context)
            if (activity == null || activity.window == null) {
              // Unlikely case. Let's finish the input anyways.
              finishInputLockedMethod.invoke(inputMethodManager)
            } else {
              val decorView = activity.window
                .peekDecorView()
              val windowAttached =
                decorView.windowVisibility != View.GONE
              // If the window is attached, we do nothing. The IMM is leaking a detached view
              // hierarchy, but we haven't found a way to clear the reference without breaking
              // the IMM behavior.
              if (!windowAttached) {
                finishInputLockedMethod.invoke(inputMethodManager)
              }
            }
          }
        }
      }
    } catch (ignored: Throwable) {
      SharkLog.d(ignored) { "Could not fix leak" }
    }
  }

  private fun extractActivity(sourceContext: Context): Activity? {
    var context = sourceContext
    while (true) {
      context = when (context) {
        is Application -> {
          return null
        }
        is Activity -> {
          return context
        }
        is ContextWrapper -> {
          val baseContext =
            context.baseContext
          // Prevent Stack Overflow.
          if (baseContext === context) {
            return null
          }
          baseContext
        }
        else -> {
          return null
        }
      }
    }
  }
}

package shark.internal

/**
 * Provides read access to a sub part of a larger array.
 */
internal class ByteSubArray(
  private val array: ByteArray,
  private val rangeStart: Int,
  size: Int,
  private val longIdentifiers: Boolean
) {

  private val endInclusive = size - 1

  private var currentIndex = 0

  fun readByte(): Byte {
    val index = currentIndex
    currentIndex++
    require(index in 0..endInclusive) {
      "Index $index should be between 0 and $endInclusive"
    }
    return array[rangeStart + index]
  }

  fun readId(): Long {
    return if (longIdentifiers) {
      readLong()
    } else {
      readInt().toLong()
    }
  }

  fun readInt(): Int {
    val index = currentIndex
    currentIndex += 4
    require(index >= 0 && index <= endInclusive - 3) {
      "Index $index should be between 0 and ${endInclusive - 3}"
    }
    return array.readInt(rangeStart + index)
  }

  fun readTruncatedLong(byteCount: Int): Long {
    val index = currentIndex
    currentIndex += byteCount
    require(index >= 0 && index <= endInclusive - (byteCount - 1)) {
      "Index $index should be between 0 and ${endInclusive - (byteCount - 1)}"
    }
    var pos = rangeStart + index
    val array = array

    var value = 0L

    var shift = (byteCount - 1) * 8
    while (shift >= 8) {
      value = value or (array[pos++] and 0xffL shl shift)
      shift -= 8
    }
    value = value or (array[pos] and 0xffL)
    return value
  }

  fun readLong(): Long {
    val index = currentIndex
    currentIndex += 8
    require(index >= 0 && index <= endInclusive - 7) {
      "Index $index should be between 0 and ${endInclusive - 7}"
    }
    return array.readLong(rangeStart + index)
  }
}

internal fun ByteArray.readShort(index: Int): Short {
  var pos = index
  val array = this
  val valueAsInt = array[pos++] and 0xff shl 8 or (array[pos] and 0xff)
  return valueAsInt.toShort()
}

internal fun ByteArray.readInt(index: Int): Int {
  var pos = index
  val array = this
  return (array[pos++] and 0xff shl 24
    or (array[pos++] and 0xff shl 16)
    or (array[pos++] and 0xff shl 8)
    or (array[pos] and 0xff))
}

internal fun ByteArray.readLong(index: Int): Long {
  var pos = index
  val array = this
  return (array[pos++] and 0xffL shl 56
    or (array[pos++] and 0xffL shl 48)
    or (array[pos++] and 0xffL shl 40)
    or (array[pos++] and 0xffL shl 32)
    or (array[pos++] and 0xffL shl 24)
    or (array[pos++] and 0xffL shl 16)
    or (array[pos++] and 0xffL shl 8)
    or (array[pos] and 0xffL))
}

@Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
private inline infix fun Byte.and(other: Long): Long = toLong() and other

@Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
private inline infix fun Byte.and(other: Int): Int = toInt() and other

package shark.internal

import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.FieldRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.StaticFieldRecord
import shark.PrimitiveType
import shark.PrimitiveType.BOOLEAN
import shark.PrimitiveType.BYTE
import shark.PrimitiveType.CHAR
import shark.PrimitiveType.DOUBLE
import shark.PrimitiveType.FLOAT
import shark.PrimitiveType.INT
import shark.PrimitiveType.LONG
import shark.PrimitiveType.SHORT
import shark.ValueHolder
import shark.ValueHolder.BooleanHolder
import shark.ValueHolder.ByteHolder
import shark.ValueHolder.CharHolder
import shark.ValueHolder.DoubleHolder
import shark.ValueHolder.FloatHolder
import shark.ValueHolder.IntHolder
import shark.ValueHolder.LongHolder
import shark.ValueHolder.ReferenceHolder
import shark.ValueHolder.ShortHolder
import shark.internal.IndexedObject.IndexedClass

internal class ClassFieldsReader(
  private val identifierByteSize: Int,
  private val classFieldBytes: ByteArray
) {

  fun classDumpStaticFields(indexedClass: IndexedClass): List<StaticFieldRecord> {
    return read(initialPosition = indexedClass.fieldsIndex) {
      val staticFieldCount = readUnsignedShort()
      val staticFields = ArrayList<StaticFieldRecord>(staticFieldCount)
      for (i in 0 until staticFieldCount) {
        val nameStringId = readId()
        val type = readUnsignedByte()
        val value = readValue(type)
        staticFields.add(
          StaticFieldRecord(
            nameStringId = nameStringId,
            type = type,
            value = value
          )
        )
      }
      staticFields
    }
  }

  fun classDumpFields(indexedClass: IndexedClass): List<FieldRecord> {
    return read(initialPosition = indexedClass.fieldsIndex) {
      skipStaticFields()

      val fieldCount = readUnsignedShort()
      val fields = ArrayList<FieldRecord>(fieldCount)
      for (i in 0 until fieldCount) {
        fields.add(FieldRecord(nameStringId = readId(), type = readUnsignedByte()))
      }
      fields
    }
  }

  fun classDumpHasReferenceFields(indexedClass: IndexedClass): Boolean {
    return read(initialPosition = indexedClass.fieldsIndex) {
      skipStaticFields()
      val fieldCount = readUnsignedShort()
      for (i in 0 until fieldCount) {
        position += identifierByteSize
        val type = readUnsignedByte()
        if (type == PrimitiveType.REFERENCE_HPROF_TYPE) {
          return@read true
        }
      }
      return@read false
    }
  }

  private val readInFlightThreadLocal = object : ThreadLocal<ReadInFlight>() {
    override fun initialValue() = ReadInFlight()
  }

  private fun <R> read(
    initialPosition: Int,
    block: ReadInFlight.() -> R
  ): R {
    val readInFlight = readInFlightThreadLocal.get()
    readInFlight.position = initialPosition
    return readInFlight.run(block)
  }

  private inner class ReadInFlight {
    var position = 0

    fun skipStaticFields() {
      val staticFieldCount = readUnsignedShort()
      for (i in 0 until staticFieldCount) {
        position += identifierByteSize
        val type = readUnsignedByte()
        position += if (type == PrimitiveType.REFERENCE_HPROF_TYPE) {
          identifierByteSize
        } else {
          PrimitiveType.byteSizeByHprofType.getValue(type)
        }
      }
    }

    fun readValue(type: Int): ValueHolder {
      return when (type) {
        PrimitiveType.REFERENCE_HPROF_TYPE -> ReferenceHolder(readId())
        BOOLEAN_TYPE -> BooleanHolder(readBoolean())
        CHAR_TYPE -> CharHolder(readChar())
        FLOAT_TYPE -> FloatHolder(readFloat())
        DOUBLE_TYPE -> DoubleHolder(readDouble())
        BYTE_TYPE -> ByteHolder(readByte())
        SHORT_TYPE -> ShortHolder(readShort())
        INT_TYPE -> IntHolder(readInt())
        LONG_TYPE -> LongHolder(readLong())
        else -> throw IllegalStateException("Unknown type $type")
      }
    }

    fun readByte(): Byte {
      return classFieldBytes[position++]
    }

    fun readInt(): Int {
      return (classFieldBytes[position++].toInt() and 0xff shl 24) or
        (classFieldBytes[position++].toInt() and 0xff shl 16) or
        (classFieldBytes[position++].toInt() and 0xff shl 8) or
        (classFieldBytes[position++].toInt() and 0xff)
    }

    fun readLong(): Long {
      return (classFieldBytes[position++].toLong() and 0xff shl 56) or
        (classFieldBytes[position++].toLong() and 0xff shl 48) or
        (classFieldBytes[position++].toLong() and 0xff shl 40) or
        (classFieldBytes[position++].toLong() and 0xff shl 32) or
        (classFieldBytes[position++].toLong() and 0xff shl 24) or
        (classFieldBytes[position++].toLong() and 0xff shl 16) or
        (classFieldBytes[position++].toLong() and 0xff shl 8) or
        (classFieldBytes[position++].toLong() and 0xff)
    }

    fun readShort(): Short {
      return ((classFieldBytes[position++].toInt() and 0xff shl 8) or
        (classFieldBytes[position++].toInt() and 0xff)).toShort()
    }

    fun readUnsignedShort(): Int {
      return readShort().toInt() and 0xFFFF
    }

    fun readUnsignedByte(): Int {
      return readByte().toInt() and 0xFF
    }

    fun readId(): Long {
      // As long as we don't interpret IDs, reading signed values here is fine.
      return when (identifierByteSize) {
        1 -> readByte().toLong()
        2 -> readShort().toLong()
        4 -> readInt().toLong()
        8 -> readLong()
        else -> throw IllegalArgumentException("ID Length must be 1, 2, 4, or 8")
      }
    }

    fun readBoolean(): Boolean {
      return readByte()
        .toInt() != 0
    }

    fun readChar(): Char {
      return readShort().toChar()
    }

    fun readFloat(): Float {
      return Float.fromBits(readInt())
    }

    fun readDouble(): Double {
      return Double.fromBits(readLong())
    }
  }

  companion object {
    private val BOOLEAN_TYPE = BOOLEAN.hprofType
    private val CHAR_TYPE = CHAR.hprofType
    private val FLOAT_TYPE = FLOAT.hprofType
    private val DOUBLE_TYPE = DOUBLE.hprofType
    private val BYTE_TYPE = BYTE.hprofType
    private val SHORT_TYPE = SHORT.hprofType
    private val INT_TYPE = INT.hprofType
    private val LONG_TYPE = LONG.hprofType
  }
}

package shark.internal

import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.FieldRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.InstanceDumpRecord
import shark.PrimitiveType
import shark.PrimitiveType.BOOLEAN
import shark.PrimitiveType.BYTE
import shark.PrimitiveType.CHAR
import shark.PrimitiveType.DOUBLE
import shark.PrimitiveType.FLOAT
import shark.PrimitiveType.INT
import shark.PrimitiveType.LONG
import shark.PrimitiveType.SHORT
import shark.ValueHolder
import shark.ValueHolder.BooleanHolder
import shark.ValueHolder.ByteHolder
import shark.ValueHolder.CharHolder
import shark.ValueHolder.DoubleHolder
import shark.ValueHolder.FloatHolder
import shark.ValueHolder.IntHolder
import shark.ValueHolder.LongHolder
import shark.ValueHolder.ReferenceHolder
import shark.ValueHolder.ShortHolder

internal class FieldValuesReader(
  private val record: InstanceDumpRecord,
  private val identifierByteSize: Int
) {

  private var position = 0

  fun readValue(field: FieldRecord): ValueHolder {
    return when (field.type) {
      PrimitiveType.REFERENCE_HPROF_TYPE -> ReferenceHolder(readId())
      BOOLEAN_TYPE -> BooleanHolder(readBoolean())
      CHAR_TYPE -> CharHolder(readChar())
      FLOAT_TYPE -> FloatHolder(readFloat())
      DOUBLE_TYPE -> DoubleHolder(readDouble())
      BYTE_TYPE -> ByteHolder(readByte())
      SHORT_TYPE -> ShortHolder(readShort())
      INT_TYPE -> IntHolder(readInt())
      LONG_TYPE -> LongHolder(readLong())
      else -> throw IllegalStateException("Unknown type ${field.type}")
    }
  }

  private fun readId(): Long {
    // As long as we don't interpret IDs, reading signed values here is fine.
    return when (identifierByteSize) {
      1 -> readByte().toLong()
      2 -> readShort().toLong()
      4 -> readInt().toLong()
      8 -> readLong()
      else -> throw IllegalArgumentException("ID Length must be 1, 2, 4, or 8")
    }
  }

  private fun readBoolean(): Boolean {
    val value = record.fieldValues[position]
    position++
    return value != 0.toByte()
  }

  private fun readByte(): Byte {
    val value = record.fieldValues[position]
    position++
    return value
  }

  private fun readInt(): Int {
    val value = record.fieldValues.readInt(position)
    position += 4
    return value
  }

  private fun readShort(): Short {
    val value = record.fieldValues.readShort(position)
    position += 2
    return value
  }

  private fun readLong(): Long {
    val value = record.fieldValues.readLong(position)
    position += 8
    return value
  }

  private fun readFloat(): Float {
    return Float.fromBits(readInt())
  }

  private fun readDouble(): Double {
    return Double.fromBits(readLong())
  }

  private fun readChar(): Char {
    val string = String(record.fieldValues, position, 2, Charsets.UTF_16BE)
    position += 2
    return string[0]
  }

  companion object {
    private val BOOLEAN_TYPE = BOOLEAN.hprofType
    private val CHAR_TYPE = CHAR.hprofType
    private val FLOAT_TYPE = FLOAT.hprofType
    private val DOUBLE_TYPE = DOUBLE.hprofType
    private val BYTE_TYPE = BYTE.hprofType
    private val SHORT_TYPE = SHORT.hprofType
    private val INT_TYPE = INT.hprofType
    private val LONG_TYPE = LONG.hprofType
  }
}

package shark.internal

import shark.GcRoot
import shark.HprofHeader
import shark.HprofRecordReader
import shark.HprofRecordTag
import shark.HprofRecordTag.CLASS_DUMP
import shark.HprofRecordTag.INSTANCE_DUMP
import shark.HprofRecordTag.LOAD_CLASS
import shark.HprofRecordTag.OBJECT_ARRAY_DUMP
import shark.HprofRecordTag.PRIMITIVE_ARRAY_DUMP
import shark.HprofRecordTag.ROOT_DEBUGGER
import shark.HprofRecordTag.ROOT_FINALIZING
import shark.HprofRecordTag.ROOT_INTERNED_STRING
import shark.HprofRecordTag.ROOT_JAVA_FRAME
import shark.HprofRecordTag.ROOT_JNI_GLOBAL
import shark.HprofRecordTag.ROOT_JNI_LOCAL
import shark.HprofRecordTag.ROOT_JNI_MONITOR
import shark.HprofRecordTag.ROOT_MONITOR_USED
import shark.HprofRecordTag.ROOT_NATIVE_STACK
import shark.HprofRecordTag.ROOT_REFERENCE_CLEANUP
import shark.HprofRecordTag.ROOT_STICKY_CLASS
import shark.HprofRecordTag.ROOT_THREAD_BLOCK
import shark.HprofRecordTag.ROOT_THREAD_OBJECT
import shark.HprofRecordTag.ROOT_UNKNOWN
import shark.HprofRecordTag.ROOT_UNREACHABLE
import shark.HprofRecordTag.ROOT_VM_INTERNAL
import shark.HprofRecordTag.STRING_IN_UTF8
import shark.HprofVersion.ANDROID
import shark.OnHprofRecordTagListener
import shark.PrimitiveType
import shark.PrimitiveType.INT
import shark.ProguardMapping
import shark.StreamingHprofReader
import shark.ValueHolder
import shark.internal.IndexedObject.IndexedClass
import shark.internal.IndexedObject.IndexedInstance
import shark.internal.IndexedObject.IndexedObjectArray
import shark.internal.IndexedObject.IndexedPrimitiveArray
import shark.internal.hppc.IntObjectPair
import shark.internal.hppc.LongLongScatterMap
import shark.internal.hppc.LongObjectPair
import shark.internal.hppc.LongObjectScatterMap
import shark.internal.hppc.to
import java.util.EnumSet
import kotlin.math.max

/**
 * This class is not thread safe, should be used from a single thread.
 */
internal class HprofInMemoryIndex private constructor(
  private val positionSize: Int,
  private val hprofStringCache: LongObjectScatterMap<String>,
  private val classNames: LongLongScatterMap,
  private val classIndex: SortedBytesMap,
  private val instanceIndex: SortedBytesMap,
  private val objectArrayIndex: SortedBytesMap,
  private val primitiveArrayIndex: SortedBytesMap,
  private val gcRoots: List<GcRoot>,
  private val proguardMapping: ProguardMapping?,
  private val bytesForClassSize: Int,
  private val bytesForInstanceSize: Int,
  private val bytesForObjectArraySize: Int,
  private val bytesForPrimitiveArraySize: Int,
  private val useForwardSlashClassPackageSeparator: Boolean,
  val classFieldsReader: ClassFieldsReader,
  private val classFieldsIndexSize: Int
) {

  val classCount: Int
    get() = classIndex.size

  val instanceCount: Int
    get() = instanceIndex.size

  val objectArrayCount: Int
    get() = objectArrayIndex.size

  val primitiveArrayCount: Int
    get() = primitiveArrayIndex.size

  fun fieldName(
    classId: Long,
    id: Long
  ): String {
    val fieldNameString = hprofStringById(id)
    return proguardMapping?.let {
      val classNameStringId = classNames[classId]
      val classNameString = hprofStringById(classNameStringId)
      proguardMapping.deobfuscateFieldName(classNameString, fieldNameString)
    } ?: fieldNameString
  }

  fun className(classId: Long): String {
    // String, primitive types
    val classNameStringId = classNames[classId]
    val classNameString = hprofStringById(classNameStringId)
    return (proguardMapping?.deobfuscateClassName(classNameString) ?: classNameString).run {
      if (useForwardSlashClassPackageSeparator) {
        // JVM heap dumps use "/" for package separators (vs "." for Android heap dumps)
        replace('/', '.')
      } else this
    }
  }

  fun classId(className: String): Long? {
    val internalClassName = if (useForwardSlashClassPackageSeparator) {
      // JVM heap dumps use "/" for package separators (vs "." for Android heap dumps)
      className.replace('.', '/')
    } else className

    // Note: this performs two linear scans over arrays
    val hprofStringId = hprofStringCache.entrySequence()
      .firstOrNull { it.second == internalClassName }
      ?.first
    return hprofStringId?.let { stringId ->
      classNames.entrySequence()
        .firstOrNull { it.second == stringId }
        ?.first
    }
  }

  fun indexedClassSequence(): Sequence<LongObjectPair<IndexedClass>> {
    return classIndex.entrySequence()
      .map {
        val id = it.first
        val array = it.second
        id to array.readClass()
      }
  }

  fun indexedInstanceSequence(): Sequence<LongObjectPair<IndexedInstance>> {
    return instanceIndex.entrySequence()
      .map {
        val id = it.first
        val array = it.second
        val instance = IndexedInstance(
          position = array.readTruncatedLong(positionSize),
          classId = array.readId(),
          recordSize = array.readTruncatedLong(bytesForInstanceSize)
        )
        id to instance
      }
  }

  fun indexedObjectArraySequence(): Sequence<LongObjectPair<IndexedObjectArray>> {
    return objectArrayIndex.entrySequence()
      .map {
        val id = it.first
        val array = it.second
        val objectArray = IndexedObjectArray(
          position = array.readTruncatedLong(positionSize),
          arrayClassId = array.readId(),
          recordSize = array.readTruncatedLong(bytesForObjectArraySize)
        )
        id to objectArray
      }
  }

  fun indexedPrimitiveArraySequence(): Sequence<LongObjectPair<IndexedPrimitiveArray>> {
    return primitiveArrayIndex.entrySequence()
      .map {
        val id = it.first
        val array = it.second

        val primitiveArray = IndexedPrimitiveArray(
          position = array.readTruncatedLong(positionSize),
          primitiveType = PrimitiveType.values()[array.readByte()
            .toInt()],
          recordSize = array.readTruncatedLong(bytesForPrimitiveArraySize)
        )
        id to primitiveArray
      }
  }

  fun indexedObjectSequence(): Sequence<LongObjectPair<IndexedObject>> {
    return indexedClassSequence() +
      indexedInstanceSequence() +
      indexedObjectArraySequence() +
      indexedPrimitiveArraySequence()
  }

  fun gcRoots(): List<GcRoot> {
    return gcRoots
  }

  fun objectAtIndex(index: Int): LongObjectPair<IndexedObject> {
    require(index > 0)
    if (index < classIndex.size) {
      val objectId = classIndex.keyAt(index)
      val array = classIndex.getAtIndex(index)
      return objectId to array.readClass()
    }
    var shiftedIndex = index - classIndex.size
    if (shiftedIndex < instanceIndex.size) {
      val objectId = instanceIndex.keyAt(shiftedIndex)
      val array = instanceIndex.getAtIndex(shiftedIndex)
      return objectId to IndexedInstance(
        position = array.readTruncatedLong(positionSize),
        classId = array.readId(),
        recordSize = array.readTruncatedLong(bytesForInstanceSize)
      )
    }
    shiftedIndex -= instanceIndex.size
    if (shiftedIndex < objectArrayIndex.size) {
      val objectId = objectArrayIndex.keyAt(shiftedIndex)
      val array = objectArrayIndex.getAtIndex(shiftedIndex)
      return objectId to IndexedObjectArray(
        position = array.readTruncatedLong(positionSize),
        arrayClassId = array.readId(),
        recordSize = array.readTruncatedLong(bytesForObjectArraySize)
      )
    }
    shiftedIndex -= objectArrayIndex.size
    require(index < primitiveArrayIndex.size)
    val objectId = primitiveArrayIndex.keyAt(shiftedIndex)
    val array = primitiveArrayIndex.getAtIndex(shiftedIndex)
    return objectId to IndexedPrimitiveArray(
      position = array.readTruncatedLong(positionSize),
      primitiveType = PrimitiveType.values()[array.readByte()
        .toInt()],
      recordSize = array.readTruncatedLong(bytesForPrimitiveArraySize)
    )
  }

  @Suppress("ReturnCount")
  fun indexedObjectOrNull(objectId: Long): IntObjectPair<IndexedObject>? {
    var index = classIndex.indexOf(objectId)
    if (index >= 0) {
      val array = classIndex.getAtIndex(index)
      return index to array.readClass()
    }
    index = instanceIndex.indexOf(objectId)
    if (index >= 0) {
      val array = instanceIndex.getAtIndex(index)
      return classIndex.size + index to IndexedInstance(
        position = array.readTruncatedLong(positionSize),
        classId = array.readId(),
        recordSize = array.readTruncatedLong(bytesForInstanceSize)
      )
    }
    index = objectArrayIndex.indexOf(objectId)
    if (index >= 0) {
      val array = objectArrayIndex.getAtIndex(index)
      return classIndex.size + instanceIndex.size + index to IndexedObjectArray(
        position = array.readTruncatedLong(positionSize),
        arrayClassId = array.readId(),
        recordSize = array.readTruncatedLong(bytesForObjectArraySize)
      )
    }
    index = primitiveArrayIndex.indexOf(objectId)
    if (index >= 0) {
      val array = primitiveArrayIndex.getAtIndex(index)
      return classIndex.size + instanceIndex.size + index + primitiveArrayIndex.size to IndexedPrimitiveArray(
        position = array.readTruncatedLong(positionSize),
        primitiveType = PrimitiveType.values()[array.readByte()
          .toInt()],
        recordSize = array.readTruncatedLong(bytesForPrimitiveArraySize)
      )
    }
    return null
  }

  private fun ByteSubArray.readClass(): IndexedClass {
    val position = readTruncatedLong(positionSize)
    val superclassId = readId()
    val instanceSize = readInt()

    val recordSize = readTruncatedLong(bytesForClassSize)
    val fieldsIndex = readTruncatedLong(classFieldsIndexSize).toInt()

    return IndexedClass(
      position = position,
      superclassId = superclassId,
      instanceSize = instanceSize,
      recordSize = recordSize,
      fieldsIndex = fieldsIndex
    )
  }

  @Suppress("ReturnCount")
  fun objectIdIsIndexed(objectId: Long): Boolean {
    if (classIndex[objectId] != null) {
      return true
    }
    if (instanceIndex[objectId] != null) {
      return true
    }
    if (objectArrayIndex[objectId] != null) {
      return true
    }
    if (primitiveArrayIndex[objectId] != null) {
      return true
    }
    return false
  }

  private fun hprofStringById(id: Long): String {
    return hprofStringCache[id] ?: throw IllegalArgumentException("Hprof string $id not in cache")
  }

  private class Builder(
    longIdentifiers: Boolean,
    maxPosition: Long,
    classCount: Int,
    instanceCount: Int,
    objectArrayCount: Int,
    primitiveArrayCount: Int,
    val bytesForClassSize: Int,
    val bytesForInstanceSize: Int,
    val bytesForObjectArraySize: Int,
    val bytesForPrimitiveArraySize: Int,
    val classFieldsTotalBytes: Int
  ) : OnHprofRecordTagListener {

    private val identifierSize = if (longIdentifiers) 8 else 4
    private val positionSize = byteSizeForUnsigned(maxPosition)
    private val classFieldsIndexSize = byteSizeForUnsigned(classFieldsTotalBytes.toLong())

    /**
     * Map of string id to string
     * This currently keeps all the hprof strings that we could care about: class names,
     * static field names and instance fields names
     */
    // TODO Replacing with a radix trie reversed into a sparse array of long to trie leaf could save
    // memory. Can be stored as 3 arrays: array of keys, array of values which are indexes into
    // a large array of string bytes. Each "entry" consists of a size, the index of the previous
    // segment and then the segment content.

    private val hprofStringCache = LongObjectScatterMap<String>()

    /**
     * class id to string id
     */
    private val classNames = LongLongScatterMap(expectedElements = classCount)

    private val classFieldBytes = ByteArray(classFieldsTotalBytes)

    private var classFieldsIndex = 0

    private val classIndex = UnsortedByteEntries(
      bytesPerValue = positionSize + identifierSize + 4 + bytesForClassSize + classFieldsIndexSize,
      longIdentifiers = longIdentifiers,
      initialCapacity = classCount
    )
    private val instanceIndex = UnsortedByteEntries(
      bytesPerValue = positionSize + identifierSize + bytesForInstanceSize,
      longIdentifiers = longIdentifiers,
      initialCapacity = instanceCount
    )
    private val objectArrayIndex = UnsortedByteEntries(
      bytesPerValue = positionSize + identifierSize + bytesForObjectArraySize,
      longIdentifiers = longIdentifiers,
      initialCapacity = objectArrayCount
    )
    private val primitiveArrayIndex = UnsortedByteEntries(
      bytesPerValue = positionSize + 1 + bytesForPrimitiveArraySize,
      longIdentifiers = longIdentifiers,
      initialCapacity = primitiveArrayCount
    )

    private val gcRoots = mutableListOf<GcRoot>()

    private fun HprofRecordReader.copyToClassFields(byteCount: Int) {
      for (i in 1..byteCount) {
        classFieldBytes[classFieldsIndex++] = readByte()
      }
    }

    private fun lastClassFieldsShort() =
      ((classFieldBytes[classFieldsIndex - 2].toInt() and 0xff shl 8) or
        (classFieldBytes[classFieldsIndex - 1].toInt() and 0xff)).toShort()

    @Suppress("LongMethod")
    override fun onHprofRecord(
      tag: HprofRecordTag,
      length: Long,
      reader: HprofRecordReader
    ) {
      when (tag) {
        STRING_IN_UTF8 -> {
          hprofStringCache[reader.readId()] = reader.readUtf8(length - identifierSize)
        }
        LOAD_CLASS -> {
          // classSerialNumber
          reader.skip(INT.byteSize)
          val id = reader.readId()
          // stackTraceSerialNumber
          reader.skip(INT.byteSize)
          val classNameStringId = reader.readId()
          classNames[id] = classNameStringId
        }
        ROOT_UNKNOWN -> {
          reader.readUnknownGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_JNI_GLOBAL -> {
          reader.readJniGlobalGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_JNI_LOCAL -> {
          reader.readJniLocalGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_JAVA_FRAME -> {
          reader.readJavaFrameGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_NATIVE_STACK -> {
          reader.readNativeStackGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_STICKY_CLASS -> {
          reader.readStickyClassGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_THREAD_BLOCK -> {
          reader.readThreadBlockGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_MONITOR_USED -> {
          reader.readMonitorUsedGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_THREAD_OBJECT -> {
          reader.readThreadObjectGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_INTERNED_STRING -> {
          reader.readInternedStringGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_FINALIZING -> {
          reader.readFinalizingGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_DEBUGGER -> {
          reader.readDebuggerGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_REFERENCE_CLEANUP -> {
          reader.readReferenceCleanupGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_VM_INTERNAL -> {
          reader.readVmInternalGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_JNI_MONITOR -> {
          reader.readJniMonitorGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        ROOT_UNREACHABLE -> {
          reader.readUnreachableGcRootRecord().apply {
            if (id != ValueHolder.NULL_REFERENCE) {
              gcRoots += this
            }
          }
        }
        CLASS_DUMP -> {
          val bytesReadStart = reader.bytesRead
          val id = reader.readId()
          // stack trace serial number
          reader.skip(INT.byteSize)
          val superclassId = reader.readId()
          reader.skip(5 * identifierSize)

          // instance size (in bytes)
          // Useful to compute retained size
          val instanceSize = reader.readInt()

          reader.skipClassDumpConstantPool()

          val startPosition = classFieldsIndex

          val bytesReadFieldStart = reader.bytesRead

          reader.copyToClassFields(2)
          val staticFieldCount = lastClassFieldsShort().toInt() and 0xFFFF
          for (i in 0 until staticFieldCount) {
            reader.copyToClassFields(identifierSize)
            reader.copyToClassFields(1)
            val type = classFieldBytes[classFieldsIndex - 1].toInt() and 0xff
            if (type == PrimitiveType.REFERENCE_HPROF_TYPE) {
              reader.copyToClassFields(identifierSize)
            } else {
              reader.copyToClassFields(PrimitiveType.byteSizeByHprofType.getValue(type))
            }
          }

          reader.copyToClassFields(2)
          val fieldCount = lastClassFieldsShort().toInt() and 0xFFFF
          for (i in 0 until fieldCount) {
            reader.copyToClassFields(identifierSize)
            reader.copyToClassFields(1)
          }

          val fieldsSize = (reader.bytesRead - bytesReadFieldStart).toInt()
          val recordSize = reader.bytesRead - bytesReadStart

          classIndex.append(id)
            .apply {
              writeTruncatedLong(bytesReadStart, positionSize)
              writeId(superclassId)
              writeInt(instanceSize)
              writeTruncatedLong(recordSize, bytesForClassSize)
              writeTruncatedLong(startPosition.toLong(), classFieldsIndexSize)
            }
          require(startPosition + fieldsSize == classFieldsIndex) {
            "Expected $classFieldsIndex to have moved by $fieldsSize and be equal to ${startPosition + fieldsSize}"
          }
        }
        INSTANCE_DUMP -> {
          val bytesReadStart = reader.bytesRead
          val id = reader.readId()
          reader.skip(INT.byteSize)
          val classId = reader.readId()
          val remainingBytesInInstance = reader.readInt()
          reader.skip(remainingBytesInInstance)
          val recordSize = reader.bytesRead - bytesReadStart
          instanceIndex.append(id)
            .apply {
              writeTruncatedLong(bytesReadStart, positionSize)
              writeId(classId)
              writeTruncatedLong(recordSize, bytesForInstanceSize)
            }
        }
        OBJECT_ARRAY_DUMP -> {
          val bytesReadStart = reader.bytesRead
          val id = reader.readId()
          reader.skip(INT.byteSize)
          val size = reader.readInt()
          val arrayClassId = reader.readId()
          reader.skip(identifierSize * size)
          val recordSize = reader.bytesRead - bytesReadStart
          objectArrayIndex.append(id)
            .apply {
              writeTruncatedLong(bytesReadStart, positionSize)
              writeId(arrayClassId)
              writeTruncatedLong(recordSize, bytesForObjectArraySize)
            }
        }
        PRIMITIVE_ARRAY_DUMP -> {
          val bytesReadStart = reader.bytesRead
          val id = reader.readId()
          reader.skip(INT.byteSize)
          val size = reader.readInt()
          val type = PrimitiveType.primitiveTypeByHprofType.getValue(reader.readUnsignedByte())
          reader.skip(size * type.byteSize)
          val recordSize = reader.bytesRead - bytesReadStart
          primitiveArrayIndex.append(id)
            .apply {
              writeTruncatedLong(bytesReadStart, positionSize)
              writeByte(type.ordinal.toByte())
              writeTruncatedLong(recordSize, bytesForPrimitiveArraySize)
            }
        }
      }
    }

    fun buildIndex(
      proguardMapping: ProguardMapping?,
      hprofHeader: HprofHeader
    ): HprofInMemoryIndex {
      require(classFieldsIndex == classFieldBytes.size) {
        "Read $classFieldsIndex into fields bytes instead of expected ${classFieldBytes.size}"
      }

      val sortedInstanceIndex = instanceIndex.moveToSortedMap()
      val sortedObjectArrayIndex = objectArrayIndex.moveToSortedMap()
      val sortedPrimitiveArrayIndex = primitiveArrayIndex.moveToSortedMap()
      val sortedClassIndex = classIndex.moveToSortedMap()
      // Passing references to avoid copying the underlying data structures.
      return HprofInMemoryIndex(
        positionSize = positionSize,
        hprofStringCache = hprofStringCache,
        classNames = classNames,
        classIndex = sortedClassIndex,
        instanceIndex = sortedInstanceIndex,
        objectArrayIndex = sortedObjectArrayIndex,
        primitiveArrayIndex = sortedPrimitiveArrayIndex,
        gcRoots = gcRoots,
        proguardMapping = proguardMapping,
        bytesForClassSize = bytesForClassSize,
        bytesForInstanceSize = bytesForInstanceSize,
        bytesForObjectArraySize = bytesForObjectArraySize,
        bytesForPrimitiveArraySize = bytesForPrimitiveArraySize,
        useForwardSlashClassPackageSeparator = hprofHeader.version != ANDROID,
        classFieldsReader = ClassFieldsReader(identifierSize, classFieldBytes),
        classFieldsIndexSize = classFieldsIndexSize
      )
    }
  }

  companion object {

    private fun byteSizeForUnsigned(maxValue: Long): Int {
      var value = maxValue
      var byteCount = 0
      while (value != 0L) {
        value = value shr 8
        byteCount++
      }
      return byteCount
    }

    fun indexHprof(
      reader: StreamingHprofReader,
      hprofHeader: HprofHeader,
      proguardMapping: ProguardMapping?,
      indexedGcRootTags: Set<HprofRecordTag>
    ): HprofInMemoryIndex {

      // First pass to count and correctly size arrays once and for all.
      var maxClassSize = 0L
      var maxInstanceSize = 0L
      var maxObjectArraySize = 0L
      var maxPrimitiveArraySize = 0L
      var classCount = 0
      var instanceCount = 0
      var objectArrayCount = 0
      var primitiveArrayCount = 0
      var classFieldsTotalBytes = 0

      val bytesRead = reader.readRecords(
        EnumSet.of(CLASS_DUMP, INSTANCE_DUMP, OBJECT_ARRAY_DUMP, PRIMITIVE_ARRAY_DUMP),
        OnHprofRecordTagListener { tag, _, reader ->
          val bytesReadStart = reader.bytesRead
          when (tag) {
            CLASS_DUMP -> {
              classCount++
              reader.skipClassDumpHeader()
              val bytesReadStaticFieldStart = reader.bytesRead
              reader.skipClassDumpStaticFields()
              reader.skipClassDumpFields()
              maxClassSize = max(maxClassSize, reader.bytesRead - bytesReadStart)
              classFieldsTotalBytes += (reader.bytesRead - bytesReadStaticFieldStart).toInt()
            }
            INSTANCE_DUMP -> {
              instanceCount++
              reader.skipInstanceDumpRecord()
              maxInstanceSize = max(maxInstanceSize, reader.bytesRead - bytesReadStart)
            }
            OBJECT_ARRAY_DUMP -> {
              objectArrayCount++
              reader.skipObjectArrayDumpRecord()
              maxObjectArraySize = max(maxObjectArraySize, reader.bytesRead - bytesReadStart)
            }
            PRIMITIVE_ARRAY_DUMP -> {
              primitiveArrayCount++
              reader.skipPrimitiveArrayDumpRecord()
              maxPrimitiveArraySize = max(maxPrimitiveArraySize, reader.bytesRead - bytesReadStart)
            }
          }
        })

      val bytesForClassSize = byteSizeForUnsigned(maxClassSize)
      val bytesForInstanceSize = byteSizeForUnsigned(maxInstanceSize)
      val bytesForObjectArraySize = byteSizeForUnsigned(maxObjectArraySize)
      val bytesForPrimitiveArraySize = byteSizeForUnsigned(maxPrimitiveArraySize)

      val indexBuilderListener = Builder(
        longIdentifiers = hprofHeader.identifierByteSize == 8,
        maxPosition = bytesRead,
        classCount = classCount,
        instanceCount = instanceCount,
        objectArrayCount = objectArrayCount,
        primitiveArrayCount = primitiveArrayCount,
        bytesForClassSize = bytesForClassSize,
        bytesForInstanceSize = bytesForInstanceSize,
        bytesForObjectArraySize = bytesForObjectArraySize,
        bytesForPrimitiveArraySize = bytesForPrimitiveArraySize,
        classFieldsTotalBytes = classFieldsTotalBytes
      )

      val recordTypes = EnumSet.of(
        STRING_IN_UTF8,
        LOAD_CLASS,
        CLASS_DUMP,
        INSTANCE_DUMP,
        OBJECT_ARRAY_DUMP,
        PRIMITIVE_ARRAY_DUMP
      ) + HprofRecordTag.rootTags.intersect(indexedGcRootTags)

      reader.readRecords(recordTypes, indexBuilderListener)
      return indexBuilderListener.buildIndex(proguardMapping, hprofHeader)
    }
  }
}

package shark.internal

import shark.PrimitiveType

internal sealed class IndexedObject {
  abstract val position: Long
  abstract val recordSize: Long

  class IndexedClass(
    override val position: Long,
    val superclassId: Long,
    val instanceSize: Int,
    override val recordSize: Long,
    val fieldsIndex: Int
  ) : IndexedObject()

  class IndexedInstance(
    override val position: Long,
    val classId: Long,
    override val recordSize: Long
  ) : IndexedObject()

  class IndexedObjectArray(
    override val position: Long,
    val arrayClassId: Long,
    override val recordSize: Long
  ) : IndexedObject()

  class IndexedPrimitiveArray(
    override val position: Long,
    primitiveType: PrimitiveType,
    override val recordSize: Long
  ) : IndexedObject() {
    private val primitiveTypeOrdinal: Byte = primitiveType.ordinal.toByte()
    val primitiveType: PrimitiveType
      get() = PrimitiveType.values()[primitiveTypeOrdinal.toInt()]
  }
}

package shark.internal

import java.util.LinkedHashMap
import kotlin.collections.MutableMap.MutableEntry

/**
 * API is a simplified version of android.util.LruCache
 * Implementation is inspired from http://chriswu.me/blog/a-lru-cache-in-10-lines-of-java/
 */
internal class LruCache<K, V>(
  val maxSize: Int
) {
  private val cache: LinkedHashMap<K, V>

  val size
    get() = cache.size

  var putCount: Int = 0
    private set
  var evictionCount: Int = 0
    private set
  var hitCount: Int = 0
    private set
  var missCount: Int = 0
    private set

  init {
    require(maxSize > 0) {
      "maxSize=$maxSize <= 0"
    }
    this.cache = object : LinkedHashMap<K, V>(maxSize, 0.75f, true) {
      override fun removeEldestEntry(eldest: MutableEntry<K, V>?) = if (size > maxSize) {
        evictionCount++
        true
      } else {
        false
      }
    }
  }

  operator fun get(key: K?): V? {
    // get() moves the key to the front
    val value: V? = cache[key]
    return if (value != null) {
      hitCount++
      value
    } else {
      missCount++
      null
    }
  }

  fun put(
    key: K,
    value: V
  ): V? {
    putCount++
    return cache.put(key, value)
  }

  fun remove(key: K): V? {
    return cache.remove(key)
  }

  fun evictAll() {
    cache.clear()
  }

  override fun toString(): String {
    val accesses = hitCount + missCount
    val hitPercent = if (accesses != 0) 100 * hitCount / accesses else 0
    return String.format(
      "LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
      maxSize, hitCount, missCount, hitPercent
    )
  }
}

package shark.internal

import shark.internal.hppc.LongObjectPair
import shark.internal.hppc.to

/**
 * A read only map of `id` => `byte array` sorted by id, where `id` is a long if [longIdentifiers]
 * is true and an int otherwise. Each entry has a value byte array of size [bytesPerValue].
 *
 * Instances are created by [UnsortedByteEntries]
 *
 * [get] and [contains] perform a binary search to locate a specific entry by key.
 */
internal class SortedBytesMap(
  private val longIdentifiers: Boolean,
  private val bytesPerValue: Int,
  private val sortedEntries: ByteArray
) {
  private val bytesPerKey = if (longIdentifiers) 8 else 4
  private val bytesPerEntry = bytesPerKey + bytesPerValue

  val size = sortedEntries.size / bytesPerEntry

  operator fun get(key: Long): ByteSubArray? {
    val keyIndex = binarySearch(key)
    if (keyIndex < 0) {
      return null
    }
    return getAtIndex(keyIndex)
  }

  fun indexOf(key: Long): Int {
    return binarySearch(key)
  }

  fun getAtIndex(keyIndex: Int): ByteSubArray {
    val valueIndex = keyIndex * bytesPerEntry + bytesPerKey
    return ByteSubArray(sortedEntries, valueIndex, bytesPerValue, longIdentifiers)
  }

  operator fun contains(key: Long): Boolean {
    val keyIndex = binarySearch(key)
    return keyIndex >= 0
  }

  fun entrySequence(): Sequence<LongObjectPair<ByteSubArray>> {
    return (0 until size).asSequence()
      .map { keyIndex ->
        val valueIndex = keyIndex * bytesPerEntry + bytesPerKey
        keyAt(keyIndex) to ByteSubArray(sortedEntries, valueIndex, bytesPerValue, longIdentifiers)
      }
  }

  private fun binarySearch(
    key: Long
  ): Int {
    val startIndex = 0
    val endIndex = size
    var lo = startIndex
    var hi = endIndex - 1
    while (lo <= hi) {
      val mid = (lo + hi).ushr(1)
      val midVal = keyAt(mid)
      when {
        midVal < key -> lo = mid + 1
        midVal > key -> hi = mid - 1
        else -> return mid
      }
    }
    return lo.inv()
  }

  fun keyAt(index: Int): Long {
    val keyIndex = index * bytesPerEntry
    return if (longIdentifiers) {
      sortedEntries.readLong(keyIndex)
    } else {
      sortedEntries.readInt(keyIndex).toLong()
    }
  }
}

package shark.internal

import shark.internal.aosp.ByteArrayComparator
import shark.internal.aosp.ByteArrayTimSort

/**
 * Wraps a byte array of entries where each entry is an id followed by bytes for the value.
 * `id` is a long if [longIdentifiers] is true and an int otherwise. Each entry has [bytesPerValue]
 * value bytes. Entries are appended into the array via [append]. Once done, the backing array
 * is sorted and turned into a [SortedBytesMap] by calling [moveToSortedMap].
 */
internal class UnsortedByteEntries(
  private val bytesPerValue: Int,
  private val longIdentifiers: Boolean,
  private val initialCapacity: Int = 4,
  private val growthFactor: Double = 2.0
) {

  private val bytesPerEntry = bytesPerValue + if (longIdentifiers) 8 else 4

  private var entries: ByteArray? = null
  private val subArray = MutableByteSubArray()
  private var subArrayIndex = 0

  private var assigned: Int = 0
  private var currentCapacity = 0

  fun append(
    key: Long
  ): MutableByteSubArray {
    if (entries == null) {
      currentCapacity = initialCapacity
      entries = ByteArray(currentCapacity * bytesPerEntry)
    } else {
      if (currentCapacity == assigned) {
        val newCapacity = (currentCapacity * growthFactor).toInt()
        growEntries(newCapacity)
        currentCapacity = newCapacity
      }
    }
    assigned++
    subArrayIndex = 0
    subArray.writeId(key)
    return subArray
  }

  fun moveToSortedMap(): SortedBytesMap {
    if (assigned == 0) {
      return SortedBytesMap(longIdentifiers, bytesPerValue, ByteArray(0))
    }
    val entries = entries!!
    // Sort entries by keys, which are ids of 4 or 8 bytes.
    ByteArrayTimSort.sort(entries, 0, assigned, bytesPerEntry, object : ByteArrayComparator {
      override fun compare(
        entrySize: Int,
        o1Array: ByteArray,
        o1Index: Int,
        o2Array: ByteArray,
        o2Index: Int
      ): Int {
        return if (longIdentifiers) {
          readLong(o1Array, o1Index * entrySize)
            .compareTo(
              readLong(o2Array, o2Index * entrySize)
            )
        } else {
          readInt(o1Array, o1Index * entrySize)
            .compareTo(
              readInt(o2Array, o2Index * entrySize)
            )
        }
      }
    })
    val sortedEntries = if (entries.size > assigned * bytesPerEntry) {
      entries.copyOf(assigned * bytesPerEntry)
    } else entries
    this.entries = null
    assigned = 0
    return SortedBytesMap(
      longIdentifiers, bytesPerValue, sortedEntries
    )
  }

  private fun readInt(
    array: ByteArray,
    index: Int
  ): Int {
    var pos = index
    return (array[pos++] and 0xff shl 24
      or (array[pos++] and 0xff shl 16)
      or (array[pos++] and 0xff shl 8)
      or (array[pos] and 0xff))
  }

  @Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
  private inline infix fun Byte.and(other: Long): Long = toLong() and other

  @Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
  private inline infix fun Byte.and(other: Int): Int = toInt() and other

  private fun readLong(
    array: ByteArray,
    index: Int
  ): Long {
    var pos = index
    return (array[pos++] and 0xffL shl 56
      or (array[pos++] and 0xffL shl 48)
      or (array[pos++] and 0xffL shl 40)
      or (array[pos++] and 0xffL shl 32)
      or (array[pos++] and 0xffL shl 24)
      or (array[pos++] and 0xffL shl 16)
      or (array[pos++] and 0xffL shl 8)
      or (array[pos] and 0xffL))
  }

  private fun growEntries(newCapacity: Int) {
    val newEntries = ByteArray(newCapacity * bytesPerEntry)
    System.arraycopy(entries, 0, newEntries, 0, assigned * bytesPerEntry)
    entries = newEntries
  }

  internal inner class MutableByteSubArray {
    fun writeByte(value: Byte) {
      val index = subArrayIndex
      subArrayIndex++
      require(index in 0..bytesPerEntry) {
        "Index $index should be between 0 and $bytesPerEntry"
      }
      val valuesIndex = ((assigned - 1) * bytesPerEntry) + index
      entries!![valuesIndex] = value
    }

    fun writeId(value: Long) {
      if (longIdentifiers) {
        writeLong(value)
      } else {
        writeInt(value.toInt())
      }
    }

    fun writeInt(value: Int) {
      val index = subArrayIndex
      subArrayIndex += 4
      require(index >= 0 && index <= bytesPerEntry - 4) {
        "Index $index should be between 0 and ${bytesPerEntry - 4}"
      }
      var pos = ((assigned - 1) * bytesPerEntry) + index
      val values = entries!!
      values[pos++] = (value ushr 24 and 0xff).toByte()
      values[pos++] = (value ushr 16 and 0xff).toByte()
      values[pos++] = (value ushr 8 and 0xff).toByte()
      values[pos] = (value and 0xff).toByte()
    }

    fun writeTruncatedLong(
      value: Long,
      byteCount: Int
    ) {
      val index = subArrayIndex
      subArrayIndex += byteCount
      require(index >= 0 && index <= bytesPerEntry - byteCount) {
        "Index $index should be between 0 and ${bytesPerEntry - byteCount}"
      }
      var pos = ((assigned - 1) * bytesPerEntry) + index
      val values = entries!!

      var shift = (byteCount - 1) * 8
      while (shift >= 8) {
        values[pos++] = (value ushr shift and 0xffL).toByte()
        shift -= 8
      }
      values[pos] = (value and 0xffL).toByte()
    }

    fun writeLong(value: Long) {
      val index = subArrayIndex
      subArrayIndex += 8
      require(index >= 0 && index <= bytesPerEntry - 8) {
        "Index $index should be between 0 and ${bytesPerEntry - 8}"
      }
      var pos = ((assigned - 1) * bytesPerEntry) + index
      val values = entries!!
      values[pos++] = (value ushr 56 and 0xffL).toByte()
      values[pos++] = (value ushr 48 and 0xffL).toByte()
      values[pos++] = (value ushr 40 and 0xffL).toByte()
      values[pos++] = (value ushr 32 and 0xffL).toByte()
      values[pos++] = (value ushr 24 and 0xffL).toByte()
      values[pos++] = (value ushr 16 and 0xffL).toByte()
      values[pos++] = (value ushr 8 and 0xffL).toByte()
      values[pos] = (value and 0xffL).toByte()
    }
  }
}

package shark.internal

import shark.HeapGraph
import shark.HeapObject.HeapInstance

internal class AndroidNativeSizeMapper(private val graph: HeapGraph) {

  /**
   * Returns a map of Object id to native size as tracked by NativeAllocationRegistry$CleanerThunk
   */
  fun mapNativeSizes(): Map<Long, Int> {
    val nativeSizes = mutableMapOf<Long, Int>()
    // Doc from perflib:
    // Native allocations can be identified by looking at instances of
    // libcore.util.NativeAllocationRegistry$CleanerThunk. The "owning" Java object is the
    // "referent" field of the "sun.misc.Cleaner" instance with a hard reference to the
    // CleanerThunk.
    //
    // The size is in the 'size' field of the libcore.util.NativeAllocationRegistry instance
    // that the CleanerThunk has a pointer to. The native pointer is in the 'nativePtr' field of
    // the CleanerThunk. The hprof does not include the native bytes pointed to.
    graph.findClassByName("sun.misc.Cleaner")?.let { cleanerClass ->
      cleanerClass.directInstances.forEach { cleaner ->
        val thunkField = cleaner["sun.misc.Cleaner", "thunk"]
        val thunkId = thunkField?.value?.asNonNullObjectId
        val referentId =
          cleaner["java.lang.ref.Reference", "referent"]?.value?.asNonNullObjectId
        if (thunkId != null && referentId != null) {
          val thunkRecord = thunkField.value.asObject
          if (thunkRecord is HeapInstance && thunkRecord instanceOf "libcore.util.NativeAllocationRegistry\$CleanerThunk") {
            val allocationRegistryIdField =
              thunkRecord["libcore.util.NativeAllocationRegistry\$CleanerThunk", "this\$0"]
            if (allocationRegistryIdField != null && allocationRegistryIdField.value.isNonNullReference) {
              val allocationRegistryRecord = allocationRegistryIdField.value.asObject
              if (allocationRegistryRecord is HeapInstance && allocationRegistryRecord instanceOf "libcore.util.NativeAllocationRegistry") {
                var nativeSize = nativeSizes[referentId] ?: 0
                nativeSize += allocationRegistryRecord["libcore.util.NativeAllocationRegistry", "size"]?.value?.asLong?.toInt()
                  ?: 0
                nativeSizes[referentId] = nativeSize
              }
            }
          }
        }
      }
    }
    return nativeSizes
  }
}

@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
package shark.internal

import shark.ValueHolder
import shark.internal.ObjectDominators.DominatorNode
import shark.internal.hppc.LongLongScatterMap
import shark.internal.hppc.LongLongScatterMap.ForEachCallback
import shark.internal.hppc.LongScatterSet

internal class DominatorTree(expectedElements: Int = 4) {

  /**
   * Map of objects to their dominator.
   *
   * If an object is dominated by more than one GC root then its dominator is set to
   * [ValueHolder.NULL_REFERENCE].
   */
  private val dominated = LongLongScatterMap(expectedElements)

  /**
   * Records that [objectId] is a root.
   */
  fun updateDominatedAsRoot(objectId: Long): Boolean {
    return updateDominated(objectId, ValueHolder.NULL_REFERENCE)
  }

  /**
   * Records that [objectId] can be reached through [parentObjectId], updating the dominator for
   * [objectId] to be either [parentObjectId] if [objectId] has no known dominator and otherwise to
   * the Lowest Common Dominator between [parentObjectId] and the previously determined dominator
   * for [objectId].
   *
   * [parentObjectId] should already have been added via [updateDominatedAsRoot]. Failing to do
   * that will throw [IllegalStateException] on future calls.
   *
   * This implementation is optimized with the assumption that the graph is visited as a breadth
   * first search, so when objectId already has a known dominator then its dominator path is
   * shorter than the dominator path of [parentObjectId].
   *
   * @return true if [objectId] already had a known dominator, false otherwise.
   */
  fun updateDominated(
    objectId: Long,
    parentObjectId: Long
  ): Boolean {
    val dominatedSlot = dominated.getSlot(objectId)

    val hasDominator = dominatedSlot != -1

    if (!hasDominator || parentObjectId == ValueHolder.NULL_REFERENCE) {
      dominated[objectId] = parentObjectId
    } else {
      val currentDominator = dominated.getSlotValue(dominatedSlot)
      if (currentDominator != ValueHolder.NULL_REFERENCE) {
        // We're looking for the Lowest Common Dominator between currentDominator and
        // parentObjectId. We know that currentDominator likely has a shorter dominator path than
        // parentObjectId since we're exploring the graph with a breadth first search. So we build
        // a temporary hash set for the dominator path of currentDominator (since it's smaller)
        // and then go through the dominator path of parentObjectId checking if any id exists
        // in that hash set.
        // Once we find either a common dominator or none, we update the map accordingly
        val currentDominators = LongScatterSet()
        var dominator = currentDominator
        while (dominator != ValueHolder.NULL_REFERENCE) {
          currentDominators.add(dominator)
          val nextDominatorSlot = dominated.getSlot(dominator)
          if (nextDominatorSlot == -1) {
            throw IllegalStateException(
              "Did not find dominator for $dominator when going through the dominator chain for $currentDominator: $currentDominators"
            )
          } else {
            dominator = dominated.getSlotValue(nextDominatorSlot)
          }
        }
        dominator = parentObjectId
        while (dominator != ValueHolder.NULL_REFERENCE) {
          if (dominator in currentDominators) {
            break
          }
          val nextDominatorSlot = dominated.getSlot(dominator)
          if (nextDominatorSlot == -1) {
            throw IllegalStateException(
              "Did not find dominator for $dominator when going through the dominator chain for $parentObjectId"
            )
          } else {
            dominator = dominated.getSlotValue(nextDominatorSlot)
          }
        }
        dominated[objectId] = dominator
      }
    }
    return hasDominator
  }

  private class MutableDominatorNode {
    var shallowSize = 0
    var retainedSize = 0
    var retainedCount = 0
    val dominated = mutableListOf<Long>()
  }

  fun buildFullDominatorTree(computeSize: (Long) -> Int): Map<Long, DominatorNode> {
    val dominators = mutableMapOf<Long, MutableDominatorNode>()
    dominated.forEach(object : ForEachCallback {
      override fun onEntry(
        key: Long,
        value: Long
      ) {
        // create entry for dominated
        dominators.getOrPut(key) {
          MutableDominatorNode()
        }
        // If dominator is null ref then we still have an entry for that, to collect all dominator
        // roots.
        dominators.getOrPut(value) {
          MutableDominatorNode()
        }.dominated += key
      }
    })

    val allReachableObjectIds = dominators.keys.toSet() - ValueHolder.NULL_REFERENCE

    val retainedSizes = computeRetainedSizes(allReachableObjectIds) { objectId ->
      val shallowSize = computeSize(objectId)
      dominators.getValue(objectId).shallowSize = shallowSize
      shallowSize
    }

    dominators.forEach { (objectId, node) ->
      if (objectId != ValueHolder.NULL_REFERENCE) {
        val (retainedSize, retainedCount) = retainedSizes.getValue(objectId)
        node.retainedSize = retainedSize
        node.retainedCount = retainedCount
      }
    }

    val rootDominator = dominators.getValue(ValueHolder.NULL_REFERENCE)
    rootDominator.retainedSize = rootDominator.dominated.map { dominators[it]!!.retainedSize }.sum()
    rootDominator.retainedCount =
      rootDominator.dominated.map { dominators[it]!!.retainedCount }.sum()

    // Sort children with largest retained first
    dominators.values.forEach { node ->
      node.dominated.sortBy { -dominators.getValue(it).retainedSize }
    }

    return dominators.mapValues { (_, node) ->
      DominatorNode(
        node.shallowSize, node.retainedSize, node.retainedCount, node.dominated
      )
    }
  }

  /**
   * Computes the size retained by [retainedObjectIds] using the dominator tree built using
   * [updateDominatedAsRoot]. The shallow size of each object is provided by [computeSize].
   * @return a map of object id to retained size.
   */
  fun computeRetainedSizes(
    retainedObjectIds: Set<Long>,
    computeSize: (Long) -> Int
  ): Map<Long, Pair<Int, Int>> {
    val nodeRetainedSizes = mutableMapOf<Long, Pair<Int, Int>>()
    retainedObjectIds.forEach { objectId ->
      nodeRetainedSizes[objectId] = 0 to 0
    }

    dominated.forEach(object : ForEachCallback {
      override fun onEntry(
        key: Long,
        value: Long
      ) {
        // lazy computing of instance size
        var instanceSize = -1

        // If the entry is a node, add its size to nodeRetainedSizes
        nodeRetainedSizes[key]?.let { (currentRetainedSize, currentRetainedCount) ->
          instanceSize = computeSize(key)
          nodeRetainedSizes[key] = currentRetainedSize + instanceSize to currentRetainedCount + 1
        }

        if (value != ValueHolder.NULL_REFERENCE) {
          var dominator = value
          val dominatedByNextNode = mutableListOf(key)
          while (dominator != ValueHolder.NULL_REFERENCE) {
            // If dominator is a node
            if (nodeRetainedSizes.containsKey(dominator)) {
              // Update dominator for all objects in the dominator path so far to directly point
              // to it. We're compressing the dominator path to make this iteration faster and
              // faster as we go through each entry.
              dominatedByNextNode.forEach { objectId ->
                dominated[objectId] = dominator
              }
              if (instanceSize == -1) {
                instanceSize = computeSize(key)
              }
              // Update retained size for that node
              val (currentRetainedSize, currentRetainedCount) = nodeRetainedSizes.getValue(
                dominator
              )
              nodeRetainedSizes[dominator] =
                (currentRetainedSize + instanceSize) to currentRetainedCount + 1
              dominatedByNextNode.clear()
            } else {
              dominatedByNextNode += dominator
            }
            dominator = dominated[dominator]
          }
          // Update all dominator for all objects found in the dominator path after the last node
          dominatedByNextNode.forEach { objectId ->
            dominated[objectId] = ValueHolder.NULL_REFERENCE
          }
        }
      }
    })
    dominated.release()

    return nodeRetainedSizes
  }
}

package shark.internal

import shark.HprofRecord.HeapDumpRecord.ObjectRecord.InstanceDumpRecord

/**
 * Simplified version of [FieldValuesReader] class that can only read an ID or skip a certain
 * amount of bytes.
 */
internal class FieldIdReader(
  private val record: InstanceDumpRecord,
  private val identifierByteSize: Int
) {

  private var position = 0

  fun readId(): Long {
    // As long as we don't interpret IDs, reading signed values here is fine.
    val value = when (identifierByteSize) {
      1 -> readByteId(position, record.fieldValues)
      2 -> readShortId(position, record.fieldValues)
      4 -> readIntId(position, record.fieldValues)
      8 -> readLongId(position, record.fieldValues)
      else -> error("ID Length must be 1, 2, 4, or 8")
    }
    position += identifierByteSize
    return value
  }

  fun skipBytes(count: Int) {
    position += count
  }

  private fun readByteId(
    index: Int,
    array: ByteArray
  ) =
    array[index].toLong()

  private fun readShortId(
    index: Int,
    array: ByteArray
  ): Long {
    var pos = index
    return (array[pos++] and 0xff shl 8
      or (array[pos] and 0xff)).toLong()
  }

  private fun readIntId(
    index: Int,
    array: ByteArray
  ): Long {
    var pos = index
    return (array[pos++] and 0xff shl 24
      or (array[pos++] and 0xff shl 16)
      or (array[pos++] and 0xff shl 8)
      or (array[pos] and 0xff)).toLong()
  }

  private fun readLongId(
    index: Int,
    array: ByteArray
  ): Long {
    var pos = index
    return (array[pos++] and 0xffL shl 56
      or (array[pos++] and 0xffL shl 48)
      or (array[pos++] and 0xffL shl 40)
      or (array[pos++] and 0xffL shl 32)
      or (array[pos++] and 0xffL shl 24)
      or (array[pos++] and 0xffL shl 16)
      or (array[pos++] and 0xffL shl 8)
      or (array[pos] and 0xffL))
  }

  @Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
  private inline infix fun Byte.and(other: Long): Long = toLong() and other

  @Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
  private inline infix fun Byte.and(other: Int): Int = toInt() and other
}

package shark.internal

import shark.HeapObject.HeapInstance
import shark.ValueHolder
import shark.ValueHolder.ReferenceHolder

internal class KeyedWeakReferenceMirror(
  val referent: ReferenceHolder,
  val key: String,
  // The name field does not exist in pre 1.0 heap dumps.
  val description: String,
  // null in pre 2.0 alpha 3 heap dumps
  val watchDurationMillis: Long?,
  // null in pre 2.0 alpha 3 heap dumps, -1 if the instance is not retained.
  val retainedDurationMillis: Long?
) {

  val hasReferent = referent.value != ValueHolder.NULL_REFERENCE

  val isRetained = retainedDurationMillis == null || retainedDurationMillis != -1L

  companion object {

    private const val UNKNOWN_LEGACY = "Unknown (legacy)"

    fun fromInstance(
      weakRef: HeapInstance,
      // Null for pre 2.0 alpha 3 heap dumps
      heapDumpUptimeMillis: Long?
    ): KeyedWeakReferenceMirror {

      val keyWeakRefClassName = weakRef.instanceClassName
      val watchDurationMillis = if (heapDumpUptimeMillis != null) {
        heapDumpUptimeMillis - weakRef[keyWeakRefClassName, "watchUptimeMillis"]!!.value.asLong!!
      } else {
        null
      }

      val retainedDurationMillis = if (heapDumpUptimeMillis != null) {
        val retainedUptimeMillis =
          weakRef[keyWeakRefClassName, "retainedUptimeMillis"]!!.value.asLong!!
        if (retainedUptimeMillis == -1L) -1L else heapDumpUptimeMillis - retainedUptimeMillis
      } else {
        null
      }

      val keyString = weakRef[keyWeakRefClassName, "key"]!!.value.readAsJavaString()!!

      // Changed from name to description after 2.0
      val description = (weakRef[keyWeakRefClassName, "description"]
        ?: weakRef[keyWeakRefClassName, "name"])?.value?.readAsJavaString() ?: UNKNOWN_LEGACY
      return KeyedWeakReferenceMirror(
        watchDurationMillis = watchDurationMillis,
        retainedDurationMillis = retainedDurationMillis,
        referent = weakRef["java.lang.ref.Reference", "referent"]!!.value.holder as ReferenceHolder,
        key = keyString,
        description = description
      )
    }
  }
}

package shark.internal

import shark.GcRoot.ThreadObject
import shark.HeapGraph
import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import shark.HeapObject.HeapObjectArray
import shark.HeapObject.HeapPrimitiveArray
import shark.IgnoredReferenceMatcher
import shark.OnAnalysisProgressListener
import shark.ValueHolder

/**
 * Exposes high level APIs to compute and render a dominator tree. This class
 * needs to be public to be used by other LeakCanary modules but is internal and
 * its API might change at any moment.
 *
 * Note that the exposed APIs are not optimized for speed, memory or IO.
 *
 * Eventually this capability should become part of the Shark public APIs, please
 * open an issue if you'd like to use this directly.
 */
class ObjectDominators {

  internal data class DominatorNode(
    val shallowSize: Int,
    val retainedSize: Int,
    val retainedCount: Int,
    val dominatedObjectIds: List<Long>
  )

  fun renderDominatorTree(
    graph: HeapGraph,
    ignoredRefs: List<IgnoredReferenceMatcher>,
    minRetainedSize: Int,
    threadName: String? = null,
    printStringContent: Boolean = false
  ): String {
    val stringBuilder = StringBuilder()

    val dominatorTree = buildDominatorTree(graph, ignoredRefs)

    val root = dominatorTree.getValue(ValueHolder.NULL_REFERENCE)
    stringBuilder.append(
      "Total retained: ${root.retainedSize} bytes in ${root.retainedCount} objects. Root dominators: ${root.dominatedObjectIds.size}\n\n"
    )

    val rootIds = if (threadName != null) {
      setOf(graph.gcRoots.first { gcRoot ->
        gcRoot is ThreadObject &&
          graph.objectExists(gcRoot.id) &&
          graph.findObjectById(gcRoot.id)
            .asInstance!!["java.lang.Thread", "name"]!!
            .value.readAsJavaString() == threadName
      }.id)
    } else {
      root.dominatedObjectIds.filter { dominatorTree.getValue(it).retainedSize > minRetainedSize }
    }

    rootIds
      .forEach { objectId ->
        printTree(
          stringBuilder, graph, dominatorTree, objectId, minRetainedSize, 0, "", true,
          printStringContent
        )
        stringBuilder.append("\n")
      }
    return stringBuilder.toString()
  }

  @Suppress("LongParameterList")
  private fun printTree(
    stringBuilder: StringBuilder,
    graph: HeapGraph,
    tree: Map<Long, DominatorNode>,
    objectId: Long,
    minSize: Int,
    depth: Int,
    prefix: String,
    isLast: Boolean,
    printStringContent: Boolean
  ) {
    val node = tree.getValue(objectId)
    val heapObject = graph.findObjectById(objectId)
    val className = when (heapObject) {
      is HeapClass -> "class ${heapObject.name}"
      is HeapInstance -> heapObject.instanceClassName
      is HeapObjectArray -> heapObject.arrayClassName
      is HeapPrimitiveArray -> heapObject.arrayClassName
    }
    val anchor = if (depth == 0) "" else if (isLast) "╰─" else "├─"
    val size = if (node.retainedSize != node.shallowSize) {
      "${node.retainedSize} bytes (${node.shallowSize} self)"
    } else {
      "${node.shallowSize} bytes"
    }
    val count = if (node.retainedCount > 1) {
      " ${node.retainedCount} objects"
    } else {
      ""
    }
    val stringContent = if (
      printStringContent &&
      heapObject is HeapInstance &&
      heapObject.instanceClassName == "java.lang.String"
    ) " \"${heapObject.readAsJavaString()}\"" else ""
    stringBuilder.append(
      "$prefix$anchor$className #${heapObject.objectIndex} Retained: $size$count$stringContent\n"
    )

    val newPrefix = when {
      depth == 0 -> ""
      isLast -> {
        "$prefix  "
      }
      else -> {
        "$prefix│ "
      }
    }

    val largeChildren = node.dominatedObjectIds.filter { tree.getValue(it).retainedSize > minSize }
    val lastIndex = node.dominatedObjectIds.lastIndex

    largeChildren.forEachIndexed { index, objectId ->
      printTree(
        stringBuilder,
        graph, tree, objectId, minSize, depth + 1, newPrefix,
        index == lastIndex,
        printStringContent
      )
    }
    if (largeChildren.size < node.dominatedObjectIds.size) {
      stringBuilder.append("$newPrefix╰┄\n")
    }
  }

  private fun buildDominatorTree(
    graph: HeapGraph,
    ignoredRefs: List<IgnoredReferenceMatcher>
  ): Map<Long, DominatorNode> {
    val pathFinder = PathFinder(
      graph,
      OnAnalysisProgressListener.NO_OP, ignoredRefs
    )
    val nativeSizeMapper = AndroidNativeSizeMapper(graph)
    val nativeSizes = nativeSizeMapper.mapNativeSizes()
    val shallowSizeCalculator = ShallowSizeCalculator(graph)

    val result = pathFinder.findPathsFromGcRoots(setOf(), true)
    return result.dominatorTree!!.buildFullDominatorTree { objectId ->
      val nativeSize = nativeSizes[objectId] ?: 0
      val shallowSize = shallowSizeCalculator.computeShallowSize(objectId)
      nativeSize + shallowSize
    }
  }
}

@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
package shark.internal

import shark.GcRoot
import shark.GcRoot.JavaFrame
import shark.GcRoot.JniGlobal
import shark.GcRoot.ThreadObject
import shark.HeapGraph
import shark.HeapObject
import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import shark.HeapObject.HeapObjectArray
import shark.HeapObject.HeapPrimitiveArray
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.FieldRecord
import shark.IgnoredReferenceMatcher
import shark.LeakTraceReference.ReferenceType.ARRAY_ENTRY
import shark.LeakTraceReference.ReferenceType.INSTANCE_FIELD
import shark.LeakTraceReference.ReferenceType.LOCAL
import shark.LeakTraceReference.ReferenceType.STATIC_FIELD
import shark.LibraryLeakReferenceMatcher
import shark.OnAnalysisProgressListener
import shark.OnAnalysisProgressListener.Step.FINDING_DOMINATORS
import shark.OnAnalysisProgressListener.Step.FINDING_PATHS_TO_RETAINED_OBJECTS
import shark.PrimitiveType
import shark.PrimitiveType.BOOLEAN
import shark.PrimitiveType.BYTE
import shark.PrimitiveType.CHAR
import shark.PrimitiveType.DOUBLE
import shark.PrimitiveType.FLOAT
import shark.PrimitiveType.INT
import shark.PrimitiveType.LONG
import shark.PrimitiveType.SHORT
import shark.ReferenceMatcher
import shark.ReferencePattern
import shark.ReferencePattern.InstanceFieldPattern
import shark.ReferencePattern.NativeGlobalVariablePattern
import shark.ReferencePattern.StaticFieldPattern
import shark.ValueHolder
import shark.ValueHolder.ReferenceHolder
import shark.internal.PathFinder.VisitTracker.Dominated
import shark.internal.PathFinder.VisitTracker.Visited
import shark.internal.ReferencePathNode.ChildNode
import shark.internal.ReferencePathNode.ChildNode.LibraryLeakChildNode
import shark.internal.ReferencePathNode.ChildNode.NormalNode
import shark.internal.ReferencePathNode.LibraryLeakNode
import shark.internal.ReferencePathNode.RootNode
import shark.internal.ReferencePathNode.RootNode.LibraryLeakRootNode
import shark.internal.ReferencePathNode.RootNode.NormalRootNode
import shark.internal.hppc.LongScatterSet
import java.util.ArrayDeque
import java.util.Deque
import java.util.LinkedHashMap

/**
 * Not thread safe.
 *
 * Finds the shortest path from leaking references to a gc root, first ignoring references
 * identified as "to visit last" and then visiting them as needed if no path is
 * found.
 */
internal class PathFinder(
  private val graph: HeapGraph,
  private val listener: OnAnalysisProgressListener,
  referenceMatchers: List<ReferenceMatcher>
) {

  class PathFindingResults(
    val pathsToLeakingObjects: List<ReferencePathNode>,
    val dominatorTree: DominatorTree?
  )

  sealed class VisitTracker {

    abstract fun visited(
      objectId: Long,
      parentObjectId: Long
    ): Boolean

    class Dominated(expectedElements: Int) : VisitTracker() {
      /**
       * Tracks visited objecs and their dominator.
       * If an object is not in [dominatorTree] then it hasn't been enqueued yet.
       * If an object is in [dominatorTree] but not in [State.toVisitSet] nor [State.toVisitLastSet]
       * then it has already been dequeued.
       *
       * If an object is dominated by more than one GC root then its dominator is set to
       * [ValueHolder.NULL_REFERENCE].
       */
      val dominatorTree = DominatorTree(expectedElements)
      override fun visited(
        objectId: Long,
        parentObjectId: Long
      ): Boolean {
        return dominatorTree.updateDominated(objectId, parentObjectId)
      }
    }

    class Visited(expectedElements: Int) : VisitTracker() {
      /**
       * Set of visited objects.
       */
      private val visitedSet = LongScatterSet(expectedElements)
      override fun visited(
        objectId: Long,
        parentObjectId: Long
      ): Boolean {
        return !visitedSet.add(objectId)
      }
    }
  }

  private class State(
    val leakingObjectIds: LongScatterSet,
    val sizeOfObjectInstances: Int,
    val computeRetainedHeapSize: Boolean,
    val javaLangObjectId: Long,
    estimatedVisitedObjects: Int
  ) {

    /** Set of objects to visit */
    val toVisitQueue: Deque<ReferencePathNode> = ArrayDeque()

    /**
     * Objects to visit when [toVisitQueue] is empty. Should contain [JavaFrame] gc roots first,
     * then [LibraryLeakNode].
     */
    val toVisitLastQueue: Deque<ReferencePathNode> = ArrayDeque()

    /**
     * Enables fast checking of whether a node is already in the queue.
     */
    val toVisitSet = LongScatterSet()
    val toVisitLastSet = LongScatterSet()

    val queuesNotEmpty: Boolean
      get() = toVisitQueue.isNotEmpty() || toVisitLastQueue.isNotEmpty()

    val visitTracker = if (computeRetainedHeapSize) {
      Dominated(estimatedVisitedObjects)
    } else {
      Visited(estimatedVisitedObjects)
    }

    /**
     * A marker for when we're done exploring the graph of higher priority references and start
     * visiting the lower priority references, at which point we won't add any reference to
     * the high priority queue anymore.
     */
    var visitingLast = false
  }

  private val fieldNameByClassName: Map<String, Map<String, ReferenceMatcher>>
  private val staticFieldNameByClassName: Map<String, Map<String, ReferenceMatcher>>
  private val threadNameReferenceMatchers: Map<String, ReferenceMatcher>
  private val jniGlobalReferenceMatchers: Map<String, ReferenceMatcher>

  init {
    val fieldNameByClassName = mutableMapOf<String, MutableMap<String, ReferenceMatcher>>()
    val staticFieldNameByClassName = mutableMapOf<String, MutableMap<String, ReferenceMatcher>>()
    val threadNames = mutableMapOf<String, ReferenceMatcher>()
    val jniGlobals = mutableMapOf<String, ReferenceMatcher>()

    val appliedRefMatchers = referenceMatchers.filter {
      (it is IgnoredReferenceMatcher || (it is LibraryLeakReferenceMatcher && it.patternApplies(
        graph
      )))
    }

    appliedRefMatchers.forEach { referenceMatcher ->
      when (val pattern = referenceMatcher.pattern) {
        is ReferencePattern.JavaLocalPattern -> {
          threadNames[pattern.threadName] = referenceMatcher
        }
        is StaticFieldPattern -> {
          val mapOrNull = staticFieldNameByClassName[pattern.className]
          val map = if (mapOrNull != null) mapOrNull else {
            val newMap = mutableMapOf<String, ReferenceMatcher>()
            staticFieldNameByClassName[pattern.className] = newMap
            newMap
          }
          map[pattern.fieldName] = referenceMatcher
        }
        is InstanceFieldPattern -> {
          val mapOrNull = fieldNameByClassName[pattern.className]
          val map = if (mapOrNull != null) mapOrNull else {
            val newMap = mutableMapOf<String, ReferenceMatcher>()
            fieldNameByClassName[pattern.className] = newMap
            newMap
          }
          map[pattern.fieldName] = referenceMatcher
        }
        is NativeGlobalVariablePattern -> {
          jniGlobals[pattern.className] = referenceMatcher
        }
      }
    }
    this.fieldNameByClassName = fieldNameByClassName
    this.staticFieldNameByClassName = staticFieldNameByClassName
    this.threadNameReferenceMatchers = threadNames
    this.jniGlobalReferenceMatchers = jniGlobals
  }

  fun findPathsFromGcRoots(
    leakingObjectIds: Set<Long>,
    computeRetainedHeapSize: Boolean
  ): PathFindingResults {
    listener.onAnalysisProgress(FINDING_PATHS_TO_RETAINED_OBJECTS)

    val objectClass = graph.findClassByName("java.lang.Object")
    val sizeOfObjectInstances = determineSizeOfObjectInstances(objectClass, graph)
    val javaLangObjectId = objectClass?.objectId ?: -1

    // Estimate of how many objects we'll visit. This is a conservative estimate, we should always
    // visit more than that but this limits the number of early array growths.
    val estimatedVisitedObjects = (graph.instanceCount / 2).coerceAtLeast(4)

    val state = State(
      leakingObjectIds = leakingObjectIds.toLongScatterSet(),
      sizeOfObjectInstances = sizeOfObjectInstances,
      computeRetainedHeapSize = computeRetainedHeapSize,
      javaLangObjectId = javaLangObjectId,
      estimatedVisitedObjects = estimatedVisitedObjects
    )

    return state.findPathsFromGcRoots()
  }

  private fun determineSizeOfObjectInstances(
    objectClass: HeapClass?,
    graph: HeapGraph
  ): Int {
    return if (objectClass != null) {
      // In Android 16 ClassDumpRecord.instanceSize for java.lang.Object can be 8 yet there are 0
      // fields. This is likely because there is extra per instance data that isn't coming from
      // fields in the Object class. See #1374
      val objectClassFieldSize = objectClass.readFieldsByteSize()

      // shadow$_klass_ (object id) + shadow$_monitor_ (Int)
      val sizeOfObjectOnArt = graph.identifierByteSize + INT.byteSize
      if (objectClassFieldSize == sizeOfObjectOnArt) {
        sizeOfObjectOnArt
      } else {
        0
      }
    } else {
      0
    }
  }

  private fun Set<Long>.toLongScatterSet(): LongScatterSet {
    val longScatterSet = LongScatterSet()
    longScatterSet.ensureCapacity(size)
    forEach { longScatterSet.add(it) }
    return longScatterSet
  }

  private fun State.findPathsFromGcRoots(): PathFindingResults {
    enqueueGcRoots()

    val shortestPathsToLeakingObjects = mutableListOf<ReferencePathNode>()
    visitingQueue@ while (queuesNotEmpty) {
      val node = poll()
      if (leakingObjectIds.contains(node.objectId)) {
        shortestPathsToLeakingObjects.add(node)
        // Found all refs, stop searching (unless computing retained size)
        if (shortestPathsToLeakingObjects.size == leakingObjectIds.size()) {
          if (computeRetainedHeapSize) {
            listener.onAnalysisProgress(FINDING_DOMINATORS)
          } else {
            break@visitingQueue
          }
        }
      }

      when (val heapObject = graph.findObjectById(node.objectId)) {
        is HeapClass -> visitClassRecord(heapObject, node)
        is HeapInstance -> visitInstance(heapObject, node)
        is HeapObjectArray -> visitObjectArray(heapObject, node)
      }
    }
    return PathFindingResults(
      shortestPathsToLeakingObjects,
      if (visitTracker is Dominated) visitTracker.dominatorTree else null
    )
  }

  private fun State.poll(): ReferencePathNode {
    return if (!visitingLast && !toVisitQueue.isEmpty()) {
      val removedNode = toVisitQueue.poll()
      toVisitSet.remove(removedNode.objectId)
      removedNode
    } else {
      visitingLast = true
      val removedNode = toVisitLastQueue.poll()
      toVisitLastSet.remove(removedNode.objectId)
      removedNode
    }
  }

  private fun State.enqueueGcRoots() {
    val gcRoots = sortedGcRoots()

    val threadNames = mutableMapOf<HeapInstance, String>()
    val threadsBySerialNumber = mutableMapOf<Int, Pair<HeapInstance, ThreadObject>>()
    gcRoots.forEach { (objectRecord, gcRoot) ->
      when (gcRoot) {
        is ThreadObject -> {
          threadsBySerialNumber[gcRoot.threadSerialNumber] = objectRecord.asInstance!! to gcRoot
          enqueue(NormalRootNode(gcRoot.id, gcRoot))
        }
        is JavaFrame -> {
          val threadPair = threadsBySerialNumber[gcRoot.threadSerialNumber]
          if (threadPair == null) {
            // Could not find the thread that this java frame is for.
            enqueue(NormalRootNode(gcRoot.id, gcRoot))
          } else {
            val (threadInstance, threadRoot) = threadPair
            val threadName = threadNames[threadInstance] ?: {
              val name = threadInstance[Thread::class, "name"]?.value?.readAsJavaString() ?: ""
              threadNames[threadInstance] = name
              name
            }()
            val referenceMatcher = threadNameReferenceMatchers[threadName]

            if (referenceMatcher !is IgnoredReferenceMatcher) {
              val rootNode = NormalRootNode(threadRoot.id, gcRoot)

              val refFromParentType = LOCAL
              // Unfortunately Android heap dumps do not include stack trace data, so
              // JavaFrame.frameNumber is always -1 and we cannot know which method is causing the
              // reference to be held.
              val refFromParentName = ""

              val childNode = if (referenceMatcher is LibraryLeakReferenceMatcher) {
                LibraryLeakChildNode(
                  objectId = gcRoot.id,
                  parent = rootNode,
                  refFromParentType = refFromParentType,
                  refFromParentName = refFromParentName,
                  matcher = referenceMatcher
                )
              } else {
                NormalNode(
                  objectId = gcRoot.id,
                  parent = rootNode,
                  refFromParentType = refFromParentType,
                  refFromParentName = refFromParentName
                )
              }
              enqueue(childNode)
            }
          }
        }
        is JniGlobal -> {
          val referenceMatcher = when (objectRecord) {
            is HeapClass -> jniGlobalReferenceMatchers[objectRecord.name]
            is HeapInstance -> jniGlobalReferenceMatchers[objectRecord.instanceClassName]
            is HeapObjectArray -> jniGlobalReferenceMatchers[objectRecord.arrayClassName]
            is HeapPrimitiveArray -> jniGlobalReferenceMatchers[objectRecord.arrayClassName]
          }
          if (referenceMatcher !is IgnoredReferenceMatcher) {
            if (referenceMatcher is LibraryLeakReferenceMatcher) {
              enqueue(LibraryLeakRootNode(gcRoot.id, gcRoot, referenceMatcher))
            } else {
              enqueue(NormalRootNode(gcRoot.id, gcRoot))
            }
          }
        }
        else -> enqueue(NormalRootNode(gcRoot.id, gcRoot))
      }
    }
  }

  /**
   * Sorting GC roots to get stable shortest path
   * Once sorted all ThreadObject Gc Roots are located before JavaLocalPattern Gc Roots.
   * This ensures ThreadObjects are visited before JavaFrames, and threadsBySerialNumber can be
   * built before JavaFrames.
   */
  private fun sortedGcRoots(): List<Pair<HeapObject, GcRoot>> {
    val rootClassName: (HeapObject) -> String = { graphObject ->
      when (graphObject) {
        is HeapClass -> {
          graphObject.name
        }
        is HeapInstance -> {
          graphObject.instanceClassName
        }
        is HeapObjectArray -> {
          graphObject.arrayClassName
        }
        is HeapPrimitiveArray -> {
          graphObject.arrayClassName
        }
      }
    }

    return graph.gcRoots
      .filter { gcRoot ->
        // GC roots sometimes reference objects that don't exist in the heap dump
        // See https://github.com/square/leakcanary/issues/1516
        graph.objectExists(gcRoot.id)
      }
      .map { graph.findObjectById(it.id) to it }
      .sortedWith(Comparator { (graphObject1, root1), (graphObject2, root2) ->
        // Sorting based on pattern name first. In reverse order so that ThreadObject is before JavaLocalPattern
        val gcRootTypeComparison = root2::class.java.name.compareTo(root1::class.java.name)
        if (gcRootTypeComparison != 0) {
          gcRootTypeComparison
        } else {
          rootClassName(graphObject1).compareTo(rootClassName(graphObject2))
        }
      })
  }

  private fun State.visitClassRecord(
    heapClass: HeapClass,
    parent: ReferencePathNode
  ) {
    val ignoredStaticFields = staticFieldNameByClassName[heapClass.name] ?: emptyMap()

    for (staticField in heapClass.readStaticFields()) {
      if (!staticField.value.isNonNullReference) {
        continue
      }

      val fieldName = staticField.name
      if (fieldName == "\$staticOverhead" || fieldName == "\$classOverhead") {
        continue
      }

      // Note: instead of calling staticField.value.asObjectId!! we cast holder to ReferenceHolder
      // and access value directly. This allows us to avoid unnecessary boxing of Long.
      val objectId = (staticField.value.holder as ReferenceHolder).value

      val node = when (val referenceMatcher = ignoredStaticFields[fieldName]) {
        null -> NormalNode(
          objectId = objectId,
          parent = parent,
          refFromParentType = STATIC_FIELD,
          refFromParentName = fieldName
        )
        is LibraryLeakReferenceMatcher -> LibraryLeakChildNode(
          objectId = objectId,
          parent = parent,
          refFromParentType = STATIC_FIELD,
          refFromParentName = fieldName,
          matcher = referenceMatcher
        )
        is IgnoredReferenceMatcher -> null
      }
      if (node != null) {
        enqueue(node)
      }
    }
  }

  private fun State.visitInstance(
    instance: HeapInstance,
    parent: ReferencePathNode
  ) {
    val fieldReferenceMatchers = LinkedHashMap<String, ReferenceMatcher>()

    instance.instanceClass.classHierarchy.forEach {
      val referenceMatcherByField = fieldNameByClassName[it.name]
      if (referenceMatcherByField != null) {
        for ((fieldName, referenceMatcher) in referenceMatcherByField) {
          if (!fieldReferenceMatchers.containsKey(fieldName)) {
            fieldReferenceMatchers[fieldName] = referenceMatcher
          }
        }
      }
    }
    val classHierarchy =
      instance.instanceClass.classHierarchyWithoutJavaLangObject(javaLangObjectId)
    val fieldNamesAndValues =
      instance.readAllNonNullFieldsOfReferenceType(classHierarchy)

    fieldNamesAndValues.sortBy { it.fieldName }

    fieldNamesAndValues.forEach { instanceRefField ->
      val node = when (val referenceMatcher = fieldReferenceMatchers[instanceRefField.fieldName]) {
        null -> NormalNode(
          objectId = instanceRefField.refObjectId,
          parent = parent,
          refFromParentType = INSTANCE_FIELD,
          refFromParentName = instanceRefField.fieldName,
          owningClassId = instanceRefField.declaringClassId
        )
        is LibraryLeakReferenceMatcher ->
          LibraryLeakChildNode(
            objectId = instanceRefField.refObjectId,
            parent = parent,
            refFromParentType = INSTANCE_FIELD,
            refFromParentName = instanceRefField.fieldName,
            matcher = referenceMatcher,
            owningClassId = instanceRefField.declaringClassId
          )
        is IgnoredReferenceMatcher -> null
      }
      if (node != null) {
        enqueue(node)
      }
    }
  }

  private class InstanceRefField(
    val declaringClassId: Long,
    val refObjectId: Long,
    val fieldName: String
  )

  private fun HeapInstance.readAllNonNullFieldsOfReferenceType(
    classHierarchy: List<HeapClass>
  ): MutableList<InstanceRefField> {
    // Assigning to local variable to avoid repeated lookup and cast:
    // HeapInstance.graph casts HeapInstance.hprofGraph to HeapGraph in its getter
    val hprofGraph = graph
    var fieldReader: FieldIdReader? = null
    val result = mutableListOf<InstanceRefField>()
    var skipBytesCount = 0

    for (heapClass in classHierarchy) {
      for (fieldRecord in heapClass.readRecordFields()) {
        if (fieldRecord.type != PrimitiveType.REFERENCE_HPROF_TYPE) {
          // Skip all fields that are not references. Track how many bytes to skip
          skipBytesCount += hprofGraph.getRecordSize(fieldRecord)
        } else {
          // Initialize id reader if it's not yet initialized. Replaces `lazy` without synchronization
          if (fieldReader == null) {
            fieldReader = FieldIdReader(readRecord(), hprofGraph.identifierByteSize)
          }

          // Skip the accumulated bytes offset
          fieldReader.skipBytes(skipBytesCount)
          skipBytesCount = 0

          val objectId = fieldReader.readId()
          if (objectId != 0L) {
            result.add(
              InstanceRefField(
                heapClass.objectId, objectId, heapClass.instanceFieldName(fieldRecord)
              )
            )
          }
        }
      }
    }
    return result
  }

  /**
   * Returns class hierarchy for an instance, but without it's root element, which is always
   * java.lang.Object.
   * Why do we want class hierarchy without java.lang.Object?
   * In pre-M there were no ref fields in java.lang.Object; and FieldIdReader wouldn't be created
   * Android M added shadow$_klass_ reference to class, so now it triggers extra record read.
   * Solution: skip heap class for java.lang.Object completely when reading the records
   * @param javaLangObjectId ID of the java.lang.Object to run comparison against
   */
  private fun HeapClass.classHierarchyWithoutJavaLangObject(
    javaLangObjectId: Long
  ): List<HeapClass> {
    val result = mutableListOf<HeapClass>()
    var parent: HeapClass? = this
    while (parent != null && parent.objectId != javaLangObjectId) {
      result += parent
      parent = parent.superclass
    }
    return result
  }

  private fun HeapGraph.getRecordSize(field: FieldRecord) =
    when (field.type) {
      PrimitiveType.REFERENCE_HPROF_TYPE -> identifierByteSize
      BOOLEAN.hprofType -> 1
      CHAR.hprofType -> 2
      FLOAT.hprofType -> 4
      DOUBLE.hprofType -> 8
      BYTE.hprofType -> 1
      SHORT.hprofType -> 2
      INT.hprofType -> 4
      LONG.hprofType -> 8
      else -> throw IllegalStateException("Unknown type ${field.type}")
    }

  private fun State.visitObjectArray(
    objectArray: HeapObjectArray,
    parent: ReferencePathNode
  ) {
    val record = objectArray.readRecord()
    val nonNullElementIds = record.elementIds.filter { objectId ->
      objectId != ValueHolder.NULL_REFERENCE && graph.objectExists(objectId)
    }
    nonNullElementIds.forEachIndexed { index, elementId ->
      val name = index.toString()
      enqueue(
        NormalNode(
          objectId = elementId,
          parent = parent,
          refFromParentType = ARRAY_ENTRY,
          refFromParentName = name
        )
      )
    }
  }

  @Suppress("ReturnCount")
  private fun State.enqueue(
    node: ReferencePathNode
  ) {
    if (node.objectId == ValueHolder.NULL_REFERENCE) {
      return
    }

    val visitLast =
      visitingLast ||
        node is LibraryLeakNode ||
        // We deprioritize thread objects because on Lollipop the thread local values are stored
        // as a field.
        (node is RootNode && node.gcRoot is ThreadObject) ||
        (node is NormalNode && node.parent is RootNode && node.parent.gcRoot is JavaFrame)

    val parentObjectId = if (node is RootNode) {
      ValueHolder.NULL_REFERENCE
    } else {
      (node as ChildNode).parent.objectId
    }

    val alreadyEnqueued = visitTracker.visited(node.objectId, parentObjectId)

    if (alreadyEnqueued) {
      // Has already been enqueued and would be added to visit last => don't enqueue.
      if (visitLast) {
        return
      }
      // Has already been enqueued and exists in the to visit set => don't enqueue
      if (toVisitSet.contains(node.objectId)) {
        return
      }
      // Has already been enqueued, is not in toVisitSet, is not in toVisitLast => has been visited
      if (!toVisitLastSet.contains(node.objectId)) {
        return
      }
    }

    // Because of the checks and return statements right before, from this point on, if
    // alreadyEnqueued then it's currently enqueued in the visit last set.
    if (alreadyEnqueued) {
      // Move from "visit last" to "visit first" queue.
      toVisitQueue.add(node)
      toVisitSet.add(node.objectId)
      val nodeToRemove = toVisitLastQueue.first { it.objectId == node.objectId }
      toVisitLastQueue.remove(nodeToRemove)
      toVisitLastSet.remove(node.objectId)
      return
    }

    val isLeakingObject = leakingObjectIds.contains(node.objectId)

    if (!isLeakingObject) {
      val skip = when (val graphObject = graph.findObjectById(node.objectId)) {
        is HeapClass -> false
        is HeapInstance ->
          when {
            graphObject.isPrimitiveWrapper -> true
            graphObject.instanceClassName == "java.lang.String" -> {
              // We ignore the fact that String references a value array to avoid having
              // to read the string record and find the object id for that array, since we know
              // it won't be interesting anyway.
              // That also means the value array isn't added to the dominator tree, so we need to
              // add that back when computing shallow size in ShallowSizeCalculator.
              // Another side effect is that if the array is referenced elsewhere, we might
              // double count its side.
              true
            }
            graphObject.instanceClass.instanceByteSize <= sizeOfObjectInstances -> true
            graphObject.instanceClass.classHierarchy.all { heapClass ->
              heapClass.objectId == javaLangObjectId || !heapClass.hasReferenceInstanceFields
            } -> true
            else -> false
          }
        is HeapObjectArray -> when {
          graphObject.isSkippablePrimitiveWrapperArray -> {
            // Same optimization as we did for String above, as we know primitive wrapper arrays
            // aren't interesting.
            true
          }
          else -> false
        }
        is HeapPrimitiveArray -> true
      }
      if (skip) {
        return
      }
    }
    if (visitLast) {
      toVisitLastQueue.add(node)
      toVisitLastSet.add(node.objectId)
    } else {
      toVisitQueue.add(node)
      toVisitSet.add(node.objectId)
    }
  }
}

private val skippablePrimitiveWrapperArrayTypes = setOf(
  Boolean::class,
  Char::class,
  Float::class,
  Double::class,
  Byte::class,
  Short::class,
  Int::class,
  Long::class
).map { it.javaObjectType.name + "[]" }

internal val HeapObjectArray.isSkippablePrimitiveWrapperArray: Boolean
  get() = arrayClassName in skippablePrimitiveWrapperArrayTypes

package shark.internal

import shark.GcRoot
import shark.LibraryLeakReferenceMatcher
import shark.LeakTraceReference

internal sealed class ReferencePathNode {
  abstract val objectId: Long

  interface LibraryLeakNode {
    val matcher: LibraryLeakReferenceMatcher
  }

  sealed class RootNode : ReferencePathNode() {
    abstract val gcRoot: GcRoot

    class LibraryLeakRootNode(
      override val objectId: Long,
      override val gcRoot: GcRoot,
      override val matcher: LibraryLeakReferenceMatcher
    ) : RootNode(), LibraryLeakNode

    class NormalRootNode(
      override val objectId: Long,
      override val gcRoot: GcRoot
    ) : RootNode()
  }

  sealed class ChildNode : ReferencePathNode() {

    abstract val parent: ReferencePathNode

    /**
     * The reference from the parent to this node
     */
    abstract val refFromParentType: LeakTraceReference.ReferenceType
    abstract val refFromParentName: String

    /**
     * If this node is an instance, then this is the id of the class that actually
     * declares the node.
     */
    abstract val owningClassId: Long

    class LibraryLeakChildNode(
      override val objectId: Long,
      override val parent: ReferencePathNode,
      override val refFromParentType: LeakTraceReference.ReferenceType,
      override val refFromParentName: String,
      override val matcher: LibraryLeakReferenceMatcher,
      override val owningClassId: Long = 0
    ) : ChildNode(), LibraryLeakNode

    class NormalNode(
      override val objectId: Long,
      override val parent: ReferencePathNode,
      override val refFromParentType: LeakTraceReference.ReferenceType,
      override val refFromParentName: String,
      override val owningClassId: Long = 0
    ) : ChildNode()
  }
}

package shark.internal

import shark.HeapGraph
import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import shark.HeapObject.HeapObjectArray
import shark.HeapObject.HeapPrimitiveArray
import shark.ValueHolder

/**
 * Provides approximations for the shallow size of objects in memory.
 *
 * Determining the actual shallow size of an object in memory is hard, as it changes for each VM
 * implementation, depending on the various memory layout optimizations and bit alignment.
 *
 * More on this topic: https://dev.to/pyricau/the-real-size-of-android-objects-1i2e
 */
internal class ShallowSizeCalculator(private val graph: HeapGraph) {

  fun computeShallowSize(objectId: Long): Int {
    return when (val heapObject = graph.findObjectById(objectId)) {
      is HeapInstance -> {
        if (heapObject.instanceClassName == "java.lang.String") {
          // In PathFinder we ignore the value field of String instances when building the dominator
          // tree, so we add that size back here.
          val valueObjectId =
            heapObject["java.lang.String", "value"]?.value?.asNonNullObjectId
          heapObject.byteSize + if (valueObjectId != null) {
            computeShallowSize(valueObjectId)
          } else {
            0
          }
        } else {
          // Total byte size of fields for instances of this class, as registered in the class dump.
          // The actual memory layout likely differs.
          heapObject.byteSize
        }
      }
      // Number of elements * object id size
      is HeapObjectArray -> {
        if (heapObject.isSkippablePrimitiveWrapperArray) {
          // In PathFinder we ignore references sfrom primitive wrapper arrayss when building the
          // dominator tree, so we add that size back here.
          val elementIds = heapObject.readRecord().elementIds
          val shallowSize = elementIds.size * graph.identifierByteSize
          val firstNonNullElement = elementIds.firstOrNull { it != ValueHolder.NULL_REFERENCE }
          if (firstNonNullElement != null) {
            val sizeOfOneElement = computeShallowSize(firstNonNullElement)
            val countOfNonNullElements = elementIds.count { it != ValueHolder.NULL_REFERENCE }
            shallowSize + (sizeOfOneElement * countOfNonNullElements)
          } else {
            shallowSize
          }
        } else {
          heapObject.readByteSize()
        }
      }
      // Number of elements * primitive type size
      is HeapPrimitiveArray -> heapObject.readByteSize()
      // This is probably way off but is a cheap approximation.
      is HeapClass -> heapObject.recordSize
    }
  }
}

package shark.internal

import okio.ByteString.Companion.encodeUtf8

internal fun String.lastSegment(segmentingChar: Char): String {
  val separator = lastIndexOf(segmentingChar)
  return if (separator == -1) this else this.substring(separator + 1)
}

internal fun String.createSHA1Hash(): String = encodeUtf8().sha1().hex()

package leakcanary.internal.activity

import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
import android.os.Build
import android.view.View
import android.widget.Toast
import com.squareup.leakcanary.core.R
import leakcanary.internal.HeapAnalyzerService
import leakcanary.internal.InternalLeakCanary
import leakcanary.internal.activity.db.Db
import leakcanary.internal.activity.screen.AboutScreen
import leakcanary.internal.activity.screen.HeapDumpsScreen
import leakcanary.internal.activity.screen.LeaksScreen
import leakcanary.internal.navigation.NavigatingActivity
import leakcanary.internal.navigation.Screen
import shark.SharkLog
import java.io.FileInputStream
import java.io.IOException

internal class LeakActivity : NavigatingActivity() {

  private val leaksButton by lazy {
    findViewById<View>(R.id.leak_canary_navigation_button_leaks)
  }

  private val leaksButtonIconView by lazy {
    findViewById<View>(R.id.leak_canary_navigation_button_leaks_icon)
  }

  private val heapDumpsButton by lazy {
    findViewById<View>(R.id.leak_canary_navigation_button_heap_dumps)
  }

  private val heapDumpsButtonIconView by lazy {
    findViewById<View>(R.id.leak_canary_navigation_button_heap_dumps_icon)
  }

  private val aboutButton by lazy {
    findViewById<View>(R.id.leak_canary_navigation_button_about)
  }

  private val aboutButtonIconView by lazy {
    findViewById<View>(R.id.leak_canary_navigation_button_about_icon)
  }

  private val bottomNavigationBar by lazy {
    findViewById<View>(R.id.leak_canary_bottom_navigation_bar)
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.leak_canary_leak_activity)

    installNavigation(savedInstanceState, findViewById(R.id.leak_canary_main_container))

    leaksButton.setOnClickListener { resetTo(LeaksScreen()) }
    heapDumpsButton.setOnClickListener { resetTo(HeapDumpsScreen()) }
    aboutButton.setOnClickListener { resetTo(AboutScreen()) }

    handleViewHprof(intent)
  }

  private fun handleViewHprof(intent: Intent?) {
    if (intent?.action != Intent.ACTION_VIEW) return
    val uri = intent.data ?: return
    if (uri.lastPathSegment?.endsWith(".hprof") != true) {
      Toast.makeText(this, getString(R.string.leak_canary_import_unsupported_file_extension, uri.lastPathSegment), Toast.LENGTH_LONG).show()
      return
    }
    resetTo(HeapDumpsScreen())
    AsyncTask.THREAD_POOL_EXECUTOR.execute {
      importHprof(uri)
    }
  }

  override fun onNewScreen(screen: Screen) {
    when (screen) {
      is LeaksScreen -> {
        bottomNavigationBar.visibility = View.VISIBLE
        leaksButton.isSelected = true
        leaksButtonIconView.alpha = 1.0f
        heapDumpsButton.isSelected = false
        heapDumpsButtonIconView.alpha = 0.4f
        aboutButton.isSelected = false
        aboutButtonIconView.alpha = 0.4f
      }
      is HeapDumpsScreen -> {
        bottomNavigationBar.visibility = View.VISIBLE
        leaksButton.isSelected = false
        leaksButtonIconView.alpha = 0.4f
        heapDumpsButton.isSelected = true
        heapDumpsButtonIconView.alpha = 1.0f
        aboutButton.isSelected = false
        aboutButtonIconView.alpha = 0.4f
      }
      is AboutScreen -> {
        bottomNavigationBar.visibility = View.VISIBLE
        leaksButton.isSelected = false
        leaksButtonIconView.alpha = 0.4f
        heapDumpsButton.isSelected = false
        heapDumpsButtonIconView.alpha = 0.4f
        aboutButton.isSelected = true
        aboutButtonIconView.alpha = 1.0f
      }
      else -> {
        bottomNavigationBar.visibility = View.GONE
      }
    }
  }

  override fun getLauncherScreen(): Screen {
    return LeaksScreen()
  }

  fun requestImportHprof() {
    val requestFileIntent = Intent(Intent.ACTION_GET_CONTENT).apply {
      type = "*/*"
      addCategory(Intent.CATEGORY_OPENABLE)
    }

    val chooserIntent = Intent.createChooser(
      requestFileIntent, resources.getString(R.string.leak_canary_import_from_title)
    )
    startActivityForResult(chooserIntent, FILE_REQUEST_CODE)
  }

  override fun onActivityResult(
    requestCode: Int,
    resultCode: Int,
    returnIntent: Intent?
  ) {
    SharkLog.d {
      "Got activity result with requestCode=$requestCode resultCode=$resultCode returnIntent=$returnIntent"
    }
    if (requestCode == FILE_REQUEST_CODE && resultCode == RESULT_OK && returnIntent != null) {
      returnIntent.data?.let { fileUri ->
        AsyncTask.THREAD_POOL_EXECUTOR.execute {
          importHprof(fileUri)
        }
      }
    }
  }

  private fun importHprof(fileUri: Uri) {
    try {
      contentResolver.openFileDescriptor(fileUri, "r")
        ?.fileDescriptor?.let { fileDescriptor ->
          val inputStream = FileInputStream(fileDescriptor)
          InternalLeakCanary.createLeakDirectoryProvider(this)
            .newHeapDumpFile()
            ?.let { target ->
              inputStream.use { input ->
                target.outputStream()
                  .use { output ->
                    input.copyTo(output, DEFAULT_BUFFER_SIZE)
                  }
              }
              HeapAnalyzerService.runAnalysis(this, target, heapDumpReason = "Imported by user")
            }
        }
    } catch (e: IOException) {
      SharkLog.d(e) { "Could not imported Hprof file" }
    }
  }

  override fun onDestroy() {
    super.onDestroy()
    if (!isChangingConfigurations) {
      Db.closeDatabase()
    }
  }

  override fun setTheme(resid: Int) {
    // We don't want this to be called with an incompatible theme.
    // This could happen if you implement runtime switching of themes
    // using ActivityLifecycleCallbacks.
    if (resid != R.style.leak_canary_LeakCanary_Base) {
      return
    }
    super.setTheme(resid)
  }

  companion object {
    private const val FILE_REQUEST_CODE = 0

    fun createPendingIntent(
      context: Context,
      screens: ArrayList<Screen>
    ): PendingIntent {
      val intent = Intent(context, LeakActivity::class.java)
      intent.putExtra("screens", screens)
      intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
      val flags = if (Build.VERSION.SDK_INT >= 23) {
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
      } else {
        PendingIntent.FLAG_UPDATE_CURRENT
      }
      return PendingIntent.getActivity(context, 1, intent, flags)
    }

    fun createIntent(context: Context): Intent {
      val intent = Intent(context, LeakActivity::class.java)
      intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
      return intent
    }
  }
}

package leakcanary.internal.activity

import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.AsyncTask
import android.os.Build
import android.view.View
import android.widget.Toast
import com.squareup.leakcanary.core.BuildConfig
import com.squareup.leakcanary.core.R
import leakcanary.internal.LeakCanaryFileProvider
import leakcanary.internal.navigation.activity
import shark.HeapAnalysisFailure
import java.io.File

internal fun View.share(content: String) {
  val intent = Intent(Intent.ACTION_SEND)
  intent.type = "text/plain"
  intent.putExtra(Intent.EXTRA_TEXT, content)
  activity.startActivity(
    Intent.createChooser(intent, resources.getString(R.string.leak_canary_share_with))
  )
}

@SuppressLint("SetWorldReadable")
internal fun View.shareHeapDump(heapDumpFile: File) {
  AsyncTask.SERIAL_EXECUTOR.execute {
    heapDumpFile.setReadable(true, false)
    val heapDumpUri = LeakCanaryFileProvider.getUriForFile(
      activity,
      "com.squareup.leakcanary.fileprovider." + activity.packageName,
      heapDumpFile
    )
    activity.runOnUiThread { startShareIntentChooser(heapDumpUri) }
  }
}

private fun View.startShareIntentChooser(uri: Uri) {
  val intent = Intent(Intent.ACTION_SEND)
  intent.type = "application/octet-stream"
  intent.putExtra(Intent.EXTRA_STREAM, uri)
  activity.startActivity(
    Intent.createChooser(intent, resources.getString(R.string.leak_canary_share_with))
  )
}

internal fun View.shareToStackOverflow(content: String) {
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
  // AsyncTask was needed here due to setPrimaryClip making a disk write which
  // violated StrictMode if on the main thread
  AsyncTask.execute {
    clipboard.setPrimaryClip(
      ClipData.newPlainText(
        context.getString(R.string.leak_canary_leak_clipdata_label),
        "```\n$content```"
      )
    )
  }
  Toast.makeText(context, R.string.leak_canary_leak_copied, Toast.LENGTH_LONG)
    .show()
  val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(STACKOVERFLOW_QUESTION_URL))
  try {
    activity.startActivity(browserIntent)
  } catch (e: ActivityNotFoundException) {
    Toast.makeText(context, R.string.leak_canary_leak_missing_browser_error, Toast.LENGTH_LONG)
      .show();
  }
}

internal fun View.shareToGitHubIssue(failure: HeapAnalysisFailure) {
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
  // AsyncTask was needed here due to setPrimaryClip making a disk write which
  // violated StrictMode if on the main thread
  AsyncTask.execute {
    clipboard.setPrimaryClip(
      ClipData.newPlainText(
        context.getString(R.string.leak_canary_failure_clipdata_label),
        """```
          |${failure.exception}
          |Build.VERSION.SDK_INT: ${Build.VERSION.SDK_INT}
          |Build.MANUFACTURER: ${Build.MANUFACTURER}
          |LeakCanary version: ${BuildConfig.LIBRARY_VERSION}
          |Analysis duration: ${failure.analysisDurationMillis} ms
          |Heap dump file path: ${failure.heapDumpFile.absolutePath}
          |Heap dump timestamp: ${failure.createdAtTimeMillis}
          |```
        """.trimMargin()
      )
    )
  }
  Toast.makeText(context, R.string.leak_canary_failure_copied, Toast.LENGTH_LONG)
    .show()
  val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(NEW_ISSUE_URL))
  try {
    activity.startActivity(browserIntent)
  } catch (e: ActivityNotFoundException) {
    Toast.makeText(context, R.string.leak_canary_leak_missing_browser_error, Toast.LENGTH_LONG)
      .show();
  }
}

private const val STACKOVERFLOW_QUESTION_URL =
  "http://stackoverflow.com/questions/ask?guided=false&tags=leakcanary"

private const val NEW_ISSUE_URL =
  "https://github.com/square/leakcanary/issues/new?labels=type%3A+bug&template=2-bug.md"

@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "NOTHING_TO_INLINE")
@file:JvmName("leakcanary-android-core_Friendly")

package leakcanary.internal.friendly

internal inline val mainHandler
  get() = leakcanary.internal.mainHandler

internal inline fun checkMainThread() = leakcanary.internal.checkMainThread()

internal inline fun checkNotMainThread() = leakcanary.internal.checkNotMainThread()

internal inline fun <reified T : Any> noOpDelegate(): T = leakcanary.internal.noOpDelegate()

internal inline fun measureDurationMillis(block: () -> Unit) =
  leakcanary.internal.measureDurationMillis(block)

package leakcanary.internal.navigation

import android.os.Parcel
import android.os.Parcelable
import android.util.SparseArray
import android.view.View

internal class BackstackFrame : Parcelable {

  val screen: Screen
  private val viewState: SparseArray<Parcelable>?

  private constructor(
    source: Parcel
  ) {
    this.screen = source.readSerializable() as Screen
    @Suppress("UNCHECKED_CAST")
    this.viewState = source.readSparseArray<Parcelable>(javaClass.classLoader)
  }

  constructor(
    screen: Screen
  ) {
    this.screen = screen
    viewState = null
  }

  constructor(
    screen: Screen,
    view: View
  ) {
    this.screen = screen
    viewState = SparseArray()
    view.saveHierarchyState(viewState)
  }

  fun restore(view: View) {
    if (viewState != null) {
      view.restoreHierarchyState(viewState)
    }
  }

  override fun describeContents() = 0

  @Suppress("UNCHECKED_CAST")
  override fun writeToParcel(
    dest: Parcel,
    flags: Int
  ) {
    dest.writeSerializable(screen)
    dest.writeSparseArray(viewState as SparseArray<Any>?)
  }

  companion object {
    @Suppress("UNCHECKED_CAST")
    @JvmField val CREATOR = object : Parcelable.Creator<BackstackFrame> {
      override fun createFromParcel(source: Parcel): BackstackFrame {
        return BackstackFrame(source)
      }

      override fun newArray(size: Int): Array<BackstackFrame?> {
        return arrayOfNulls(size)
      }
    }
  }
}

package leakcanary.internal.navigation

import android.app.Activity
import android.content.Intent
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.os.Parcelable
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnimationUtils.loadAnimation
import com.squareup.leakcanary.core.R

/**
 * A simple backstack navigating activity
 */
internal abstract class NavigatingActivity : Activity() {

  private lateinit var backstack: ArrayList<BackstackFrame>
  private lateinit var currentScreen: Screen

  private lateinit var container: ViewGroup
  private lateinit var currentView: View

  var onCreateOptionsMenu = NO_MENU

  fun installNavigation(
    savedInstanceState: Bundle?,
    container: ViewGroup
  ) {
    this.container = container

    if (savedInstanceState == null) {
      backstack = ArrayList()
      currentScreen = if (intent.hasExtra("screens")) {
        @Suppress("UNCHECKED_CAST")
        val screens = intent.getSerializableExtra("screens") as List<Screen>
        screens.dropLast(1)
          .forEach { screen ->
            backstack.add(BackstackFrame(screen))
          }
        screens.last()
      } else {
        getLauncherScreen()
      }
    } else {
      currentScreen = savedInstanceState.getSerializable("currentScreen") as Screen
      @Suppress("UNCHECKED_CAST")
      backstack = savedInstanceState.getParcelableArrayList<Parcelable>(
        "backstack"
      ) as ArrayList<BackstackFrame>
    }
    currentView = currentScreen.createView(container)
    container.addView(currentView)

    actionBar?.run {
      setHomeButtonEnabled(true)
      setDisplayHomeAsUpEnabled(true)
    }
    screenUpdated()
  }

  override fun onNewIntent(intent: Intent) {
    if (intent.hasExtra("screens")) {
      @Suppress("UNCHECKED_CAST")
      val screens = intent.getSerializableExtra("screens") as List<Screen>
      goTo(intent.getSerializableExtra("screen") as Screen)
      backstack.clear()
      screens.dropLast(1)
        .forEach { screen ->
          backstack.add(BackstackFrame(screen))
        }
    }
  }

  open fun getLauncherScreen(): Screen {
    TODO("Launcher activities should override getLauncherScreen()")
  }

  public override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putSerializable("currentScreen", currentScreen)
    outState.putParcelableArrayList("backstack", backstack)
  }

  override fun onBackPressed() {
    if (backstack.size > 0) {
      goBack()
      return
    }
    super.onBackPressed()
  }

  fun resetTo(screen: Screen) {
    onCreateOptionsMenu = NO_MENU

    currentView.startAnimation(loadAnimation(this, R.anim.leak_canary_exit_alpha))
    container.removeView(currentView)
    currentView.notifyScreenExiting()

    backstack.clear()

    currentScreen = screen
    currentView = currentScreen.createView(container)
    currentView.startAnimation(loadAnimation(this, R.anim.leak_canary_enter_alpha))
    container.addView(currentView)

    screenUpdated()
  }

  fun goTo(screen: Screen) {
    onCreateOptionsMenu = NO_MENU

    currentView.startAnimation(loadAnimation(this, R.anim.leak_canary_exit_forward))
    container.removeView(currentView)
    currentView.notifyScreenExiting()
    val backstackFrame = BackstackFrame(currentScreen, currentView)
    backstack.add(backstackFrame)

    currentScreen = screen
    currentView = currentScreen.createView(container)
    currentView.startAnimation(loadAnimation(this, R.anim.leak_canary_enter_forward))
    container.addView(currentView)

    screenUpdated()
  }

  fun refreshCurrentScreen() {
    onCreateOptionsMenu = NO_MENU
    container.removeView(currentView)
    currentView.notifyScreenExiting()
    currentView = currentScreen.createView(container)
    container.addView(currentView)

    screenUpdated()
  }

  fun goBack() {
    onCreateOptionsMenu = NO_MENU

    currentView.startAnimation(loadAnimation(this, R.anim.leak_canary_exit_backward))
    container.removeView(currentView)
    currentView.notifyScreenExiting()

    val latest = backstack.removeAt(backstack.size - 1)
    currentScreen = latest.screen
    currentView = currentScreen.createView(container)
    currentView.startAnimation(loadAnimation(this, R.anim.leak_canary_enter_backward))
    container.addView(currentView, 0)
    latest.restore(currentView)

    screenUpdated()
  }

  private fun screenUpdated() {
    invalidateOptionsMenu()
    if (SDK_INT >= 18) {
      actionBar?.run {
        val goBack = backstack.size > 0
        val indicator = if (goBack) 0 else android.R.drawable.ic_menu_close_clear_cancel
        setHomeAsUpIndicator(indicator)
      }
    }
    onNewScreen(currentScreen)
  }

  protected open fun onNewScreen(screen: Screen) {
  }

  override fun onCreateOptionsMenu(menu: Menu): Boolean {
    onCreateOptionsMenu.invoke(menu)
    return true
  }

  override fun onOptionsItemSelected(item: MenuItem): Boolean =
    when (item.itemId) {
      android.R.id.home -> {
        onBackPressed()
        true
      }
      else -> super.onOptionsItemSelected(item)
    }

  override fun onDestroy() {
    super.onDestroy()
    currentView.notifyScreenExiting()
  }

  companion object {
    val NO_MENU: ((Menu) -> Unit) = {}
  }
}

package leakcanary.internal.navigation

import android.view.View
import android.view.ViewGroup
import java.io.Serializable

/**
 * Replaces Fragments, MVP, MVC, MVVM, MVMVMVM and everything else in just one tiny class.
 * A screen is a location to go to, and it can build a view to display.
 */
internal abstract class Screen : Serializable {

  abstract fun createView(container: ViewGroup): View
}

package leakcanary.internal.navigation

import android.app.Activity
import android.content.Context
import android.os.Build.VERSION
import android.view.LayoutInflater
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import com.squareup.leakcanary.core.R

internal fun ViewGroup.inflate(layoutResId: Int) = LayoutInflater.from(context)
  .inflate(layoutResId, this, false)!!

internal val View.activity
  get() = context as Activity

@Suppress("UNCHECKED_CAST")
internal fun <T : Activity> View.activity() = context as T

internal fun View.onCreateOptionsMenu(onCreateOptionsMenu: (Menu) -> Unit) {
  activity<NavigatingActivity>().onCreateOptionsMenu = onCreateOptionsMenu
  activity.invalidateOptionsMenu()
}

internal fun View.goTo(screen: Screen) {
  activity<NavigatingActivity>().goTo(screen)
}

internal fun View.goBack() {
  activity<NavigatingActivity>().goBack()
}

internal fun Context.getColorCompat(id: Int): Int {
  return if (VERSION.SDK_INT >= 23) {
    getColor(id)
  } else {
    resources.getColor(id)
  }
}

internal fun View.onScreenExiting(block: () -> Unit) {
  @Suppress("UNCHECKED_CAST")
  var callbacks = getTag(R.id.leak_canary_notification_on_screen_exit) as MutableList<() -> Unit>?
  if (callbacks == null) {
    callbacks = mutableListOf()
    setTag(R.id.leak_canary_notification_on_screen_exit, callbacks)
  }
  callbacks.add(block)
}

internal fun View.notifyScreenExiting() {
  @Suppress("UNCHECKED_CAST")
  val callbacks = getTag(R.id.leak_canary_notification_on_screen_exit)
    as MutableList<() -> Unit>?
  callbacks?.forEach { it.invoke() }
}

package leakcanary.internal.tv

import android.app.Application
import android.os.Handler
import android.os.Looper
import com.squareup.leakcanary.core.R
import leakcanary.LeakCanary
import leakcanary.internal.InternalLeakCanary
import leakcanary.internal.OnRetainInstanceListener
import leakcanary.internal.RetainInstanceEvent
import leakcanary.internal.RetainInstanceEvent.CountChanged.BelowThreshold
import leakcanary.internal.RetainInstanceEvent.CountChanged.DumpHappenedRecently
import leakcanary.internal.RetainInstanceEvent.CountChanged.DumpingDisabled
import leakcanary.internal.RetainInstanceEvent.NoMoreObjects
import leakcanary.internal.friendly.mainHandler
import shark.SharkLog

/**
 * [OnRetainInstanceListener] implementation for Android TV devices which displays a helpful
 * Toast message on current state of retained instances.
 * It will notify user when heap dump is going to happen, so that users who set the
 * [LeakCanary.Config.retainedVisibleThreshold] to any value other than 1 will be aware of retain
 * instances changes. Additionally, it notifies about debugger being attached and whether heap dump
 * happened recently.
 */
internal class TvOnRetainInstanceListener(private val application: Application) :
  OnRetainInstanceListener {

  override fun onEvent(event: RetainInstanceEvent) {
    val message = when (event) {
      NoMoreObjects -> {
        application.getString(R.string.leak_canary_notification_no_retained_object_title)
      }
      is BelowThreshold -> {
        application.getString(
          R.string.leak_canary_tv_toast_retained_objects,
          event.retainedCount,
          LeakCanary.config.retainedVisibleThreshold
        )
      }
      is DumpingDisabled -> {
        event.reason
      }
      is DumpHappenedRecently -> {
        application.getString(R.string.leak_canary_notification_retained_dump_wait)
      }
    }
    SharkLog.d { message }

    mainHandler.post {
      val resumedActivity = InternalLeakCanary.resumedActivity ?: return@post
      TvToast.makeText(resumedActivity, message).show()
    }
  }
}

package leakcanary.internal.tv

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.widget.TextView
import android.widget.Toast
import com.squareup.leakcanary.core.R

/**
 * Toast helper for Android TV preconfigured with LeakCanary icon.
 */
internal object TvToast {

  /**
   * Make an Android TV toast.
   * Don't forget to call [Toast.show] to display the toast!
   * @param activity Currently resumed [Activity] to display toast on. Note that it's not [Context]
   *        to prevent passing application context that could lead to crashes on older platforms.
   * @param text The text to show. Can be formatted text.
   */
  @SuppressLint("ShowToast")
  fun makeText(
    activity: Activity,
    text: CharSequence
  ): Toast {
    val toast: Toast = Toast.makeText(activity, text, Toast.LENGTH_LONG)
    val textView = toast.view.findViewById<TextView>(android.R.id.message)
    textView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.leak_canary_icon, 0, 0, 0)
    textView.compoundDrawablePadding =
      activity.resources.getDimensionPixelSize(R.dimen.leak_canary_toast_icon_tv_padding)
    return toast
  }
}

package leakcanary.internal.utils

import kotlin.math.ln
import kotlin.math.pow

// https://stackoverflow.com/a/3758880
internal fun humanReadableByteCount(
  bytes: Long,
  si: Boolean
): String {
  val unit = if (si) 1000 else 1024
  if (bytes < unit) return "$bytes B"
  val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
  val pre = (if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i"
  return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
}

package leakcanary.internal.utils

internal infix fun <A, B, C> Pair<A, B>.to(that: C): Triple<A, B, C> = Triple(first, second, that)

@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
@file:JvmName("leakcanary-android-instrumentation_Friendly")
package leakcanary.internal.friendly

internal inline fun <reified T : Any> noOpDelegate(): T = leakcanary.internal.noOpDelegate()

@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "NOTHING_TO_INLINE")
@file:JvmName("leakcanary-android-release_Friendly")

package leakcanary.internal.friendly

internal inline val mainHandler
  get() = leakcanary.internal.mainHandler

internal inline fun checkMainThread() = leakcanary.internal.checkMainThread()

internal inline fun <reified T : Any> noOpDelegate(): T = leakcanary.internal.noOpDelegate()

internal inline fun measureDurationMillis(block: () -> Unit) =
  leakcanary.internal.measureDurationMillis(block)

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.example.leakcanary

import android.app.Application
import android.app.Dialog
import android.app.Service
import android.os.StrictMode
import android.view.View
import leakcanary.AppWatcher
import leakcanary.ReachabilityWatcher
import leakcanary.RootViewWatcher

open class ExampleApplication : Application() {
  val leakedViews = mutableListOf<View>()
  val leakedDialogs = mutableListOf<Dialog>()
  val leakedServices = mutableListOf<Service>()

  override fun onCreate() {
    super.onCreate()
    enabledStrictMode()
  }

  private fun enabledStrictMode() {
    StrictMode.setThreadPolicy(
      StrictMode.ThreadPolicy.Builder()
        .detectAll()
        .penaltyLog()
        .penaltyDeath()
        .build()
    )
  }
}

package com.example.leakcanary

import android.app.Service
import android.content.Intent
import android.os.IBinder

class LeakingService : Service() {

  override fun onCreate() {
    super.onCreate()
    (application as ExampleApplication).leakedServices += this
    stopSelf()
  }

  override fun onBind(intent: Intent?): IBinder? {
    return null
  }
}

package com.example.leakcanary

import android.view.View

object LeakingSingleton {
  val leakedViews = mutableListOf<View>()
}

package com.example.leakcanary

import android.view.View

class LeakingThread : Thread() {

  val leakedViews = mutableListOf<View>()

  init {
    name = "Leaking thread"
    start()
  }

  override fun run() {
    synchronized(obj) {
      obj.wait()
    }
  }

  companion object {
    private val obj = Object()
    val thread = LeakingThread()
  }
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.example.leakcanary

import android.app.Activity
import android.app.AlertDialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.os.Handler
import android.os.StatFs
import android.os.SystemClock
import android.view.View
import android.widget.Button
import shark.SharkLog
import java.util.concurrent.atomic.AtomicReference
import kotlin.random.Random

class MainActivity : Activity() {

  private var leakyReceiver = false

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_activity)

    val app = application as ExampleApplication
    findViewById<Button>(R.id.recreate_activity_button).setOnClickListener { recreate() }
    findViewById<Button>(R.id.leak_activity_button).setOnClickListener {
      val leakedView = findViewById<View>(R.id.helper_text)
      when (Random.nextInt(4)) {
        // Leak from application class
        0 -> app.leakedViews.add(leakedView)
        // Leak from Kotlin object singleton
        1 -> LeakingSingleton.leakedViews.add(leakedView)
        2 -> {
          // Leak from local variable on thread
          val ref = AtomicReference(this)
          val thread = Thread {
            val activity = ref.get()
            ref.set(null)
            while (true) {
              print(activity)
              SystemClock.sleep(1000)
            }
          }
          thread.name = "Leaking local variables"
          thread.start()
        }
        // Leak from thread fields
        else -> LeakingThread.thread.leakedViews.add(leakedView)
      }
    }
    findViewById<Button>(R.id.show_dialog_button).setOnClickListener {
      AlertDialog.Builder(this)
        .setTitle("Leaky dialog")
        .setPositiveButton("Dismiss and leak dialog") { dialog, _ ->
          app.leakedDialogs += dialog as AlertDialog
        }
        .show()
    }
    findViewById<Button>(R.id.start_service_button).setOnClickListener {
      startService(Intent(this, LeakingService::class.java))
    }
    findViewById<Button>(R.id.leak_receiver_button).setOnClickListener {
      leakyReceiver = true
      recreate()
    }
  }

  class NoOpBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(
      context: Context,
      intent: Intent
    ) = Unit
  }

  override fun onDestroy() {
    super.onDestroy()
    if (leakyReceiver) {
      Handler().postDelayed({
        registerReceiver(NoOpBroadcastReceiver(), IntentFilter())
      }, 500)
    }
  }
}

package com.example.leakcanary

import android.os.Process.THREAD_PRIORITY_BACKGROUND
import android.util.Log
import leakcanary.BackgroundTrigger
import leakcanary.HeapAnalysisClient
import leakcanary.HeapAnalysisConfig
import leakcanary.HeapAnalysisJob.Result
import leakcanary.LogcatSharkLog
import leakcanary.LogcatSharkLog.Companion
import leakcanary.ScreenOffTrigger
import shark.SharkLog
import shark.SharkLog.Logger
import java.util.concurrent.Executors

class ReleaseExampleApplication : ExampleApplication() {

  override fun onCreate() {
    super.onCreate()
    // Useful to debug in release builds. Don't use in real builds.
    LogcatSharkLog.install()

    val analysisClient = HeapAnalysisClient(
      heapDumpDirectoryProvider = { cacheDir },
      config = HeapAnalysisConfig(),
      interceptors = HeapAnalysisClient.defaultInterceptors(this)
    )

    val analysisExecutor = Executors.newSingleThreadExecutor {
      Thread {
        android.os.Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND)
        it.run()
      }.apply {
        name = "Heap analysis executor"
      }
    }
    analysisExecutor.execute {
      analysisClient.deleteHeapDumpFiles()
    }
    val analysisCallback: (Result) -> Unit = { result ->
      SharkLog.d { "$result" }
    }
    BackgroundTrigger(
      application = this,
      analysisClient = analysisClient,
      analysisExecutor = analysisExecutor,
      analysisCallback = analysisCallback
    ).start()

    ScreenOffTrigger(
      application = this,
      analysisClient = analysisClient,
      analysisExecutor = analysisExecutor,
      analysisCallback = analysisCallback
    ).start()
  }
}

@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "NOTHING_TO_INLINE")
@file:JvmName("leakcanary-object-watcher-android_Friendly")

package leakcanary.internal.friendly

internal inline val mainHandler
  get() = leakcanary.internal.mainHandler

internal inline fun checkMainThread() = leakcanary.internal.checkMainThread()

internal inline fun <reified T : Any> noOpDelegate(): T = leakcanary.internal.noOpDelegate()

@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "NOTHING_TO_INLINE")
@file:JvmName("plumber-android_Friendly")

package leakcanary.internal.friendly

internal inline fun checkMainThread() = leakcanary.internal.checkMainThread()

internal inline fun <reified T : Any> noOpDelegate(): T = leakcanary.internal.noOpDelegate()

package shark.internal.aosp

internal interface ByteArrayComparator {

  /**
   * Indexes are divided by entrySize
   */
  fun compare(
    entrySize: Int,
    o1Array: ByteArray,
    o1Index: Int,
    o2Array: ByteArray,
    o2Index: Int
  ): Int
}

/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package shark.internal.aosp

/*
This is TimSort.java from AOSP (Jelly Bean MR2, Apache 2 license), converted to Kotlin and adapted
to work with byte array chunks. The passed in byte array is virtually divided into entries of a
fixed number of bytes N. Each entry is compared by a custom comparator.

 Copied from https://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/java/util/TimSort.java
*/

/**
 * A stable, adaptive, iterative mergesort that requires far fewer than
 * n lg(n) comparisons when running on partially sorted arrays, while
 * offering performance comparable to a traditional mergesort when run
 * on random arrays.  Like all proper mergesorts, this sort is stable and
 * runs O(n log n) time (worst case).  In the worst case, this sort requires
 * temporary storage space for n/2 object references; in the best case,
 * it requires only a small constant amount of space.
 *
 * This implementation was adapted from Tim Peters's list sort for
 * Python, which is described in detail here:
 *
 * http://svn.python.org/projects/python/trunk/Objects/listsort.txt
 *
 * Tim's C code may be found here:
 *
 * http://svn.python.org/projects/python/trunk/Objects/listobject.c
 *
 * The underlying techniques are described in this paper (and may have
 * even earlier origins):
 *
 * "Optimistic Sorting and Information Theoretic Complexity"
 * Peter McIlroy
 * SODA (Fourth Annual ACM-SIAM Symposium on Discrete Algorithms),
 * pp 467-474, Austin, Texas, 25-27 January 1993.
 *
 * While the API to this class consists solely of static methods, it is
 * (privately) instantiable; a TimSort instance holds the state of an ongoing
 * sort, assuming the input array is large enough to warrant the full-blown
 * TimSort. Small arrays are sorted in place, using a binary insertion sort.
 */
@Suppress("detekt.complexity", "detekt.style")
internal class ByteArrayTimSort
/**
 * Creates a TimSort instance to maintain the state of an ongoing sort.
 *
 * @param a the array to be sorted
 * @param c the comparator to determine the order of the sort
 */
private constructor(
  /**
   * The array being sorted.
   */
  private val a: ByteArray,
  /**
   * The comparator for this sort.
   */
  private val c: ByteArrayComparator,

  private val entrySize: Int
) {
  /**
   * This controls when we get *into* galloping mode.  It is initialized
   * to MIN_GALLOP.  The mergeLo and mergeHi methods nudge it higher for
   * random data, and lower for highly structured data.
   */
  private var minGallop = MIN_GALLOP

  /**
   * Temp storage for merges.
   */
  private var tmp: ByteArray? = null // Actual runtime type will be Object[], regardless of T

  /**
   * A stack of pending runs yet to be merged.  Run i starts at
   * address base[i] and extends for len[i] elements.  It's always
   * true (so long as the indices are in bounds) that:
   *
   * runBase[i] + runLen[i] == runBase[i + 1]
   *
   * so we could cut the storage for this, but it's a minor amount,
   * and keeping all the info explicit simplifies the code.
   */
  private var stackSize = 0  // Number of pending runs on stack
  private val runBase: IntArray
  private val runLen: IntArray

  init {
    // Allocate temp storage (which may be increased later if necessary)
    val len = a.size / entrySize
    val newArray = ByteArray(
      entrySize *
        if (len < 2 * INITIAL_TMP_STORAGE_LENGTH)
          len.ushr(1)
        else
          INITIAL_TMP_STORAGE_LENGTH
    )
    tmp = newArray
    /*
         * Allocate runs-to-be-merged stack (which cannot be expanded).  The
         * stack length requirements are described in listsort.txt.  The C
         * version always uses the same stack length (85), but this was
         * measured to be too expensive when sorting "mid-sized" arrays (e.g.,
         * 100 elements) in Java.  Therefore, we use smaller (but sufficiently
         * large) stack lengths for smaller arrays.  The "magic numbers" in the
         * computation below must be changed if MIN_MERGE is decreased.  See
         * the MIN_MERGE declaration above for more information.
         */
    val stackLen = if (len < 120)
      5
    else if (len < 1542)
      10
    else if (len < 119151) 19 else 40
    runBase = IntArray(stackLen)
    runLen = IntArray(stackLen)
  }

  /**
   * Pushes the specified run onto the pending-run stack.
   *
   * @param runBase index of the first element in the run
   * @param runLen  the number of elements in the run
   */
  private fun pushRun(
    runBase: Int,
    runLen: Int
  ) {
    this.runBase[stackSize] = runBase
    this.runLen[stackSize] = runLen
    stackSize++
  }

  /**
   * Examines the stack of runs waiting to be merged and merges adjacent runs
   * until the stack invariants are reestablished:
   *
   * 1. runLen[i - 3] > runLen[i - 2] + runLen[i - 1]
   * 2. runLen[i - 2] > runLen[i - 1]
   *
   * This method is called each time a new run is pushed onto the stack,
   * so the invariants are guaranteed to hold for i < stackSize upon
   * entry to the method.
   */
  // Fixed with http://www.envisage-project.eu/proving-android-java-and-python-sorting-algorithm-is-broken-and-how-to-fix-it/
  private fun mergeCollapse() {
    while (stackSize > 1) {
      var n = stackSize - 2
      if (n >= 1 && runLen[n - 1] <= runLen[n] + runLen[n + 1] || n >= 2 && runLen[n - 2] <= runLen[n] + runLen[n - 1]) {
        if (runLen[n - 1] < runLen[n + 1])
          n--
      } else if (runLen[n] > runLen[n + 1]) {
        break // Invariant is established
      }
      mergeAt(n)
    }
  }

  /**
   * Merges all runs on the stack until only one remains.  This method is
   * called once, to complete the sort.
   */
  private fun mergeForceCollapse() {
    while (stackSize > 1) {
      var n = stackSize - 2
      if (n > 0 && runLen[n - 1] < runLen[n + 1])
        n--
      mergeAt(n)
    }
  }

  /**
   * Merges the two runs at stack indices i and i+1.  Run i must be
   * the penultimate or antepenultimate run on the stack.  In other words,
   * i must be equal to stackSize-2 or stackSize-3.
   *
   * @param i stack index of the first of the two runs to merge
   */
  private fun mergeAt(i: Int) {
    if (DEBUG) assert(stackSize >= 2)
    if (DEBUG) assert(i >= 0)
    if (DEBUG) assert(i == stackSize - 2 || i == stackSize - 3)
    var base1 = runBase[i]
    var len1 = runLen[i]
    val base2 = runBase[i + 1]
    var len2 = runLen[i + 1]
    if (DEBUG) assert(len1 > 0 && len2 > 0)
    if (DEBUG) assert(base1 + len1 == base2)
    /*
         * Record the length of the combined runs; if i is the 3rd-last
         * run now, also slide over the last run (which isn't involved
         * in this merge).  The current run (i+1) goes away in any case.
         */
    runLen[i] = len1 + len2
    if (i == stackSize - 3) {
      runBase[i + 1] = runBase[i + 2]
      runLen[i + 1] = runLen[i + 2]
    }
    stackSize--
    /*
         * Find where the first element of run2 goes in run1. Prior elements
         * in run1 can be ignored (because they're already in place).
         */
    val k = gallopRight(a, base2, a, base1, len1, 0, entrySize, c)
    if (DEBUG) assert(k >= 0)
    base1 += k
    len1 -= k
    if (len1 == 0)
      return
    /*
         * Find where the last element of run1 goes in run2. Subsequent elements
         * in run2 can be ignored (because they're already in place).
         */
    len2 = gallopLeft(a, base1 + len1 - 1, a, base2, len2, len2 - 1, entrySize, c)
    if (DEBUG) assert(len2 >= 0)
    if (len2 == 0)
      return
    // Merge remaining runs, using tmp array with min(len1, len2) elements
    if (len1 <= len2)
      mergeLo(base1, len1, base2, len2)
    else
      mergeHi(base1, len1, base2, len2)
  }

  /**
   * Merges two adjacent runs in place, in a stable fashion.  The first
   * element of the first run must be greater than the first element of the
   * second run (a[base1] > a[base2]), and the last element of the first run
   * (a[base1 + len1-1]) must be greater than all elements of the second run.
   *
   * For performance, this method should be called only when len1 <= len2;
   * its twin, mergeHi should be called if len1 >= len2.  (Either method
   * may be called if len1 == len2.)
   *
   * @param base1 index of first element in first run to be merged
   * @param len1  length of first run to be merged (must be > 0)
   * @param base2 index of first element in second run to be merged
   * (must be aBase + aLen)
   * @param len2  length of second run to be merged (must be > 0)
   */
  private fun mergeLo(
    base1: Int,
    len1: Int,
    base2: Int,
    len2: Int
  ) {
    var len1 = len1
    var len2 = len2
    if (DEBUG) assert(len1 > 0 && len2 > 0 && base1 + len1 == base2)
    // Copy first run into temp array
    val a = this.a // For performance
    val entrySize = entrySize
    val tmp = ensureCapacity(len1)
    System.arraycopy(a, base1 * entrySize, tmp, 0, len1 * entrySize)
    var cursor1 = 0       // Indexes into tmp array
    var cursor2 = base2   // Indexes int a
    var dest = base1      // Indexes int a
    // Move first element of second run and deal with degenerate cases
    val destIndex = dest * entrySize
    val cursor2Index = cursor2 * entrySize
    for (i in 0 until entrySize) {
      a[destIndex + i] = a[cursor2Index + i]
    }
    dest++
    cursor2++

    if (--len2 == 0) {
      System.arraycopy(tmp, cursor1 * entrySize, a, dest * entrySize, len1 * entrySize)
      return
    }
    if (len1 == 1) {
      System.arraycopy(a, cursor2 * entrySize, a, dest * entrySize, len2 * entrySize)
      val destLen2Index = (dest + len2) * entrySize
      val cursor1Index = cursor1 * entrySize
      for (i in 0 until entrySize) {
        a[destLen2Index + i] = tmp[cursor1Index + i] // Last elt of run 1 to end of merge
      }
      return
    }
    val c = this.c  // Use local variable for performance
    var minGallop = this.minGallop    //  "    "       "     "      "
    outer@ while (true) {
      var count1 = 0 // Number of times in a row that first run won
      var count2 = 0 // Number of times in a row that second run won
      /*
       * Do the straightforward thing until (if ever) one run starts
       * winning consistently.
       */
      do {
        if (DEBUG) assert(len1 > 1 && len2 > 0)
        if (c.compare(entrySize, a, cursor2, tmp, cursor1) < 0) {
          val destIndex = dest * entrySize
          val cursor2Index = cursor2 * entrySize
          for (i in 0 until entrySize) {
            a[destIndex + i] = a[cursor2Index + i]
          }
          dest++
          cursor2++
          count2++
          count1 = 0
          if (--len2 == 0)
            break@outer
        } else {
          val destIndex = dest * entrySize
          val cursor1Index = cursor1 * entrySize
          for (i in 0 until entrySize) {
            a[destIndex + i] = tmp[cursor1Index + i]
          }
          dest++
          cursor1++
          count1++
          count2 = 0
          if (--len1 == 1)
            break@outer
        }
      } while (count1 or count2 < minGallop)
      /*
             * One run is winning so consistently that galloping may be a
             * huge win. So try that, and continue galloping until (if ever)
             * neither run appears to be winning consistently anymore.
             */
      do {
        if (DEBUG) assert(len1 > 1 && len2 > 0)
        count1 = gallopRight(a, cursor2, tmp, cursor1, len1, 0, entrySize, c)
        if (count1 != 0) {
          System.arraycopy(tmp, cursor1 * entrySize, a, dest * entrySize, count1 * entrySize)
          dest += count1
          cursor1 += count1
          len1 -= count1
          if (len1 <= 1)
          // len1 == 1 || len1 == 0
            break@outer
        }
        var destIndex = dest * entrySize
        val cursor2Index = cursor2 * entrySize
        for (i in 0 until entrySize) {
          a[destIndex + i] = a[cursor2Index + i]
        }
        dest++
        cursor2++
        if (--len2 == 0)
          break@outer
        count2 = gallopLeft(tmp, cursor1, a, cursor2, len2, 0, entrySize, c)
        if (count2 != 0) {
          System.arraycopy(a, cursor2 * entrySize, a, dest * entrySize, count2 * entrySize)
          dest += count2
          cursor2 += count2
          len2 -= count2
          if (len2 == 0)
            break@outer
        }
        destIndex = dest * entrySize
        val cursor1Index = cursor1 * entrySize
        for (i in 0 until entrySize) {
          a[destIndex + i] = tmp[cursor1Index + i]
        }
        dest++
        cursor1++
        if (--len1 == 1)
          break@outer
        minGallop--
      } while ((count1 >= MIN_GALLOP) or (count2 >= MIN_GALLOP))
      if (minGallop < 0)
        minGallop = 0
      minGallop += 2  // Penalize for leaving gallop mode
    }  // End of "outer" loop
    this.minGallop = if (minGallop < 1) 1 else minGallop  // Write back to field
    if (len1 == 1) {
      if (DEBUG) assert(len2 > 0)
      System.arraycopy(a, cursor2 * entrySize, a, dest * entrySize, len2 * entrySize)
      val destLen2Index = (dest + len2) * entrySize
      val cursor1Index = cursor1 * entrySize
      for (i in 0 until entrySize) {
        a[destLen2Index + i] = tmp[cursor1Index + i] //  Last elt of run 1 to end of merge
      }
    } else if (len1 == 0) {
      throw IllegalArgumentException(
        "Comparison method violates its general contract!"
      )
    } else {
      if (DEBUG) assert(len2 == 0)
      if (DEBUG) assert(len1 > 1)
      System.arraycopy(tmp, cursor1 * entrySize, a, dest * entrySize, len1 * entrySize)
    }
  }

  /**
   * Like mergeLo, except that this method should be called only if
   * len1 >= len2; mergeLo should be called if len1 <= len2.  (Either method
   * may be called if len1 == len2.)
   *
   * @param base1 index of first element in first run to be merged
   * @param len1  length of first run to be merged (must be > 0)
   * @param base2 index of first element in second run to be merged
   * (must be aBase + aLen)
   * @param len2  length of second run to be merged (must be > 0)
   */
  private fun mergeHi(
    base1: Int,
    len1: Int,
    base2: Int,
    len2: Int
  ) {
    var len1 = len1
    var len2 = len2
    if (DEBUG) assert(len1 > 0 && len2 > 0 && base1 + len1 == base2)
    // Copy second run into temp array
    val a = this.a // For performance
    val tmp = ensureCapacity(len2)
    val entrySize = entrySize
    System.arraycopy(a, base2 * entrySize, tmp, 0, len2 * entrySize)
    var cursor1 = base1 + len1 - 1  // Indexes into a
    var cursor2 = len2 - 1          // Indexes into tmp array
    var dest = base2 + len2 - 1     // Indexes into a
    // Move last element of first run and deal with degenerate cases
    var destIndex = dest * entrySize
    val cursor1Index = cursor1 * entrySize
    for (i in 0 until entrySize) {
      a[destIndex + i] = a[cursor1Index + i]
    }
    dest--
    cursor1--
    if (--len1 == 0) {
      System.arraycopy(tmp, 0, a, (dest - (len2 - 1)) * entrySize, len2 * entrySize)
      return
    }
    if (len2 == 1) {
      dest -= len1
      cursor1 -= len1
      System.arraycopy(a, (cursor1 + 1) * entrySize, a, (dest + 1) * entrySize, len1 * entrySize)
      val destIndex = dest * entrySize
      val cursor2Index = cursor2 * entrySize
      for (i in 0 until entrySize) {
        a[destIndex + i] = tmp[cursor2Index + i]
      }
      return
    }
    val c = this.c  // Use local variable for performance
    var minGallop = this.minGallop    //  "    "       "     "      "
    outer@ while (true) {
      var count1 = 0 // Number of times in a row that first run won
      var count2 = 0 // Number of times in a row that second run won
      /*
             * Do the straightforward thing until (if ever) one run
             * appears to win consistently.
             */
      do {
        if (DEBUG) assert(len1 > 0 && len2 > 1)
        if (c.compare(entrySize, tmp, cursor2, a, cursor1) < 0) {
          val destIndex = dest * entrySize
          val cursor1Index = cursor1 * entrySize
          for (i in 0 until entrySize) {
            a[destIndex + i] = a[cursor1Index + i]
          }
          dest--
          cursor1--
          count1++
          count2 = 0
          if (--len1 == 0)
            break@outer
        } else {
          val destIndex = dest * entrySize
          val cursor2Index = cursor2 * entrySize
          for (i in 0 until entrySize) {
            a[destIndex + i] = tmp[cursor2Index + i]
          }
          dest--
          cursor2--
          count2++
          count1 = 0
          if (--len2 == 1)
            break@outer
        }
      } while (count1 or count2 < minGallop)
      /*
             * One run is winning so consistently that galloping may be a
             * huge win. So try that, and continue galloping until (if ever)
             * neither run appears to be winning consistently anymore.
             */
      do {
        if (DEBUG) assert(len1 > 0 && len2 > 1)
        count1 = len1 - gallopRight(tmp, cursor2, a, base1, len1, len1 - 1, entrySize, c)
        if (count1 != 0) {
          dest -= count1
          cursor1 -= count1
          len1 -= count1
          System.arraycopy(
            a, (cursor1 + 1) * entrySize, a, (dest + 1) * entrySize, count1 * entrySize
          )
          if (len1 == 0)
            break@outer
        }
        destIndex = dest * entrySize
        val cursor2Index = cursor2 * entrySize
        for (i in 0 until entrySize) {
          a[destIndex + i] = tmp[cursor2Index + i]
        }
        dest--
        cursor2--
        if (--len2 == 1)
          break@outer
        count2 = len2 - gallopLeft(a, cursor1, tmp, 0, len2, len2 - 1, entrySize, c)
        if (count2 != 0) {
          dest -= count2
          cursor2 -= count2
          len2 -= count2
          System.arraycopy(
            tmp, (cursor2 + 1) * entrySize, a, (dest + 1) * entrySize, count2 * entrySize
          )
          if (len2 <= 1)
          // len2 == 1 || len2 == 0
            break@outer
        }
        val destIndex = dest * entrySize
        val cursor1Index = cursor1 * entrySize
        for (i in 0 until entrySize) {
          a[destIndex + i] = a[cursor1Index + i]
        }
        dest--
        cursor1--
        if (--len1 == 0)
          break@outer
        minGallop--
      } while ((count1 >= MIN_GALLOP) or (count2 >= MIN_GALLOP))
      if (minGallop < 0)
        minGallop = 0
      minGallop += 2  // Penalize for leaving gallop mode
    }  // End of "outer" loop
    this.minGallop = if (minGallop < 1) 1 else minGallop  // Write back to field
    if (len2 == 1) {
      if (DEBUG) assert(len1 > 0)
      dest -= len1
      cursor1 -= len1
      System.arraycopy(a, (cursor1 + 1) * entrySize, a, (dest + 1) * entrySize, len1 * entrySize)
      val destIndex = dest * entrySize
      val cursor2Index = cursor2 * entrySize
      for (i in 0 until entrySize) {
        a[destIndex + i] = tmp[cursor2Index + i] // Move first elt of run2 to front of merge
      }
    } else if (len2 == 0) {
      throw IllegalArgumentException(
        "Comparison method violates its general contract!"
      )
    } else {
      if (DEBUG) assert(len1 == 0)
      if (DEBUG) assert(len2 > 0)
      System.arraycopy(tmp, 0, a, (dest - (len2 - 1)) * entrySize, len2 * entrySize)
    }
  }

  /**
   * Ensures that the external array tmp has at least the specified
   * number of elements, increasing its size if necessary.  The size
   * increases exponentially to ensure amortized linear time complexity.
   *
   * @param minCapacity the minimum required capacity of the tmp array
   * @return tmp, whether or not it grew
   */
  private fun ensureCapacity(minCapacity: Int): ByteArray {
    if (tmp!!.size < minCapacity * entrySize) {
      // Compute smallest power of 2 > minCapacity
      var newSize = minCapacity
      newSize = newSize or (newSize shr 1)
      newSize = newSize or (newSize shr 2)
      newSize = newSize or (newSize shr 4)
      newSize = newSize or (newSize shr 8)
      newSize = newSize or (newSize shr 16)
      newSize++
      if (newSize < 0)
      // Not bloody likely!
        newSize = minCapacity
      else
        newSize = Math.min(newSize, (a.size / entrySize).ushr(1))
      val newArray = ByteArray(newSize * entrySize)
      tmp = newArray
    }
    return tmp!!
  }

  companion object {
    /**
     * This is the minimum sized sequence that will be merged.  Shorter
     * sequences will be lengthened by calling binarySort.  If the entire
     * array is less than this length, no merges will be performed.
     *
     * This constant should be a power of two.  It was 64 in Tim Peter's C
     * implementation, but 32 was empirically determined to work better in
     * this implementation.  In the unlikely event that you set this constant
     * to be a number that's not a power of two, you'll need to change the
     * [.minRunLength] computation.
     *
     * If you decrease this constant, you must change the stackLen
     * computation in the TimSort constructor, or you risk an
     * ArrayOutOfBounds exception.  See listsort.txt for a discussion
     * of the minimum stack length required as a function of the length
     * of the array being sorted and the minimum merge sequence length.
     */
    private val MIN_MERGE = 32

    /**
     * When we get into galloping mode, we stay there until both runs win less
     * often than MIN_GALLOP consecutive times.
     */
    private val MIN_GALLOP = 7

    /**
     * Maximum initial size of tmp array, which is used for merging.  The array
     * can grow to accommodate demand.
     *
     * Unlike Tim's original C version, we do not allocate this much storage
     * when sorting smaller arrays.  This change was required for performance.
     */
    private val INITIAL_TMP_STORAGE_LENGTH = 256

    /**
     * Asserts have been placed in if-statements for performace. To enable them,
     * set this field to true and enable them in VM with a command line flag.
     * If you modify this class, please do test the asserts!
     */
    private val DEBUG = false

    /*
     * The next two methods (which are package private and static) constitute
     * the entire API of this class.  Each of these methods obeys the contract
     * of the public method with the same signature in java.util.Arrays.
     */
    fun sort(
      a: ByteArray,
      entrySize: Int,
      c: ByteArrayComparator
    ) {
      sort(a, 0, a.size / entrySize, entrySize, c)
    }

    fun sort(
      a: ByteArray,
      lo: Int,
      hi: Int,
      entrySize: Int,
      c: ByteArrayComparator
    ) {
      var lo = lo
      checkStartAndEnd(a.size / entrySize, lo, hi)
      var nRemaining = hi - lo
      if (nRemaining < 2)
        return   // Arrays of size 0 and 1 are always sorted
      // If array is small, do a "mini-TimSort" with no merges
      if (nRemaining < MIN_MERGE) {
        val initRunLen = countRunAndMakeAscending(a, lo, hi, entrySize, c)
        binarySort(a, lo, hi, lo + initRunLen, entrySize, c)
        return
      }
      /**
       * March over the array once, left to right, finding natural runs,
       * extending short natural runs to minRun elements, and merging runs
       * to maintain stack invariant.
       */
      val ts = ByteArrayTimSort(a, c, entrySize)
      val minRun = minRunLength(nRemaining)
      do {
        // Identify next run
        var runLen = countRunAndMakeAscending(a, lo, hi, entrySize, c)
        // If run is short, extend to min(minRun, nRemaining)
        if (runLen < minRun) {
          val force = if (nRemaining <= minRun) nRemaining else minRun
          binarySort(a, lo, lo + force, lo + runLen, entrySize, c)
          runLen = force
        }
        // Push run onto pending-run stack, and maybe merge
        ts.pushRun(lo, runLen)
        ts.mergeCollapse()
        // Advance to find next run
        lo += runLen
        nRemaining -= runLen
      } while (nRemaining != 0)
      // Merge all remaining runs to complete sort
      if (DEBUG) assert(lo == hi)
      ts.mergeForceCollapse()
      if (DEBUG) assert(ts.stackSize == 1)
    }

    private fun checkStartAndEnd(
      len: Int,
      start: Int,
      end: Int
    ) {
      if (start < 0 || end > len) {
        throw ArrayIndexOutOfBoundsException(
          "start < 0 || end > len."
            + " start=" + start + ", end=" + end + ", len=" + len
        )
      }
      if (start > end) {
        throw IllegalArgumentException("start > end: $start > $end")
      }
    }

    /**
     * Sorts the specified portion of the specified array using a binary
     * insertion sort.  This is the best method for sorting small numbers
     * of elements.  It requires O(n log n) compares, but O(n^2) data
     * movement (worst case).
     *
     * If the initial part of the specified range is already sorted,
     * this method can take advantage of it: the method assumes that the
     * elements from index `lo`, inclusive, to `start`,
     * exclusive are already sorted.
     *
     * @param a the array in which a range is to be sorted
     * @param lo the index of the first element in the range to be sorted
     * @param hi the index after the last element in the range to be sorted
     * @param start the index of the first element in the range that is
     * not already known to be sorted (@code lo <= start <= hi}
     * @param c comparator to used for the sort
     */
    private fun binarySort(
      a: ByteArray,
      lo: Int,
      hi: Int,
      start: Int,
      entrySize: Int,
      c: ByteArrayComparator
    ) {
      var start = start
      if (DEBUG) assert(lo <= start && start <= hi)
      if (start == lo)
        start++
      val pivot = ByteArray(entrySize)
      while (start < hi) {
        val startIndex = start * entrySize
        for (i in 0 until entrySize) {
          pivot[i] = a[startIndex + i]
        }
        // Set left (and right) to the index where a[start] (pivot) belongs
        var left = lo
        var right = start
        if (DEBUG) assert(left <= right)
        /*
             * Invariants:
             *   pivot >= all in [lo, left).
             *   pivot <  all in [right, start).
             */
        while (left < right) {
          val mid = (left + right).ushr(1)
          if (c.compare(entrySize, pivot, 0, a, mid) < 0)
            right = mid
          else
            left = mid + 1
        }
        if (DEBUG) assert(left == right)
        /*
             * The invariants still hold: pivot >= all in [lo, left) and
             * pivot < all in [left, start), so pivot belongs at left.  Note
             * that if there are elements equal to pivot, left points to the
             * first slot after them -- that's why this sort is stable.
             * Slide elements over to make room for pivot.
             */
        val n = start - left  // The number of elements to move
        // Switch is just an optimization for arraycopy in default case
        when (n) {
          2 -> {
            val leftIndex = left * entrySize
            val leftPlusOneIndex = (left + 1) * entrySize
            val leftPlusTwoIndex = (left + 2) * entrySize
            for (i in 0 until entrySize) {
              a[leftPlusTwoIndex + i] = a[leftPlusOneIndex + i]
            }
            for (i in 0 until entrySize) {
              a[leftPlusOneIndex + i] = a[leftIndex + i]
            }
          }
          1 -> {
            val leftIndex = left * entrySize
            val leftPlusOneIndex = (left + 1) * entrySize
            for (i in 0 until entrySize) {
              a[leftPlusOneIndex + i] = a[leftIndex + i]
            }
          }
          else -> {
            System.arraycopy(a, left * entrySize, a, (left + 1) * entrySize, n * entrySize)
          }
        }
        val leftIndex = left * entrySize
        for (i in 0 until entrySize) {
          a[leftIndex + i] = pivot[i]
        }
        start++
      }
    }

    /**
     * Returns the length of the run beginning at the specified position in
     * the specified array and reverses the run if it is descending (ensuring
     * that the run will always be ascending when the method returns).
     *
     * A run is the longest ascending sequence with:
     *
     * a[lo] <= a[lo + 1] <= a[lo + 2] <= ...
     *
     * or the longest descending sequence with:
     *
     * a[lo] >  a[lo + 1] >  a[lo + 2] >  ...
     *
     * For its intended use in a stable mergesort, the strictness of the
     * definition of "descending" is needed so that the call can safely
     * reverse a descending sequence without violating stability.
     *
     * @param a the array in which a run is to be counted and possibly reversed
     * @param lo index of the first element in the run
     * @param hi index after the last element that may be contained in the run.
     * It is required that @code{lo < hi}.
     * @param c the comparator to used for the sort
     * @return  the length of the run beginning at the specified position in
     * the specified array
     */
    private fun countRunAndMakeAscending(
      a: ByteArray,
      lo: Int,
      hi: Int,
      entrySize: Int,
      c: ByteArrayComparator
    ): Int {
      if (DEBUG) assert(lo < hi)
      var runHi = lo + 1
      if (runHi == hi)
        return 1
      // Find end of run, and reverse range if descending

      val comparison = c.compare(entrySize, a, runHi, a, lo)
      runHi++
      if (comparison < 0) { // Descending
        while (runHi < hi && c.compare(entrySize, a, runHi, a, runHi - 1) < 0)
          runHi++
        reverseRange(a, lo, runHi, entrySize)
      } else {                              // Ascending
        while (runHi < hi && c.compare(entrySize, a, runHi, a, runHi - 1) >= 0)
          runHi++
      }
      return runHi - lo
    }

    /**
     * Reverse the specified range of the specified array.
     *
     * @param a the array in which a range is to be reversed
     * @param lo the index of the first element in the range to be reversed
     * @param hi the index after the last element in the range to be reversed
     */
    private fun reverseRange(
      a: ByteArray,
      lo: Int,
      hi: Int,
      entrySize: Int
    ) {
      var lo = lo
      var hi = hi
      hi--
      while (lo < hi) {
        val loIndex = lo * entrySize
        val hiIndex = hi * entrySize
        for (i in 0 until entrySize) {
          val t = a[loIndex + i]
          a[loIndex + i] = a[hiIndex + i]
          a[hiIndex + i] = t
        }
        lo++
        hi--
      }
    }

    /**
     * Returns the minimum acceptable run length for an array of the specified
     * length. Natural runs shorter than this will be extended with
     * [.binarySort].
     *
     * Roughly speaking, the computation is:
     *
     * If n < MIN_MERGE, return n (it's too small to bother with fancy stuff).
     * Else if n is an exact power of 2, return MIN_MERGE/2.
     * Else return an int k, MIN_MERGE/2 <= k <= MIN_MERGE, such that n/k
     * is close to, but strictly less than, an exact power of 2.
     *
     * For the rationale, see listsort.txt.
     *
     * @param n the length of the array to be sorted
     * @return the length of the minimum run to be merged
     */
    private fun minRunLength(n: Int): Int {
      var n = n
      if (DEBUG) assert(n >= 0)
      var r = 0      // Becomes 1 if any 1 bits are shifted off
      while (n >= MIN_MERGE) {
        r = r or (n and 1)
        n = n shr 1
      }
      return n + r
    }

    /**
     * Locates the position at which to insert the specified key into the
     * specified sorted range; if the range contains an element equal to key,
     * returns the index of the leftmost equal element.
     *
     * @param key the key whose insertion point to search for
     * @param a the array in which to search
     * @param base the index of the first element in the range
     * @param len the length of the range; must be > 0
     * @param hint the index at which to begin the search, 0 <= hint < n.
     * The closer hint is to the result, the faster this method will run.
     * @param c the comparator used to order the range, and to search
     * @return the int k,  0 <= k <= n such that a[b + k - 1] < key <= a[b + k],
     * pretending that a[b - 1] is minus infinity and a[b + n] is infinity.
     * In other words, key belongs at index b + k; or in other words,
     * the first k elements of a should precede key, and the last n - k
     * should follow it.
     */
    private fun gallopLeft(
      keyArray: ByteArray,
      // Index already divided by entrySize
      keyIndex: Int,
      a: ByteArray,
      base: Int,
      len: Int,
      hint: Int,
      entrySize: Int,
      c: ByteArrayComparator
    ): Int {
      if (DEBUG) assert(len > 0 && hint >= 0 && hint < len)
      var lastOfs = 0
      var ofs = 1
      if (c.compare(entrySize, keyArray, keyIndex, a, base + hint) > 0) {
        // Gallop right until a[base+hint+lastOfs] < key <= a[base+hint+ofs]
        val maxOfs = len - hint
        while (ofs < maxOfs && c.compare(entrySize, keyArray, keyIndex, a, base + hint + ofs) > 0) {
          lastOfs = ofs
          ofs = ofs * 2 + 1
          if (ofs <= 0)
          // int overflow
            ofs = maxOfs
        }
        if (ofs > maxOfs)
          ofs = maxOfs
        // Make offsets relative to base
        lastOfs += hint
        ofs += hint
      } else { // key <= a[base + hint]
        // Gallop left until a[base+hint-ofs] < key <= a[base+hint-lastOfs]
        val maxOfs = hint + 1
        while (ofs < maxOfs && c.compare(
            entrySize, keyArray, keyIndex, a, base + hint - ofs
          ) <= 0
        ) {
          lastOfs = ofs
          ofs = ofs * 2 + 1
          if (ofs <= 0)
          // int overflow
            ofs = maxOfs
        }
        if (ofs > maxOfs)
          ofs = maxOfs
        // Make offsets relative to base
        val tmp = lastOfs
        lastOfs = hint - ofs
        ofs = hint - tmp
      }
      if (DEBUG) assert(-1 <= lastOfs && lastOfs < ofs && ofs <= len)
      /*
         * Now a[base+lastOfs] < key <= a[base+ofs], so key belongs somewhere
         * to the right of lastOfs but no farther right than ofs.  Do a binary
         * search, with invariant a[base + lastOfs - 1] < key <= a[base + ofs].
         */
      lastOfs++
      while (lastOfs < ofs) {
        val m = lastOfs + (ofs - lastOfs).ushr(1)
        if (c.compare(entrySize, keyArray, keyIndex, a, base + m) > 0)
          lastOfs = m + 1  // a[base + m] < key
        else
          ofs = m          // key <= a[base + m]
      }
      if (DEBUG) assert(lastOfs == ofs)    // so a[base + ofs - 1] < key <= a[base + ofs]
      return ofs
    }

    /**
     * Like gallopLeft, except that if the range contains an element equal to
     * key, gallopRight returns the index after the rightmost equal element.
     *
     * @param key the key whose insertion point to search for
     * @param a the array in which to search
     * @param base the index of the first element in the range
     * @param len the length of the range; must be > 0
     * @param hint the index at which to begin the search, 0 <= hint < n.
     * The closer hint is to the result, the faster this method will run.
     * @param c the comparator used to order the range, and to search
     * @return the int k,  0 <= k <= n such that a[b + k - 1] <= key < a[b + k]
     */
    private fun gallopRight(
      keyArray: ByteArray,
      // Index already divided by entrySize
      keyIndex: Int,
      a: ByteArray,
      base: Int,
      len: Int,
      hint: Int,
      entrySize: Int,
      c: ByteArrayComparator
    ): Int {
      if (DEBUG) assert(len > 0 && hint >= 0 && hint < len)
      var ofs = 1
      var lastOfs = 0
      if (c.compare(entrySize, keyArray, keyIndex, a, base + hint) < 0) {
        // Gallop left until a[b+hint - ofs] <= key < a[b+hint - lastOfs]
        val maxOfs = hint + 1
        while (ofs < maxOfs && c.compare(entrySize, keyArray, keyIndex, a, base + hint - ofs) < 0) {
          lastOfs = ofs
          ofs = ofs * 2 + 1
          if (ofs <= 0)
          // int overflow
            ofs = maxOfs
        }
        if (ofs > maxOfs)
          ofs = maxOfs
        // Make offsets relative to b
        val tmp = lastOfs
        lastOfs = hint - ofs
        ofs = hint - tmp
      } else { // a[b + hint] <= key
        // Gallop right until a[b+hint + lastOfs] <= key < a[b+hint + ofs]
        val maxOfs = len - hint
        while (ofs < maxOfs && c.compare(
            entrySize, keyArray, keyIndex, a, base + hint + ofs
          ) >= 0
        ) {
          lastOfs = ofs
          ofs = ofs * 2 + 1
          if (ofs <= 0)
          // int overflow
            ofs = maxOfs
        }
        if (ofs > maxOfs)
          ofs = maxOfs
        // Make offsets relative to b
        lastOfs += hint
        ofs += hint
      }
      if (DEBUG) assert(-1 <= lastOfs && lastOfs < ofs && ofs <= len)
      /*
         * Now a[b + lastOfs] <= key < a[b + ofs], so key belongs somewhere to
         * the right of lastOfs but no farther right than ofs.  Do a binary
         * search, with invariant a[b + lastOfs - 1] <= key < a[b + ofs].
         */
      lastOfs++
      while (lastOfs < ofs) {
        val m = lastOfs + (ofs - lastOfs).ushr(1)
        if (c.compare(entrySize, keyArray, keyIndex, a, base + m) < 0)
          ofs = m          // key < a[b + m]
        else
          lastOfs = m + 1  // a[b + m] <= key
      }
      if (DEBUG) assert(lastOfs == ofs)    // so a[b + ofs - 1] <= key < a[b + ofs]
      return ofs
    }
  }
}

/*
 *  Copyright 2010-2013, Carrot Search s.c., Boznicza 11/56, Poznan, Poland
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package shark.internal.hppc

import java.util.Locale

/**
 * Code from https://github.com/carrotsearch/hppc copy pasted, inlined and converted to Kotlin.
 */
internal object HPPC {

  private const val PHI_C64 = -0x61c8864680b583ebL

  fun mixPhi(k: Long): Int {
    val h = k * PHI_C64
    return (h xor h.ushr(32)).toInt()
  }

  private const val MIN_HASH_ARRAY_LENGTH = 4
  private const val MAX_HASH_ARRAY_LENGTH = (-0x80000000).ushr(1)

  fun minBufferSize(
    elements: Int,
    loadFactor: Double
  ): Int {
    var length = Math.ceil(elements / loadFactor)
      .toLong()
    if (length == elements.toLong()) {
      length++
    }
    length = Math.max(MIN_HASH_ARRAY_LENGTH.toLong(), nextHighestPowerOfTwo(length))

    if (length > MAX_HASH_ARRAY_LENGTH) {
      throw RuntimeException(
        String.format(
          Locale.ROOT,
          "Maximum array size exceeded for this load factor (elements: %d, load factor: %f)",
          elements,
          loadFactor
        )
      )
    }

    return length.toInt()
  }

  fun nextHighestPowerOfTwo(input: Long): Long {
    var v = input
    v--
    v = v or (v shr 1)
    v = v or (v shr 2)
    v = v or (v shr 4)
    v = v or (v shr 8)
    v = v or (v shr 16)
    v = v or (v shr 32)
    v++
    return v
  }

  fun expandAtCount(
    arraySize: Int,
    loadFactor: Double
  ): Int {
    return Math.min(arraySize - 1, Math.ceil(arraySize * loadFactor).toInt())
  }

  fun nextBufferSize(
    arraySize: Int,
    elements: Int,
    loadFactor: Double
  ): Int {
    if (arraySize == MAX_HASH_ARRAY_LENGTH) {
      throw RuntimeException(
        String.format(
          Locale.ROOT,
          "Maximum array size exceeded for this load factor (elements: %d, load factor: %f)",
          elements,
          loadFactor
        )
      )
    }

    return arraySize shl 1
  }
}

/*
 *  Copyright 2010-2013, Carrot Search s.c., Boznicza 11/56, Poznan, Poland
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package shark.internal.hppc

import java.util.Locale

/**
 * Code from com.carrotsearch.hppc.LongLongScatterMap copy pasted, inlined and converted to Kotlin.
 *
 * See https://github.com/carrotsearch/hppc .
 */
internal class LongLongScatterMap constructor(expectedElements: Int = 4) {

  interface ForEachCallback {
    fun onEntry(
      key: Long,
      value: Long
    )
  }

  /**
   * The array holding keys.
   */
  private var keys: LongArray = longArrayOf()

  /**
   * The array holding values.
   */
  private var values: LongArray = longArrayOf()

  /**
   * The number of stored keys (assigned key slots), excluding the special
   * "empty" key, if any (use [.size] instead).
   *
   * @see .size
   */
  private var assigned: Int = 0

  /**
   * Mask for slot scans in [.keys].
   */
  private var mask: Int = 0

  /**
   * Expand (rehash) [.keys] when [.assigned] hits this value.
   */
  private var resizeAt: Int = 0

  /**
   * Special treatment for the "empty slot" key marker.
   */
  private var hasEmptyKey: Boolean = false

  /**
   * The load factor for [.keys].
   */
  private var loadFactor: Double = 0.75

  val isEmpty: Boolean
    get() = size == 0

  init {
    ensureCapacity(expectedElements)
  }

  operator fun set(
    key: Long,
    value: Long
  ): Long {
    val mask = this.mask
    if (key == 0L) {
      hasEmptyKey = true
      val previousValue = values[mask + 1]
      values[mask + 1] = value
      return previousValue
    } else {
      val keys = this.keys
      var slot = hashKey(key) and mask

      var existing = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          val previousValue = values[slot]
          values[slot] = value
          return previousValue
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }

      if (assigned == resizeAt) {
        allocateThenInsertThenRehash(slot, key, value)
      } else {
        keys[slot] = key
        values[slot] = value
      }

      assigned++
      return 0L
    }
  }

  fun remove(key: Long): Long {
    val mask = this.mask
    if (key == 0L) {
      hasEmptyKey = false
      val previousValue = values[mask + 1]
      values[mask + 1] = 0L
      return previousValue
    } else {
      val keys = this.keys
      var slot = hashKey(key) and mask

      var existing = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          val previousValue = values[slot]
          shiftConflictingKeys(slot)
          return previousValue
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }

      return 0L
    }
  }

  /**
   * Being given a key looks it up in the map and returns the slot where element sits, so it later
   * can be retrieved with [getSlotValue]; return '-1' if element not found.
   * Why so complicated and not just make [get] return null if value not found? The reason is performance:
   * this approach prevents unnecessary boxing of the primitive long that would happen with nullable Long?
   */
  fun getSlot(key: Long): Int {
    if (key == 0L) {
      return if (hasEmptyKey) mask + 1 else -1
    } else {
      val keys = this.keys
      val mask = this.mask
      var slot = hashKey(key) and mask

      var existing = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          return slot
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }

      return -1
    }
  }

  /**
   * Being given a slot of element retrieves it from the collection
   */
  fun getSlotValue(slot: Int): Long = values[slot]

  /**
   * Returns an element matching a provided [key]; throws [IllegalArgumentException] if element not found
   */
  operator fun get(key: Long): Long {
    val slot = getSlot(key)
    require(slot != -1) { "Unknown key $key" }

    return getSlotValue(slot)
  }

  fun forEach(forEachCallback: ForEachCallback) {
    val max = mask + 1
    var slot = -1

    exitWhile@ while (true) {
      if (slot < max) {
        var existing: Long
        slot++
        while (slot < max) {
          existing = keys[slot]
          if (existing != 0L) {
            forEachCallback.onEntry(existing, values[slot])
            continue@exitWhile
          }
          slot++
        }
      }

      if (slot == max && hasEmptyKey) {
        slot++
        forEachCallback.onEntry(0L, values[max])
        continue@exitWhile
      }
      break@exitWhile
    }
  }

  fun entrySequence(): Sequence<LongLongPair> {
    val max = mask + 1
    var slot = -1
    return generateSequence {
      if (slot < max) {
        var existing: Long
        slot++
        while (slot < max) {
          existing = keys[slot]
          if (existing != 0L) {
            return@generateSequence existing to values[slot]
          }
          slot++
        }
      }
      if (slot == max && hasEmptyKey) {
        slot++
        return@generateSequence 0L to values[max]
      }
      return@generateSequence null
    }
  }

  fun containsKey(key: Long): Boolean {
    if (key == 0L) {
      return hasEmptyKey
    } else {
      val keys = this.keys
      val mask = this.mask
      var slot = hashKey(key) and mask

      var existing = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          return true
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }

      return false
    }
  }

  fun release() {
    assigned = 0
    hasEmptyKey = false

    allocateBuffers(HPPC.minBufferSize(4, loadFactor))
  }

  val size: Int
    get() {
      return assigned + if (hasEmptyKey) 1 else 0
    }

  fun ensureCapacity(expectedElements: Int) {
    if (expectedElements > resizeAt) {
      val prevKeys = this.keys
      val prevValues = this.values
      allocateBuffers(HPPC.minBufferSize(expectedElements, loadFactor))
      if (!isEmpty) {
        rehash(prevKeys, prevValues)
      }
    }
  }

  private fun hashKey(key: Long): Int {
    return HPPC.mixPhi(key)
  }

  /**
   * Rehash from old buffers to new buffers.
   */
  private fun rehash(
    fromKeys: LongArray,
    fromValues: LongArray
  ) {
    // Rehash all stored key/value pairs into the new buffers.
    val keys = this.keys
    val values = this.values
    val mask = this.mask
    var existing: Long

    // Copy the zero element's slot, then rehash everything else.
    var from = fromKeys.size - 1
    keys[keys.size - 1] = fromKeys[from]
    values[values.size - 1] = fromValues[from]
    while (--from >= 0) {
      existing = fromKeys[from]
      if (existing != 0L) {
        var slot = hashKey(existing) and mask
        while (keys[slot] != 0L) {
          slot = slot + 1 and mask
        }
        keys[slot] = existing
        values[slot] = fromValues[from]
      }
    }
  }

  /**
   * Allocate new internal buffers. This method attempts to allocate
   * and assign internal buffers atomically (either allocations succeed or not).
   */
  private fun allocateBuffers(arraySize: Int) {

    // Ensure no change is done if we hit an OOM.
    val prevKeys = this.keys
    val prevValues = this.values
    try {
      val emptyElementSlot = 1
      this.keys = LongArray(arraySize + emptyElementSlot)
      this.values = LongArray(arraySize + emptyElementSlot)
    } catch (e: OutOfMemoryError) {
      this.keys = prevKeys
      this.values = prevValues
      throw RuntimeException(
        String.format(
          Locale.ROOT,
          "Not enough memory to allocate buffers for rehashing: %,d -> %,d",
          this.mask + 1,
          arraySize
        ), e
      )
    }

    this.resizeAt = HPPC.expandAtCount(arraySize, loadFactor)
    this.mask = arraySize - 1
  }

  /**
   * This method is invoked when there is a new key/ value pair to be inserted into
   * the buffers but there is not enough empty slots to do so.
   *
   * New buffers are allocated. If this succeeds, we know we can proceed
   * with rehashing so we assign the pending element to the previous buffer
   * (possibly violating the invariant of having at least one empty slot)
   * and rehash all keys, substituting new buffers at the end.
   */
  private fun allocateThenInsertThenRehash(
    slot: Int,
    pendingKey: Long,
    pendingValue: Long
  ) {

    // Try to allocate new buffers first. If we OOM, we leave in a consistent state.
    val prevKeys = this.keys
    val prevValues = this.values
    allocateBuffers(HPPC.nextBufferSize(mask + 1, size, loadFactor))

    // We have succeeded at allocating new data so insert the pending key/value at
    // the free slot in the old arrays before rehashing.
    prevKeys[slot] = pendingKey
    prevValues[slot] = pendingValue

    // Rehash old keys, including the pending key.
    rehash(prevKeys, prevValues)
  }

  /**
   * Shift all the slot-conflicting keys and values allocated to
   * (and including) `slot`.
   */
  private fun shiftConflictingKeys(gapSlotArg: Int) {
    var gapSlot = gapSlotArg
    val keys = this.keys
    val values = this.values
    val mask = this.mask

    // Perform shifts of conflicting keys to fill in the gap.
    var distance = 0
    while (true) {
      val slot = gapSlot + ++distance and mask
      val existing = keys[slot]
      if (existing == 0L) {
        break
      }

      val idealSlot = hashKey(existing)
      val shift = slot - idealSlot and mask
      if (shift >= distance) {
        // Entry at this position was originally at or before the gap slot.
        // Move the conflict-shifted entry to the gap's position and repeat the procedure
        // for any entries to the right of the current position, treating it
        // as the new gap.
        keys[gapSlot] = existing
        values[gapSlot] = values[slot]
        gapSlot = slot
        distance = 0
      }
    }

    // Mark the last found gap slot without a conflict as empty.
    keys[gapSlot] = 0L
    values[gapSlot] = 0L
    assigned--
  }
}

/*
 *  Copyright 2010-2013, Carrot Search s.c., Boznicza 11/56, Poznan, Poland
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package shark.internal.hppc

import java.util.Locale

/**
 * Code from com.carrotsearch.hppc.LongLongScatterMap copy pasted, inlined and converted to Kotlin.
 *
 * See https://github.com/carrotsearch/hppc .
 */
internal class LongObjectScatterMap<T> {
  /**
   * The array holding keys.
   */
  private var keys: LongArray = longArrayOf()

  /**
   * The array holding values.
   */
  @Suppress("UNCHECKED_CAST")
  private var values: Array<T?> = emptyArray<Any?>() as Array<T?>

  /**
   * The number of stored keys (assigned key slots), excluding the special
   * "empty" key, if any (use [.size] instead).
   *
   * @see .size
   */
  private var assigned: Int = 0

  /**
   * Mask for slot scans in [.keys].
   */
  private var mask: Int = 0

  /**
   * Expand (rehash) [.keys] when [.assigned] hits this value.
   */
  private var resizeAt: Int = 0

  /**
   * Special treatment for the "empty slot" key marker.
   */
  private var hasEmptyKey: Boolean = false

  /**
   * The load factor for [.keys].
   */
  private var loadFactor: Double = 0.75

  val isEmpty: Boolean
    get() = size == 0

  init {
    ensureCapacity(4)
  }

  operator fun set(
    key: Long,
    value: T
  ): T? {
    val mask = this.mask
    if (key == 0L) {
      hasEmptyKey = true
      val previousValue = values[mask + 1]
      values[mask + 1] = value
      return previousValue
    } else {
      val keys = this.keys
      var slot = hashKey(key) and mask

      var existing = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          val previousValue = values[slot]
          values[slot] = value
          return previousValue
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }

      if (assigned == resizeAt) {
        allocateThenInsertThenRehash(slot, key, value)
      } else {
        keys[slot] = key
        values[slot] = value
      }

      assigned++
      return null
    }
  }

  fun remove(key: Long): T? {
    val mask = this.mask
    if (key == 0L) {
      hasEmptyKey = false
      val previousValue = values[mask + 1]
      values[mask + 1] = null
      return previousValue
    } else {
      val keys = this.keys
      var slot = hashKey(key) and mask

      var existing = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          val previousValue = values[slot]
          shiftConflictingKeys(slot)
          return previousValue
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }

      return null
    }
  }

  operator fun get(key: Long): T? {
    if (key == 0L) {
      return if (hasEmptyKey) values[mask + 1] else null
    } else {
      val keys = this.keys
      val mask = this.mask
      var slot = hashKey(key) and mask

      var existing = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          return values[slot]
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }

      return null
    }
  }

  fun entrySequence(): Sequence<LongObjectPair<T>> {
    val max = mask + 1
    var slot = -1
    return generateSequence {
      if (slot < max) {
        var existing: Long
        slot++
        while (slot < max) {
          existing = keys[slot]
          if (existing != 0L) {
            return@generateSequence existing to values[slot]!!
          }
          slot++
        }
      }
      if (slot == max && hasEmptyKey) {
        slot++
        return@generateSequence 0L to values[max]!!
      }
      return@generateSequence null
    }
  }

  fun containsKey(key: Long): Boolean {
    if (key == 0L) {
      return hasEmptyKey
    } else {
      val keys = this.keys
      val mask = this.mask
      var slot = hashKey(key) and mask

      var existing = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          return true
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }

      return false
    }
  }

  fun release() {
    assigned = 0
    hasEmptyKey = false

    allocateBuffers(HPPC.minBufferSize(4, loadFactor))
  }

  val size: Int
    get() {
      return assigned + if (hasEmptyKey) 1 else 0
    }

  fun ensureCapacity(expectedElements: Int) {
    if (expectedElements > resizeAt) {
      val prevKeys = this.keys
      val prevValues = this.values
      allocateBuffers(HPPC.minBufferSize(expectedElements, loadFactor))
      if (!isEmpty) {
        rehash(prevKeys, prevValues)
      }
    }
  }

  private fun hashKey(key: Long): Int {
    return HPPC.mixPhi(key)
  }

  /**
   * Rehash from old buffers to new buffers.
   */
  private fun rehash(
    fromKeys: LongArray,
    fromValues: Array<T?>
  ) {
    // Rehash all stored key/value pairs into the new buffers.
    val keys = this.keys
    val values = this.values
    val mask = this.mask
    var existing: Long

    // Copy the zero element's slot, then rehash everything else.
    var from = fromKeys.size - 1
    keys[keys.size - 1] = fromKeys[from]
    values[values.size - 1] = fromValues[from]
    while (--from >= 0) {
      existing = fromKeys[from]
      if (existing != 0L) {
        var slot = hashKey(existing) and mask
        while (keys[slot] != 0L) {
          slot = slot + 1 and mask
        }
        keys[slot] = existing
        values[slot] = fromValues[from]
      }
    }
  }

  /**
   * Allocate new internal buffers. This method attempts to allocate
   * and assign internal buffers atomically (either allocations succeed or not).
   */
  private fun allocateBuffers(arraySize: Int) {

    // Ensure no change is done if we hit an OOM.
    val prevKeys = this.keys
    val prevValues = this.values
    try {
      val emptyElementSlot = 1
      this.keys = LongArray(arraySize + emptyElementSlot)
      @Suppress("UNCHECKED_CAST")
      this.values = arrayOfNulls<Any?>(arraySize + emptyElementSlot) as Array<T?>
    } catch (e: OutOfMemoryError) {
      this.keys = prevKeys
      this.values = prevValues
      throw RuntimeException(
        String.format(
          Locale.ROOT,
          "Not enough memory to allocate buffers for rehashing: %,d -> %,d",
          this.mask + 1,
          arraySize
        ), e
      )
    }

    this.resizeAt = HPPC.expandAtCount(arraySize, loadFactor)
    this.mask = arraySize - 1
  }

  /**
   * This method is invoked when there is a new key/ value pair to be inserted into
   * the buffers but there is not enough empty slots to do so.
   *
   * New buffers are allocated. If this succeeds, we know we can proceed
   * with rehashing so we assign the pending element to the previous buffer
   * (possibly violating the invariant of having at least one empty slot)
   * and rehash all keys, substituting new buffers at the end.
   */
  private fun allocateThenInsertThenRehash(
    slot: Int,
    pendingKey: Long,
    pendingValue: T
  ) {

    // Try to allocate new buffers first. If we OOM, we leave in a consistent state.
    val prevKeys = this.keys
    val prevValues = this.values
    allocateBuffers(HPPC.nextBufferSize(mask + 1, size, loadFactor))

    // We have succeeded at allocating new data so insert the pending key/value at
    // the free slot in the old arrays before rehashing.
    prevKeys[slot] = pendingKey
    prevValues[slot] = pendingValue

    // Rehash old keys, including the pending key.
    rehash(prevKeys, prevValues)
  }

  /**
   * Shift all the slot-conflicting keys and values allocated to
   * (and including) `slot`.
   */
  private fun shiftConflictingKeys(gapSlotArg: Int) {
    var gapSlot = gapSlotArg
    val keys = this.keys
    val values = this.values
    val mask = this.mask

    // Perform shifts of conflicting keys to fill in the gap.
    var distance = 0
    while (true) {
      val slot = gapSlot + ++distance and mask
      val existing = keys[slot]
      if (existing == 0L) {
        break
      }

      val idealSlot = hashKey(existing)
      val shift = slot - idealSlot and mask
      if (shift >= distance) {
        // Entry at this position was originally at or before the gap slot.
        // Move the conflict-shifted entry to the gap's position and repeat the procedure
        // for any entries to the right of the current position, treating it
        // as the new gap.
        keys[gapSlot] = existing
        values[gapSlot] = values[slot]
        gapSlot = slot
        distance = 0
      }
    }

    // Mark the last found gap slot without a conflict as empty.
    keys[gapSlot] = 0L
    values[gapSlot] = null
    assigned--
  }
}

/*
 *  Copyright 2010-2013, Carrot Search s.c., Boznicza 11/56, Poznan, Poland
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package shark.internal.hppc

import java.util.Locale

/**
 * Code from com.carrotsearch.hppc.LongScatterSet copy pasted, inlined and converted to Kotlin.
 *
 * See https://github.com/carrotsearch/hppc .
 */
internal class LongScatterSet(expectedElements: Int = 4) {
  /** The hash array holding keys.  */
  private var keys: LongArray = longArrayOf()

  /**
   * The number of stored keys (assigned key slots), excluding the special
   * "empty" key, if any.
   *
   * @see .size
   * @see .hasEmptyKey
   */
  private var assigned = 0

  /**
   * Mask for slot scans in [.keys].
   */
  private var mask = 0

  /**
   * Expand (rehash) [.keys] when [.assigned] hits this value.
   */
  private var resizeAt = 0

  /**
   * Special treatment for the "empty slot" key marker.
   */
  private var hasEmptyKey = false

  /**
   * The load factor for [.keys].
   */
  private val loadFactor = 0.75

  init {
    ensureCapacity(expectedElements)
  }

  private fun hashKey(key: Long): Int {
    return HPPC.mixPhi(key)
  }

  operator fun plusAssign(key: Long) {
    add(key)
  }

  fun add(key: Long): Boolean {
    if (key == 0L) {
      val added = !hasEmptyKey
      hasEmptyKey = true
      return added
    } else {
      val keys = this.keys
      val mask = this.mask
      var slot = hashKey(key) and mask

      var existing = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          return false
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }

      if (assigned == resizeAt) {
        allocateThenInsertThenRehash(slot, key)
      } else {
        keys[slot] = key
      }

      assigned++
      return true
    }
  }

  operator fun contains(key: Long): Boolean {
    if (key == 0L) {
      return hasEmptyKey
    } else {
      val keys = this.keys
      val mask = this.mask
      var slot = hashKey(key) and mask
      var existing = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          return true
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }
      return false
    }
  }

  fun remove(key: Long): Boolean {
    return if (key == 0L) {
      val hadEmptyKey = hasEmptyKey
      hasEmptyKey = false
      hadEmptyKey
    } else {
      val keys = this.keys
      val mask = this.mask
      var slot = hashKey(key) and mask
      var existing: Long = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          shiftConflictingKeys(slot)
          return true
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }
      false
    }
  }

  /**
   * Shift all the slot-conflicting keys allocated to (and including) `slot`.
   */
  private fun shiftConflictingKeys(inputGapSlot: Int) {
    var gapSlot = inputGapSlot
    val keys = keys
    val mask = mask
    // Perform shifts of conflicting keys to fill in the gap.
    var distance = 0
    while (true) {
      val slot = (gapSlot + (++distance)) and mask
      val existing = keys[slot]
      if (existing == 0L) {
        break
      }
      val idealSlot = hashKey(existing)
      val shift = (slot - idealSlot) and mask
      if (shift >= distance) {
        // Entry at this position was originally at or before the gap slot.
        // Move the conflict-shifted entry to the gap's position and repeat the procedure
        // for any entries to the right of the current position, treating it
        // as the new gap.
        keys[gapSlot] = existing
        gapSlot = slot
        distance = 0
      }
    }
    // Mark the last found gap slot without a conflict as empty.
    keys[gapSlot] = 0L
    assigned--
  }

  fun release() {
    assigned = 0
    hasEmptyKey = false
    allocateBuffers(HPPC.minBufferSize(4, loadFactor))
  }

  fun ensureCapacity(expectedElements: Int) {
    if (expectedElements > resizeAt) {
      val prevKeys = this.keys
      allocateBuffers(HPPC.minBufferSize(expectedElements, loadFactor))
      if (size() != 0) {
        rehash(prevKeys)
      }
    }
  }

  fun size(): Int {
    return assigned + if (hasEmptyKey) 1 else 0
  }

  private fun rehash(fromKeys: LongArray) {
    // Rehash all stored keys into the new buffers.
    val keys = this.keys
    val mask = this.mask
    var existing: Long
    var i = fromKeys.size - 1
    while (--i >= 0) {
      existing = fromKeys[i]
      if (existing != 0L) {
        var slot = hashKey(existing) and mask
        while (keys[slot] != 0L) {
          slot = slot + 1 and mask
        }
        keys[slot] = existing
      }
    }
  }

  /**
   * Allocate new internal buffers. This method attempts to allocate
   * and assign internal buffers atomically (either allocations succeed or not).
   */
  private fun allocateBuffers(arraySize: Int) {
    // Ensure no change is done if we hit an OOM.
    val prevKeys = this.keys
    try {
      val emptyElementSlot = 1
      this.keys = LongArray(arraySize + emptyElementSlot)
    } catch (e: OutOfMemoryError) {
      this.keys = prevKeys
      throw RuntimeException(
        String.format(
          Locale.ROOT,
          "Not enough memory to allocate buffers for rehashing: %,d -> %,d",
          size(),
          arraySize
        ), e
      )
    }

    this.resizeAt = HPPC.expandAtCount(arraySize, loadFactor)
    this.mask = arraySize - 1
  }

  private fun allocateThenInsertThenRehash(
    slot: Int,
    pendingKey: Long
  ) {
    // Try to allocate new buffers first. If we OOM, we leave in a consistent state.
    val prevKeys = this.keys
    allocateBuffers(HPPC.nextBufferSize(mask + 1, size(), loadFactor))

    // We have succeeded at allocating new data so insert the pending key/value at
    // the free slot in the old arrays before rehashing.
    prevKeys[slot] = pendingKey

    // Rehash old keys, including the pending key.
    rehash(prevKeys)
  }
}

package shark.internal.hppc

/** Alternative to Pair<Long, Object> that doesn't box long.*/
internal data class LongObjectPair<out B>(
  val first: Long,
  val second: B
)

/** Alternative to Pair<Int, Object> that doesn't box int.*/
internal data class IntObjectPair<out B>(
  val first: Int,
  val second: B
)

/** Alternative to Pair<Long, Long> that doesn't box longs. */
internal data class LongLongPair(
  val first: Long,
  val second: Long
)

internal infix fun <B> Long.to(that: B): LongObjectPair<B> = LongObjectPair(this, that)

internal infix fun <B> Int.to(that: B): IntObjectPair<B> = IntObjectPair(this, that)

internal infix fun Long.to(that: Long): LongLongPair = LongLongPair(this, that)

package leakcanary.internal.activity.db

import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import shark.SharkLog
import kotlin.concurrent.getOrSet

/**
 * Similar to the more generic use() for Closable.
 * Cursor started implementing Closable in API 16.
 */
internal inline fun <R> Cursor.use(block: (Cursor) -> R): R {
  var exception: Throwable? = null
  try {
    return block(this)
  } catch (e: Throwable) {
    exception = e
    throw e
  } finally {
    when (exception) {
      null -> close()
      else -> try {
        close()
      } catch (ignoredCloseException: Throwable) {
      }
    }
  }
}

private val inTransaction = ThreadLocal<Boolean>()

internal inline fun <T> SQLiteDatabase.inTransaction(block: SQLiteDatabase.() -> T): T {
  if (inTransaction.getOrSet { false }) {
    return block()
  }
  try {
    inTransaction.set(true)
    beginTransaction()
    val result = block()
    setTransactionSuccessful()
    return result
  } finally {
    endTransaction()
    inTransaction.set(false)
  }
}

package leakcanary.internal.activity.db

import android.database.sqlite.SQLiteDatabase
import android.view.View
import leakcanary.internal.InternalLeakCanary
import leakcanary.internal.activity.db.Db.OnDb
import leakcanary.internal.activity.db.Db.dbHelper
import leakcanary.internal.activity.db.Io.OnIo

internal object Db {

  // Accessed on the IO thread only.
  private var dbHelper: LeaksDbHelper? = null

  interface OnDb : OnIo {
    val db: SQLiteDatabase
  }

  private class DbContext(override val db: SQLiteDatabase) : OnDb {
    var updateUi: (View.() -> Unit)? = null

    override fun updateUi(updateUi: View.() -> Unit) {
      this.updateUi = updateUi
    }
  }

  fun execute(
    view: View,
    block: OnDb.() -> Unit
  ) {
    val appContext = view.context.applicationContext
    Io.execute(view) {
      if (dbHelper == null) {
        dbHelper = LeaksDbHelper(appContext)
      }
      val dbBlock = DbContext(dbHelper!!.writableDatabase)
      block(dbBlock)
      val updateUi = dbBlock.updateUi
      if (updateUi != null) {
        updateUi(updateUi)
      }
    }
  }

  fun closeDatabase() {
    // Closing on the serial IO thread to ensure we don't close while using the db.
    Io.execute {
      dbHelper?.close()
    }
  }
}

internal fun View.executeOnDb(block: OnDb.() -> Unit) {
  Db.execute(this, block)
}

package leakcanary.internal.activity.db

import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import android.os.AsyncTask
import leakcanary.internal.LeakDirectoryProvider
import leakcanary.internal.Serializables
import leakcanary.internal.toByteArray
import leakcanary.internal.friendly.checkNotMainThread
import leakcanary.internal.friendly.mainHandler
import org.intellij.lang.annotations.Language
import shark.HeapAnalysis
import shark.HeapAnalysisFailure
import shark.HeapAnalysisSuccess
import shark.SharkLog
import java.io.File
import java.util.concurrent.CopyOnWriteArrayList

internal object HeapAnalysisTable {

  /**
   * CopyOnWriteArrayList because registered listeners can remove themselves from this list while
   * iterating and invoking them, which would trigger a ConcurrentModificationException (see #2019).
   */
  private val updateListeners = CopyOnWriteArrayList<() -> Unit>()

  @Language("RoomSql")
  const val create = """CREATE TABLE heap_analysis
        (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        created_at_time_millis INTEGER,
        dump_duration_millis INTEGER DEFAULT -1,
        leak_count INTEGER DEFAULT 0,
        exception_summary TEXT DEFAULT NULL,
        object BLOB
        )"""

  @Language("RoomSql")
  const val drop = "DROP TABLE IF EXISTS heap_analysis"

  fun onUpdate(block: () -> Unit): () -> Unit {
    updateListeners.add(block)
    return {
      updateListeners.remove(block)
    }
  }

  fun insert(
    db: SQLiteDatabase,
    heapAnalysis: HeapAnalysis
  ): Long {
    val values = ContentValues()
    values.put("created_at_time_millis", heapAnalysis.createdAtTimeMillis)
    values.put("dump_duration_millis", heapAnalysis.dumpDurationMillis)
    values.put("object", heapAnalysis.toByteArray())
    when (heapAnalysis) {
      is HeapAnalysisSuccess -> {
        val leakCount = heapAnalysis.applicationLeaks.size + heapAnalysis.libraryLeaks.size
        values.put("leak_count", leakCount)
      }
      is HeapAnalysisFailure -> {
        val cause = heapAnalysis.exception.cause!!
        val exceptionSummary = "${cause.javaClass.simpleName} ${cause.message}"
        values.put("exception_summary", exceptionSummary)
      }
    }

    return db.inTransaction {
      val heapAnalysisId = db.insertOrThrow("heap_analysis", null, values)
      if (heapAnalysis is HeapAnalysisSuccess) {
        heapAnalysis.allLeaks
          .forEach { leakingInstance ->
            LeakTable.insert(
              db, heapAnalysisId, leakingInstance
            )
          }
      }
      heapAnalysisId
    }.apply { notifyUpdateOnMainThread() }
  }

  private fun notifyUpdateOnMainThread() {
    checkNotMainThread()
    mainHandler.post {
      updateListeners.forEach { it() }
    }
  }

  inline fun <reified T : HeapAnalysis> retrieve(
    db: SQLiteDatabase,
    id: Long
  ): T? {
    return db.rawQuery(
      """
              SELECT
              object
              FROM heap_analysis
              WHERE id=$id
              """, null
    )
      .use { cursor ->
        if (cursor.moveToNext()) {
          val analysis = Serializables.fromByteArray<T>(cursor.getBlob(0))
          if (analysis == null) {
            delete(db, id, null)
          }
          analysis
        } else {
          null
        }
      }
  }

  fun retrieveAll(db: SQLiteDatabase): List<Projection> {
    return db.rawQuery(
      """
          SELECT
          id
          , created_at_time_millis
          , leak_count
          , exception_summary
          FROM heap_analysis
          ORDER BY created_at_time_millis DESC
          """, null
    )
      .use { cursor ->
        val all = mutableListOf<Projection>()
        while (cursor.moveToNext()) {
          val summary = Projection(
            id = cursor.getLong(0),
            createdAtTimeMillis = cursor.getLong(1),
            leakCount = cursor.getInt(2),
            exceptionSummary = cursor.getString(3)
          )
          all.add(summary)
        }
        all
      }
  }

  fun delete(
    db: SQLiteDatabase,
    heapAnalysisId: Long,
    heapDumpFile: File?
  ) {
    if (heapDumpFile != null) {
      AsyncTask.SERIAL_EXECUTOR.execute {
        val path = heapDumpFile.absolutePath
        val heapDumpDeleted = heapDumpFile.delete()
        if (heapDumpDeleted) {
          LeakDirectoryProvider.filesDeletedRemoveLeak += path
        } else {
          SharkLog.d { "Could not delete heap dump file ${heapDumpFile.path}" }
        }
      }
    }

    db.inTransaction {
      db.delete("heap_analysis", "id=$heapAnalysisId", null)
      LeakTable.deleteByHeapAnalysisId(db, heapAnalysisId)
    }
    notifyUpdateOnMainThread()
  }

  fun deleteAll(db: SQLiteDatabase) {
    db.inTransaction {
      rawQuery(
        """
              SELECT
              id,
              object
              FROM heap_analysis
              """, null
      )
        .use { cursor ->
          val all = mutableListOf<Pair<Long, HeapAnalysis>>()
          while (cursor.moveToNext()) {
            val id = cursor.getLong(0)
            val analysis = Serializables.fromByteArray<HeapAnalysis>(cursor.getBlob(1))
            if (analysis != null) {
              all += id to analysis
            }
          }
          all.forEach { (id, _) ->
            db.delete("heap_analysis", "id=$id", null)
            LeakTable.deleteByHeapAnalysisId(db, id)
          }
          AsyncTask.SERIAL_EXECUTOR.execute {
            all.forEach { (_, analysis) ->
              analysis.heapDumpFile.delete()
            }
          }
        }
    }
    notifyUpdateOnMainThread()
  }

  class Projection(
    val id: Long,
    val createdAtTimeMillis: Long,
    val leakCount: Int,
    val exceptionSummary: String?
  )
}

package leakcanary.internal.activity.db

import android.view.View
import leakcanary.internal.activity.db.Io.OnIo
import leakcanary.internal.navigation.onScreenExiting
import leakcanary.internal.friendly.checkMainThread
import leakcanary.internal.friendly.mainHandler
import java.util.concurrent.Executors

internal object Io {

  private val serialExecutor = Executors.newSingleThreadExecutor()

  interface OnIo {
    fun updateUi(updateUi: View.() -> Unit)
  }

  private class IoContext : OnIo {
    var updateUi: (View.() -> Unit)? = null

    override fun updateUi(updateUi: View.() -> Unit) {
      this.updateUi = updateUi
    }
  }

  fun execute(block: () -> Unit) {
    serialExecutor.execute(block)
  }

  fun execute(
    view: View,
    block: OnIo.() -> Unit
  ) {
    checkMainThread()
    val viewWrapper: VolatileObjectRef<View> = VolatileObjectRef(view)
    view.onScreenExiting {
      viewWrapper.element = null
    }
    serialExecutor.execute backgroundExecute@{
      if (viewWrapper.element == null) {
        return@backgroundExecute
      }
      val context = IoContext()
      block(context)
      val updateUi = context.updateUi
      if (viewWrapper.element != null && updateUi != null) {
        mainHandler.post mainThreadPost@{
          val attachedView = viewWrapper.element ?: return@mainThreadPost
          updateUi(attachedView)
        }
      }
    }
  }

  /**
   * Similar to kotlin.jvm.internal.Ref.ObjectRef but volatile
   */
  private class VolatileObjectRef<T>(
    @Volatile
    var element: T? = null
  )
}

internal fun View.executeOnIo(block: OnIo.() -> Unit) {
  Io.execute(this, block)
}

package leakcanary.internal.activity.db

import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import org.intellij.lang.annotations.Language
import shark.Leak
import shark.LibraryLeak

internal object LeakTable {

  @Language("RoomSql")
  const val create = """
        CREATE TABLE leak
        (
        id INTEGER PRIMARY KEY,
        signature TEXT UNIQUE,
        short_description TEXT,
        is_library_leak INTEGER,
        is_read INTEGER
        )"""

  @Language("RoomSql")
  const val createSignatureIndex = """
        CREATE INDEX leak_signature
        on leak (signature)
    """

  @Language("RoomSql")
  const val drop = "DROP TABLE IF EXISTS leak"

  fun insert(
    db: SQLiteDatabase,
    heapAnalysisId: Long,
    leak: Leak
  ): Long {
    val values = ContentValues()
    values.put("signature", leak.signature)
    values.put("short_description", leak.shortDescription)
    values.put("is_library_leak", if (leak is LibraryLeak) 1 else 0)
    values.put("is_read", 0)

    db.insertWithOnConflict("leak", null, values, SQLiteDatabase.CONFLICT_IGNORE)

    val leakId =
      db.rawQuery("SELECT id from leak WHERE signature = '${leak.signature}' LIMIT 1", null)
        .use { cursor ->
          if (cursor.moveToFirst()) cursor.getLong(0) else throw IllegalStateException(
            "No id found for leak with signature '${leak.signature}'"
          )
        }

    leak.leakTraces.forEachIndexed { index, leakTrace ->
      LeakTraceTable.insert(
        db = db,
        leakId = leakId,
        heapAnalysisId = heapAnalysisId,
        leakTraceIndex = index,
        leakingObjectClassSimpleName = leakTrace.leakingObject.classSimpleName
      )
    }

    return leakId
  }

  fun retrieveLeakReadStatuses(
    db: SQLiteDatabase,
    signatures: Set<String>
  ): Map<String, Boolean> {
    return db.rawQuery(
      """
      SELECT
      signature
      , is_read
      FROM leak
      WHERE signature IN (${signatures.joinToString { "'$it'" }})
    """, null
    )
      .use { cursor ->
        val leakReadStatuses = mutableMapOf<String, Boolean>()
        while (cursor.moveToNext()) {
          val signature = cursor.getString(0)
          val isRead = cursor.getInt(1) == 1
          leakReadStatuses[signature] = isRead
        }
        leakReadStatuses
      }
  }

  class AllLeaksProjection(
    val signature: String,
    val shortDescription: String,
    val createdAtTimeMillis: Long,
    val leakTraceCount: Int,
    val isLibraryLeak: Boolean,
    val isNew: Boolean
  )

  fun retrieveAllLeaks(
    db: SQLiteDatabase
  ): List<AllLeaksProjection> {
    return db.rawQuery(
      """
          SELECT
          l.signature
          , MIN(l.short_description)
          , MAX(h.created_at_time_millis) as created_at_time_millis
          , COUNT(*) as leak_trace_count
          , MIN(l.is_library_leak) as is_library_leak
          , MAX(l.is_read) as is_read
          FROM leak_trace lt
          LEFT JOIN leak l on lt.leak_id = l.id
          LEFT JOIN heap_analysis h ON lt.heap_analysis_id = h.id
          GROUP BY 1
          ORDER BY leak_trace_count DESC, created_at_time_millis DESC
          """, null
    )
      .use { cursor ->
        val all = mutableListOf<AllLeaksProjection>()
        while (cursor.moveToNext()) {
          val group = AllLeaksProjection(
            signature = cursor.getString(0),
            shortDescription = cursor.getString(1),
            createdAtTimeMillis = cursor.getLong(2),
            leakTraceCount = cursor.getInt(3),
            isLibraryLeak = cursor.getInt(4) == 1,
            isNew = cursor.getInt(5) == 0
          )
          all.add(group)
        }
        all
      }
  }

  fun markAsRead(
    db: SQLiteDatabase,
    signature: String
  ) {
    val values = ContentValues().apply { put("is_read", 1) }
    db.update("leak", values, "signature = ?", arrayOf(signature))
  }

  class LeakProjection(
    val shortDescription: String,
    val isNew: Boolean,
    val isLibraryLeak: Boolean,
    val leakTraces: List<LeakTraceProjection>
  )

  class LeakTraceProjection(
    val leakTraceIndex: Int,
    val heapAnalysisId: Long,
    val classSimpleName: String,
    val createdAtTimeMillis: Long
  )

  fun retrieveLeakBySignature(
    db: SQLiteDatabase,
    signature: String
  ): LeakProjection? {
    return db.rawQuery(
      """
          SELECT
          lt.leak_trace_index
          , lt.heap_analysis_id
          , lt.class_simple_name
          , h.created_at_time_millis
          , l.short_description
          , l.is_read
          , l.is_library_leak
          FROM leak_trace lt
          LEFT JOIN leak l on lt.leak_id = l.id
          LEFT JOIN heap_analysis h ON lt.heap_analysis_id = h.id
          WHERE l.signature = ?
          ORDER BY h.created_at_time_millis DESC
          """, arrayOf(signature)
    )
      .use { cursor ->
        return if (cursor.moveToFirst()) {
          val leakTraces = mutableListOf<LeakTraceProjection>()
          val leakProjection = LeakProjection(
            shortDescription = cursor.getString(4),
            isNew = cursor.getInt(5) == 0,
            isLibraryLeak = cursor.getInt(6) == 1,
            leakTraces = leakTraces
          )
          leakTraces.addAll(generateSequence(cursor, {
            if (cursor.moveToNext()) cursor else null
          }).map {
            LeakTraceProjection(
              leakTraceIndex = cursor.getInt(0),
              heapAnalysisId = cursor.getLong(1),
              classSimpleName = cursor.getString(2),
              createdAtTimeMillis = cursor.getLong(3)
            )
          })
          leakProjection
        } else {
          null
        }
      }
  }

  fun deleteByHeapAnalysisId(
    db: SQLiteDatabase,
    heapAnalysisId: Long
  ) {
    LeakTraceTable.deleteByHeapAnalysisId(db, heapAnalysisId)
    db.execSQL(
      """
      DELETE
      FROM leak
      WHERE NOT EXISTS (
      SELECT *
      FROM leak_trace lt
      WHERE leak.id = lt.leak_id)
    """
    )
  }
}

package leakcanary.internal.activity.db

import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import org.intellij.lang.annotations.Language

internal object LeakTraceTable {

  @Language("RoomSql")
  const val create = """
        CREATE TABLE leak_trace
        (
        id INTEGER PRIMARY KEY,
        heap_analysis_id REFERENCES heap_analysis(id),
        leak_id REFERENCES leak(id),
        class_simple_name TEXT,
        leak_trace_index INTEGER
        )"""

  @Language("RoomSql")
  const val drop = "DROP TABLE IF EXISTS leak_trace"

  fun insert(
    db: SQLiteDatabase,
    leakId: Long,
    heapAnalysisId: Long,
    leakTraceIndex: Int,
    leakingObjectClassSimpleName: String
  ): Long {
    val values = ContentValues()
    values.put("heap_analysis_id", heapAnalysisId)
    values.put("leak_id", leakId)
    values.put("class_simple_name", leakingObjectClassSimpleName)
    values.put("leak_trace_index", leakTraceIndex)
    return db.insertOrThrow("leak_trace", null, values)
  }

  fun deleteByHeapAnalysisId(
    db: SQLiteDatabase,
    heapAnalysisId: Long
  ) {
    db.delete("leak_trace", "heap_analysis_id=$heapAnalysisId", null)
  }
}

package leakcanary.internal.activity.db

import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import leakcanary.internal.Serializables
import leakcanary.internal.toByteArray
import shark.HeapAnalysis
import shark.HeapAnalysisSuccess
import shark.LeakTrace

internal class LeaksDbHelper(context: Context) : SQLiteOpenHelper(
  context, DATABASE_NAME, null, VERSION
) {

  override fun onCreate(db: SQLiteDatabase) {
    db.execSQL(HeapAnalysisTable.create)
    db.execSQL(LeakTable.create)
    db.execSQL(LeakTable.createSignatureIndex)
    db.execSQL(LeakTraceTable.create)
  }

  override fun onUpgrade(
    db: SQLiteDatabase,
    oldVersion: Int,
    newVersion: Int
  ) {
    if (oldVersion < 23) {
      recreateDb(db)
      return
    }
    if (oldVersion < 24) {
      db.execSQL("ALTER TABLE heap_analysis ADD COLUMN dump_duration_millis INTEGER DEFAULT -1")
    }
    if (oldVersion < 25) {
      // Fix owningClassName=null in the serialized heap analysis.
      // https://github.com/square/leakcanary/issues/2067
      val idToAnalysis = db.rawQuery("SELECT id, object FROM heap_analysis", null)
        .use { cursor ->
          generateSequence {
            if (cursor.moveToNext()) {
              val id = cursor.getLong(0)
              val analysis = Serializables.fromByteArray<HeapAnalysis>(cursor.getBlob(1))
              id to analysis
            } else {
              null
            }
          }
            .filter {
              it.second is HeapAnalysisSuccess
            }
            .map { pair ->
              val analysis = pair.second as HeapAnalysisSuccess

              val unreachableObjects = try {
                analysis.unreachableObjects
              } catch (ignored: NullPointerException) {
                // This currently doesn't trigger but the Kotlin compiler might change one day.
                emptyList()
              } ?: emptyList() // Compiler doesn't know it but runtime can have null.
              pair.first to analysis.copy(
                unreachableObjects = unreachableObjects,
                applicationLeaks = analysis.applicationLeaks.map { leak ->
                  leak.copy(leak.leakTraces.fixNullReferenceOwningClassName())
                },
                libraryLeaks = analysis.libraryLeaks.map { leak ->
                  leak.copy(leak.leakTraces.fixNullReferenceOwningClassName())
                }
              )
            }.toList()
        }
      db.inTransaction {
        idToAnalysis.forEach { (id, heapAnalysis) ->
          val values = ContentValues()
          values.put("object", heapAnalysis.toByteArray())
          db.update("heap_analysis", values, "id=$id", null)
        }
      }
    }
  }

  private fun List<LeakTrace>.fixNullReferenceOwningClassName(): List<LeakTrace> {
    return map { leakTrace ->
      leakTrace.copy(
        referencePath = leakTrace.referencePath.map { reference ->
          val owningClassName = try {
            // This can return null at runtime from previous serialized version without the field.
            reference.owningClassName
          } catch (ignored: NullPointerException) {
            // This currently doesn't trigger but the Kotlin compiler might change one day.
            null
          }
          if (owningClassName == null) {
            reference.copy(owningClassName = reference.originObject.classSimpleName)
          } else {
            reference
          }
        })
    }
  }

  override fun onDowngrade(
    db: SQLiteDatabase,
    oldVersion: Int,
    newVersion: Int
  ) {
    recreateDb(db)
  }

  private fun recreateDb(db: SQLiteDatabase) {
    db.execSQL(HeapAnalysisTable.drop)
    db.execSQL(LeakTable.drop)
    db.execSQL(LeakTraceTable.drop)
    onCreate(db)
  }

  companion object {
    internal const val VERSION = 25
    internal const val DATABASE_NAME = "leaks.db"
  }
}

package leakcanary.internal.activity.screen

import android.content.Context
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.ViewGroup
import android.widget.Switch
import android.widget.TextView
import com.squareup.leakcanary.core.BuildConfig
import com.squareup.leakcanary.core.R
import leakcanary.internal.HeapDumpControl
import leakcanary.internal.HeapDumpControl.ICanHazHeap.Nope
import leakcanary.internal.HeapDumpControl.ICanHazHeap.Yup
import leakcanary.internal.InternalLeakCanary
import leakcanary.internal.navigation.Screen
import leakcanary.internal.navigation.activity
import leakcanary.internal.navigation.inflate

internal class AboutScreen : Screen() {
  override fun createView(container: ViewGroup) =
    container.inflate(R.layout.leak_canary_about_screen)
      .apply {
        activity.title =
          resources.getString(R.string.leak_canary_about_title, BuildConfig.LIBRARY_VERSION)
        val aboutTextView = findViewById<TextView>(R.id.leak_canary_about_text)
        aboutTextView.movementMethod = LinkMovementMethod.getInstance()
        val application = activity.application
        val appName = application.packageManager.getApplicationLabel(application.applicationInfo)
        val appPackageName = context.packageName

        aboutTextView.text = Html.fromHtml(
          String.format(
            resources.getString(R.string.leak_canary_about_message), appName, appPackageName
          )
        )

        val heapDumpTextView = findViewById<TextView>(R.id.leak_canary_about_heap_dump_text)
        updateHeapDumpTextView(heapDumpTextView)
        val heapDumpSwitchView =
          findViewById<Switch>(R.id.leak_canary_about_heap_dump_switch_button)
        heapDumpSwitchView.isChecked = InternalLeakCanary.dumpEnabledInAboutScreen
        heapDumpSwitchView.setOnCheckedChangeListener { _, checked ->
          // Updating the value wouldn't normally immediately trigger a heap dump, however
          // by updating the view we also have a side effect of querying which will notify
          // the heap dumper if the value has become positive.
          InternalLeakCanary.dumpEnabledInAboutScreen = checked
          updateHeapDumpTextView(heapDumpTextView)
        }
      }

  private fun updateHeapDumpTextView(view: TextView) {
    view.text = when (val iCanHasHeap = HeapDumpControl.iCanHasHeap()) {
      is Yup -> view.resources.getString(R.string.leak_canary_heap_dump_enabled_text)
      is Nope -> view.resources.getString(
        R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
      )

    }
  }
}

package leakcanary.internal.activity.screen

import android.app.ActivityManager
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.squareup.leakcanary.core.R
import leakcanary.internal.HeapAnalyzerService
import leakcanary.internal.activity.db.HeapAnalysisTable
import leakcanary.internal.activity.db.executeOnDb
import leakcanary.internal.activity.shareHeapDump
import leakcanary.internal.activity.shareToGitHubIssue
import leakcanary.internal.activity.ui.UiUtils
import leakcanary.internal.navigation.Screen
import leakcanary.internal.navigation.activity
import leakcanary.internal.navigation.goBack
import leakcanary.internal.navigation.inflate
import leakcanary.internal.navigation.onCreateOptionsMenu
import shark.HeapAnalysisFailure

internal class HeapAnalysisFailureScreen(
  private val analysisId: Long
) : Screen() {

  override fun createView(container: ViewGroup) =
    container.inflate(R.layout.leak_canary_heap_analysis_failure_screen).apply {
      activity.title = resources.getString(R.string.leak_canary_loading_title)
      executeOnDb {
        val heapAnalysis = HeapAnalysisTable.retrieve<HeapAnalysisFailure>(db, analysisId)
        if (heapAnalysis == null) {
          updateUi {
            activity.title = resources.getString(R.string.leak_canary_analysis_deleted_title)
          }
        } else {
          val heapDumpFileExist = heapAnalysis.heapDumpFile.exists()
          updateUi { onFailureRetrieved(heapAnalysis, heapDumpFileExist) }
        }
      }
    }

  private fun View.onFailureRetrieved(
    heapAnalysis: HeapAnalysisFailure,
    heapDumpFileExist: Boolean
  ) {
    activity.title = resources.getString(R.string.leak_canary_analysis_failed)

    val failureText =
      if (heapDumpFileExist) {
        "You can run the analysis again.

"
      } else {
        ""
      } + """
      Please click here to file a bug report.
      The stacktrace details will be copied into the clipboard and you just need to paste into the
      GitHub issue description.""" + (if (heapDumpFileExist) {
        """

To help reproduce the issue, please share the
        Heap Dump file and upload it to the GitHub issue.
      """
      } else "")

    val failure = Html.fromHtml(failureText) as SpannableStringBuilder

    UiUtils.replaceUrlSpanWithAction(failure) { urlSpan ->
      when (urlSpan) {
        "file_issue" -> {
          {
            shareToGitHubIssue(heapAnalysis)
          }
        }
        "share_hprof" -> {
          {
            shareHeapDump(heapAnalysis.heapDumpFile)
          }
        }
        "try_again" -> {
          {
            HeapAnalyzerService.runAnalysis(
              context = context,
              heapDumpFile = heapAnalysis.heapDumpFile,
              heapDumpDurationMillis = heapAnalysis.dumpDurationMillis,
              heapDumpReason = "Retrying heap analysis after failure."
            )
          }
        }
        else -> null
      }
    }
    findViewById<TextView>(R.id.leak_canary_header_text).apply {
      movementMethod = LinkMovementMethod.getInstance()
      text = failure
    }

    findViewById<TextView>(R.id.leak_canary_stacktrace).text = heapAnalysis.exception.toString()

    onCreateOptionsMenu { menu ->
      if (!ActivityManager.isUserAMonkey()) {
        menu.add(R.string.leak_canary_delete)
          .setOnMenuItemClickListener {
            executeOnDb {
              HeapAnalysisTable.delete(db, analysisId, heapAnalysis.heapDumpFile)
              updateUi {
                goBack()
              }
            }
            true
          }
      }
    }
  }
}

package leakcanary.internal.activity.screen

import android.content.Context
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Bitmap.Config.ARGB_8888
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Paint.Style.FILL
import android.graphics.Paint.Style.STROKE
import android.graphics.Rect
import com.squareup.leakcanary.core.R
import leakcanary.internal.navigation.getColorCompat
import shark.HprofRecord
import shark.HprofRecord.HeapDumpEndRecord
import shark.HprofRecord.HeapDumpRecord.GcRootRecord
import shark.HprofRecord.HeapDumpRecord.HeapDumpInfoRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.InstanceDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ObjectArrayDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.BooleanArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ByteArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.CharArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.DoubleArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.FloatArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.IntArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.LongArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ShortArrayDump
import shark.HprofRecord.LoadClassRecord
import shark.HprofRecord.StackTraceRecord
import shark.HprofRecord.StringRecord
import shark.StreamingHprofReader
import shark.OnHprofRecordListener
import shark.StreamingRecordReaderAdapter.Companion.asStreamingRecordReader
import java.io.File

internal object HeapDumpRenderer {

  private class HasDensity(resources: Resources) {
    val density = resources.displayMetrics.density

    val Int.dp
      get() = this * density

    val Float.dp
      get() = this * density
  }

  @Suppress("LongMethod")
  fun render(
    context: Context,
    heapDumpFile: File,
    sourceWidth: Int,
    sourceHeight: Int,
    /**
     * If [sourceBytesPerPixel] > 0 then [sourceHeight] will be ignored.
     */
    sourceBytesPerPixel: Int
  ): Bitmap = with(HasDensity(context.resources)) {
    val recordPositions = mutableListOf<Pair<Int, Long>>()
    var currentRecord: HprofRecord? = null

    val otherColor = context.getColorCompat(R.color.leak_canary_heap_other)
    val stackTraceColor = context.getColorCompat(R.color.leak_canary_heap_stack_trace)
    val hprofStringColor = context.getColorCompat(R.color.leak_canary_heap_hprof_string)
    val loadClassColor = context.getColorCompat(R.color.leak_canary_heap_load_class)
    val classDumpColor = context.getColorCompat(R.color.leak_canary_heap_class_dump)
    val instanceColor = context.getColorCompat(R.color.leak_canary_heap_instance)
    val objectArrayColor = context.getColorCompat(R.color.leak_canary_heap_object_array)
    val booleanArrayColor = context.getColorCompat(R.color.leak_canary_heap_boolean_array)
    val charArrayColor = context.getColorCompat(R.color.leak_canary_heap_char_array)
    val floatArrayColor = context.getColorCompat(R.color.leak_canary_heap_float_array)
    val doubleArrayColor = context.getColorCompat(R.color.leak_canary_heap_double_array)
    val byteArrayColor = context.getColorCompat(R.color.leak_canary_heap_byte_array)
    val shortArrayColor = context.getColorCompat(R.color.leak_canary_heap_short_array)
    val intArrayColor = context.getColorCompat(R.color.leak_canary_heap_int_array)
    val longArrayColor = context.getColorCompat(R.color.leak_canary_heap_long_array)
    val colors = mapOf(
      StringRecord::class to hprofStringColor,
      LoadClassRecord::class to loadClassColor,
      ClassDumpRecord::class to classDumpColor,
      InstanceDumpRecord::class to instanceColor,
      ObjectArrayDumpRecord::class to objectArrayColor,
      BooleanArrayDump::class to booleanArrayColor,
      CharArrayDump::class to charArrayColor,
      FloatArrayDump::class to floatArrayColor,
      DoubleArrayDump::class to doubleArrayColor,
      ByteArrayDump::class to byteArrayColor,
      ShortArrayDump::class to shortArrayColor,
      IntArrayDump::class to intArrayColor,
      LongArrayDump::class to longArrayColor,
      StackTraceRecord::class to stackTraceColor,
      HeapDumpEndRecord::class to otherColor,
      GcRootRecord::class to otherColor
    )

    val appHeapColor = context.getColorCompat(R.color.leak_canary_heap_app)
    val imageHeapColor = context.getColorCompat(R.color.leak_canary_heap_image)
    val zygoteHeapColor = context.getColorCompat(R.color.leak_canary_heap_zygote)
    val stringColor = context.getColorCompat(R.color.leak_canary_heap_instance_string)

    var lastPosition = 0L

    val reader = StreamingHprofReader.readerFor(heapDumpFile).asStreamingRecordReader()
    val hprofStringCache = mutableMapOf<Long, String>()
    val classNames = mutableMapOf<Long, Long>()
    reader.readRecords(
      setOf(HprofRecord::class), OnHprofRecordListener { position, record ->
      lastPosition = position
      when (record) {
        is StringRecord -> {
          hprofStringCache[record.id] = record.string
        }
        is LoadClassRecord -> {
          classNames[record.id] = record.classNameStringId
        }
      }
      val localCurrentRecord = currentRecord
      when {
        localCurrentRecord is HeapDumpInfoRecord -> {
          val colorForHeapInfo =
            when (hprofStringCache[localCurrentRecord.heapNameStringId]) {
              // The primary heap on which your app allocates memory.
              "app" -> appHeapColor
              // The system boot image, containing classes that are preloaded during boot time.
              // Allocations here are guaranteed to never move or go away.
              "image" -> imageHeapColor
              // The copy-on-write heap where an app process is forked from in the Android system.
              "zygote" -> zygoteHeapColor
              // JNI heap: The heap that shows where Java Native Interface (JNI) references are allocated and released.
              // default heap: When no heap is specified by the system
              else -> otherColor
            }
          recordPositions.add(colorForHeapInfo to position)
          currentRecord = record
        }
        localCurrentRecord is InstanceDumpRecord
          && hprofStringCache[classNames[localCurrentRecord.classId]] == "java.lang.String"
          && (record !is InstanceDumpRecord || hprofStringCache[classNames[record.classId]]
          != "java.lang.String")
        -> {
          recordPositions.add(stringColor to position)
          currentRecord = record
        }
        currentRecord == null -> {
          recordPositions.add(otherColor to position)
          currentRecord = record
        }
        currentRecord!!::class != record::class -> {
          recordPositions.add(colors.getValue(currentRecord!!::class) to position)
          currentRecord = record
        }
      }
    })
    val heapLength = lastPosition

    val width = sourceWidth
    var height: Int
    val bytesPerPixel: Double

    if (sourceBytesPerPixel > 0) {
      bytesPerPixel = sourceBytesPerPixel.toDouble()
      height = Math.ceil((heapLength / bytesPerPixel) / width)
        .toInt()
    } else {
      height = sourceHeight
      bytesPerPixel = heapLength * 1.0 / (width * height)
    }

    val bitmap: Bitmap =
      Bitmap.createBitmap(width, height, ARGB_8888)

    val canvas = Canvas(bitmap)

    val legend = mapOf(
      "Hprof string" to hprofStringColor,
      "Class name" to loadClassColor,
      "App heap" to appHeapColor,
      "Image heap" to imageHeapColor,
      "Zygote heap" to zygoteHeapColor,
      "Other heap" to otherColor,
      "Class content" to classDumpColor,
      "Instance" to instanceColor,
      "String" to stringColor,
      "Object array" to objectArrayColor,
      "Boolean array" to booleanArrayColor,
      "Char array" to charArrayColor,
      "Float array" to floatArrayColor,
      "Double array" to doubleArrayColor,
      "Byte array" to byteArrayColor,
      "Short array" to shortArrayColor,
      "Int array" to intArrayColor,
      "Long array" to longArrayColor,
      "Stack trace" to stackTraceColor,
      "Heap End" to otherColor
    )

    val legendTextPaint = Paint(Paint.ANTI_ALIAS_FLAG)

    legendTextPaint.color = Color.WHITE
    legendTextPaint.style = FILL
    canvas.drawPaint(legendTextPaint)

    val legendSquareFillPaint = Paint()
    legendSquareFillPaint.style = FILL
    val legendSquareStrokePaint = Paint()
    legendSquareStrokePaint.style = STROKE
    legendSquareStrokePaint.strokeWidth = 0.8f.dp
    legendSquareStrokePaint.color = Color.BLACK

    legendTextPaint.color = Color.BLACK
    legendTextPaint.textSize = 16.dp

    val metrics = legendTextPaint.fontMetrics
    val textHeight = metrics.descent - metrics.ascent

    val xBounds = Rect()
    legendTextPaint.getTextBounds("x", 0, 1, xBounds)
    val squareSize = xBounds.height()
    val squarePaddingTop = (textHeight - squareSize) / 2
    val squareToTextPadding = 4.dp
    val blockToBlockPadding = 8.dp

    var maxTextWidth = 0f
    for (name in legend.keys) {
      maxTextWidth = Math.max(maxTextWidth, legendTextPaint.measureText(name))
    }

    val padding = 8.dp
    var blockLeft = padding
    var blockTop = padding
    val legendWidth = width - 2 * padding
    for ((name, color) in legend) {
      if (blockLeft + squareSize + squareToTextPadding + maxTextWidth > legendWidth) {
        blockLeft = padding
        blockTop += textHeight
      }

      legendSquareFillPaint.color = color
      canvas.drawRect(
        blockLeft, blockTop + squarePaddingTop, blockLeft + squareSize,
        blockTop + squarePaddingTop + squareSize,
        legendSquareFillPaint
      )
      canvas.drawRect(
        blockLeft, blockTop + squarePaddingTop, blockLeft + squareSize,
        blockTop + squarePaddingTop + squareSize,
        legendSquareStrokePaint
      )
      blockLeft += squareSize + squareToTextPadding
      canvas.drawText(name, blockLeft, blockTop - metrics.ascent, legendTextPaint)
      blockLeft += maxTextWidth
      blockLeft += blockToBlockPadding
    }
    val legendHeight = blockTop + textHeight + padding
    val source = Rect(0, 0, width, legendHeight.toInt())
    val destination = Rect(0, (height - legendHeight).toInt(), width, height)
    canvas.drawBitmap(bitmap, source, destination, null)
    height -= legendHeight.toInt()

    val pixelPaint = Paint()
    pixelPaint.style = FILL

    var recordIndex = 0
    for (y in 0 until height) {
      for (x in 0 until width) {
        val bitmapPosition = y * width + x
        val heapPosition = (bitmapPosition * bytesPerPixel).toInt()
        while (heapPosition > recordPositions[recordIndex].second && recordIndex < recordPositions.lastIndex) {
          recordIndex++
        }
        pixelPaint.color = recordPositions[recordIndex].first
        canvas.drawPoint(x.toFloat(), y.toFloat(), pixelPaint)
      }
    }
    return bitmap
  }
}

package leakcanary.internal.activity.screen

import android.R.drawable
import android.R.string
import android.app.ActivityManager
import android.app.AlertDialog.Builder
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ListView
import android.widget.TextView
import com.squareup.leakcanary.core.R
import com.squareup.leakcanary.core.R.layout
import leakcanary.internal.activity.db.HeapAnalysisTable
import leakcanary.internal.activity.db.LeakTable
import leakcanary.internal.activity.db.executeOnDb
import leakcanary.internal.activity.share
import leakcanary.internal.activity.shareHeapDump
import leakcanary.internal.activity.ui.TimeFormatter
import leakcanary.internal.activity.ui.UiUtils
import leakcanary.internal.navigation.Screen
import leakcanary.internal.navigation.activity
import leakcanary.internal.navigation.goBack
import leakcanary.internal.navigation.goTo
import leakcanary.internal.navigation.inflate
import leakcanary.internal.navigation.onCreateOptionsMenu
import shark.HeapAnalysis
import shark.HeapAnalysisSuccess
import shark.LibraryLeak
import shark.SharkLog

internal class HeapDumpScreen(
  private val analysisId: Long
) : Screen() {

  override fun createView(container: ViewGroup) =
    container.inflate(R.layout.leak_canary_list).apply {
      activity.title = resources.getString(R.string.leak_canary_loading_title)

      executeOnDb {
        val heapAnalysis = HeapAnalysisTable.retrieve<HeapAnalysisSuccess>(db, analysisId)
        if (heapAnalysis == null) {
          updateUi {
            activity.title = resources.getString(R.string.leak_canary_analysis_deleted_title)
          }
        } else {
          val signatures = heapAnalysis.allLeaks.map { it.signature }
            .toSet()
          val leakReadStatus = LeakTable.retrieveLeakReadStatuses(db, signatures)
          val heapDumpFileExist = heapAnalysis.heapDumpFile.exists()
          updateUi { onSuccessRetrieved(heapAnalysis, leakReadStatus, heapDumpFileExist) }
        }
      }
    }

  private fun View.onSuccessRetrieved(
    heapAnalysis: HeapAnalysisSuccess,
    leakReadStatus: Map<String, Boolean>,
    heapDumpFileExist: Boolean
  ) {

    activity.title = TimeFormatter.formatTimestamp(context, heapAnalysis.createdAtTimeMillis)

    onCreateOptionsMenu { menu ->
      if (!ActivityManager.isUserAMonkey()) {
        menu.add(R.string.leak_canary_delete)
          .setOnMenuItemClickListener {
            executeOnDb {
              HeapAnalysisTable.delete(db, analysisId, heapAnalysis.heapDumpFile)
              updateUi {
                goBack()
              }
            }
            true
          }
      }
      if (heapDumpFileExist) {
        menu.add(R.string.leak_canary_options_menu_render_heap_dump)
          .setOnMenuItemClickListener {
            goTo(RenderHeapDumpScreen(heapAnalysis.heapDumpFile))
            true
          }
      }
    }

    val listView = findViewById<ListView>(R.id.leak_canary_list)

    val leaks = heapAnalysis.allLeaks.sortedByDescending { it.leakTraces.size }
      .toList()

    listView.adapter = object : BaseAdapter() {
      override fun getView(
        position: Int,
        convertView: View?,
        parent: ViewGroup
      ) = when (getItemViewType(position)) {
        METADATA -> {
          bindMetadataRow(convertView, parent, heapDumpFileExist, heapAnalysis)
        }
        LEAK_TITLE -> {
          val view = convertView ?: parent.inflate(R.layout.leak_canary_heap_dump_leak_title)
          val leaksTextView = view.findViewById<TextView>(R.id.leak_canary_heap_dump_leaks)
          leaksTextView.text = resources.getQuantityString(
            R.plurals.leak_canary_distinct_leaks,
            leaks.size, leaks.size
          )
          view
        }
        LEAK_ROW -> {
          val view = convertView ?: parent.inflate(R.layout.leak_canary_leak_row)
          val countView = view.findViewById<TextView>(R.id.leak_canary_count_text)
          val descriptionView = view.findViewById<TextView>(R.id.leak_canary_leak_text)
          val timeView = view.findViewById<TextView>(R.id.leak_canary_time_text)
          val newChipView = view.findViewById<TextView>(R.id.leak_canary_chip_new)
          val libraryLeakChipView = view.findViewById<TextView>(R.id.leak_canary_chip_library_leak)

          val leak = leaks[position - 2]

          val isNew = !leakReadStatus.getValue(leak.signature)

          countView.isEnabled = isNew
          countView.text = leak.leakTraces.size.toString()
          newChipView.visibility = if (isNew) VISIBLE else GONE
          libraryLeakChipView.visibility = if (leak is LibraryLeak) VISIBLE else GONE
          descriptionView.text = leak.shortDescription

          val formattedDate =
            TimeFormatter.formatTimestamp(view.context, heapAnalysis.createdAtTimeMillis)
          timeView.text = formattedDate
          view
        }
        else -> {
          throw IllegalStateException("Unexpected type ${getItemViewType(position)}")
        }
      }

      override fun getItem(position: Int) = this

      override fun getItemId(position: Int) = position.toLong()

      override fun getCount() = 2 + leaks.size

      override fun getItemViewType(position: Int) = when (position) {
        0 -> METADATA
        1 -> LEAK_TITLE
        else -> LEAK_ROW
      }

      override fun getViewTypeCount() = 3

      override fun isEnabled(position: Int) = getItemViewType(position) == LEAK_ROW
    }

    listView.setOnItemClickListener { _, _, position, _ ->
      if (position > LEAK_TITLE) {
        goTo(LeakScreen(leaks[position - 2].signature, analysisId))
      }
    }
  }

  private fun View.bindMetadataRow(
    convertView: View?,
    parent: ViewGroup,
    heapDumpFileExist: Boolean,
    heapAnalysis: HeapAnalysisSuccess
  ): View {
    val view = convertView ?: parent.inflate(layout.leak_canary_leak_header)
    val textView = view.findViewById<TextView>(R.id.leak_canary_header_text)
    textView.movementMethod = LinkMovementMethod.getInstance()

    val explore =
      if (heapDumpFileExist) """Explore Heap Dump

""" else ""
    val shareAnalysis = """Share Heap Dump analysis

"""
    val printAnalysis = """Print analysis to Logcat (tag: LeakCanary)

"""
    val shareFile =
      if (heapDumpFileExist) """Share Heap Dump file

""" else ""

    val seeMetadata = "See Metadata"

    val dumpDurationMillis =
      if (heapAnalysis.dumpDurationMillis != HeapAnalysis.DUMP_DURATION_UNKNOWN) {
        "${heapAnalysis.dumpDurationMillis} ms"
      } else {
        "Unknown"
      }
    val metadata = (heapAnalysis.metadata + mapOf(
      "Analysis duration" to "${heapAnalysis.analysisDurationMillis} ms",
      "Heap dump file path" to heapAnalysis.heapDumpFile.absolutePath,
      "Heap dump timestamp" to "${heapAnalysis.createdAtTimeMillis}",
      "Heap dump duration" to dumpDurationMillis
    ))
      .map { "${it.key}: ${it.value}" }
      .joinToString("
")
    val titleText = explore + shareAnalysis + printAnalysis + shareFile + seeMetadata
    val title = Html.fromHtml(titleText) as SpannableStringBuilder

    UiUtils.replaceUrlSpanWithAction(title) { urlSpan ->
      when (urlSpan) {
        "explore_hprof" -> {
          {
            goTo(HprofExplorerScreen(heapAnalysis.heapDumpFile))
          }
        }
        "share" -> {
          {
            share(LeakTraceWrapper.wrap(heapAnalysis.toString(), 80))
          }
        }
        "print" -> {
          {
            SharkLog.d { "\u200B\n" + LeakTraceWrapper.wrap(heapAnalysis.toString(), 120) }
          }
        }
        "share_hprof" -> {
          {
            shareHeapDump(heapAnalysis.heapDumpFile)
          }
        }
        "metadata" -> {
          {
            Builder(context)
              .setIcon(drawable.ic_dialog_info)
              .setTitle("Metadata")
              .setMessage(Html.fromHtml(metadata))
              .setPositiveButton(string.ok, null)
              .show()
          }
        }
        else -> null
      }
    }

    textView.text = title
    return view
  }

  companion object {
    const val METADATA = 0
    const val LEAK_TITLE = 1
    const val LEAK_ROW = 2
  }
}

package leakcanary.internal.activity.screen

import android.app.ActivityManager
import android.app.AlertDialog
import android.view.View
import android.view.ViewGroup
import android.widget.ListView
import android.widget.TextView
import com.squareup.leakcanary.core.R
import leakcanary.LeakCanary
import leakcanary.internal.activity.LeakActivity
import leakcanary.internal.activity.db.HeapAnalysisTable
import leakcanary.internal.activity.db.HeapAnalysisTable.Projection
import leakcanary.internal.activity.db.executeOnDb
import leakcanary.internal.activity.ui.SimpleListAdapter
import leakcanary.internal.activity.ui.TimeFormatter
import leakcanary.internal.navigation.NavigatingActivity
import leakcanary.internal.navigation.Screen
import leakcanary.internal.navigation.activity
import leakcanary.internal.navigation.goTo
import leakcanary.internal.navigation.inflate
import leakcanary.internal.navigation.onCreateOptionsMenu
import leakcanary.internal.navigation.onScreenExiting

internal class HeapDumpsScreen : Screen() {
  override fun createView(container: ViewGroup) =
    container.inflate(R.layout.leak_canary_heap_dumps_screen).apply {

      val unsubscribeRefresh = HeapAnalysisTable.onUpdate {
        activity<NavigatingActivity>().refreshCurrentScreen()
      }

      onScreenExiting { unsubscribeRefresh() }

      onCreateOptionsMenu { menu ->
        if (!ActivityManager.isUserAMonkey()) {
          menu.add(R.string.leak_canary_delete_all)
            .setOnMenuItemClickListener {
              AlertDialog.Builder(context)
                .setIcon(android.R.drawable.ic_dialog_alert)
                .setTitle(R.string.leak_canary_delete_all)
                .setMessage(R.string.leak_canary_delete_all_leaks_title)
                .setPositiveButton(android.R.string.ok) { _, _ ->
                  executeOnDb {
                    HeapAnalysisTable.deleteAll(db)
                    updateUi {
                      val listView = findViewById<ListView>(R.id.leak_canary_list)
                      listView.adapter =
                        SimpleListAdapter(
                          R.layout.leak_canary_simple_row, emptyList<Any>()
                        ) { _, _ -> }
                    }
                  }
                }
                .setNegativeButton(android.R.string.cancel, null)
                .show()
              true
            }
        }
      }

      findViewById<View>(R.id.leak_canary_import_heap_dump).setOnClickListener {
        activity<LeakActivity>().requestImportHprof()
      }

      findViewById<View>(R.id.leak_canary_dump_heap_now).setOnClickListener {
        LeakCanary.dumpHeap()
      }

      executeOnDb {
        val projections = HeapAnalysisTable.retrieveAll(db)
        updateUi { onAnalysesRetrieved(projections) }
      }

    }

  private fun View.onAnalysesRetrieved(projections: List<Projection>) {
    activity.title = resources.getQuantityString(
      R.plurals.leak_canary_heap_analysis_list_screen_title,
      projections.size, projections.size
    )

    val listView = findViewById<ListView>(R.id.leak_canary_list)

    listView.setOnItemClickListener { _, _, position, _ ->
      val projection = projections[position]
      val analysisScreen = if (projection.exceptionSummary != null) {
        HeapAnalysisFailureScreen(projection.id)
      } else {
        HeapDumpScreen(projection.id)
      }
      goTo(analysisScreen)
    }

    listView.adapter =
      SimpleListAdapter(R.layout.leak_canary_leak_row, projections) { view, position ->
        val goneView = view.findViewById<TextView>(R.id.leak_canary_count_text)
        goneView.visibility = View.GONE
        val timeView = view.findViewById<TextView>(R.id.leak_canary_leak_text)
        val countView = view.findViewById<TextView>(R.id.leak_canary_time_text)

        val projection = getItem(position)
        // Enable means "new"
        countView.isEnabled = false

        timeView.text = TimeFormatter.formatTimestamp(view.context, projection.createdAtTimeMillis)

        val count = projection.exceptionSummary ?: resources.getQuantityString(
          R.plurals.leak_canary_distinct_leaks,
          projection.leakCount, projection.leakCount
        )
        countView.text = count
      }
  }
}

package leakcanary.internal.activity.screen

import android.app.AlertDialog
import android.view.View
import android.view.View.OnAttachStateChangeListener
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ListView
import android.widget.TextView
import android.widget.Toast
import com.squareup.leakcanary.core.R
import leakcanary.internal.activity.db.Io
import leakcanary.internal.activity.db.executeOnIo
import leakcanary.internal.activity.ui.SimpleListAdapter
import leakcanary.internal.navigation.Screen
import leakcanary.internal.navigation.activity
import leakcanary.internal.navigation.inflate
import shark.HeapField
import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import shark.HeapObject.HeapObjectArray
import shark.HeapObject.HeapPrimitiveArray
import shark.HeapValue
import shark.HprofHeapGraph.Companion.openHeapGraph
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.BooleanArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ByteArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.CharArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.DoubleArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.FloatArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.IntArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.LongArrayDump
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ShortArrayDump
import shark.ValueHolder.BooleanHolder
import shark.ValueHolder.ByteHolder
import shark.ValueHolder.CharHolder
import shark.ValueHolder.DoubleHolder
import shark.ValueHolder.FloatHolder
import shark.ValueHolder.IntHolder
import shark.ValueHolder.LongHolder
import shark.ValueHolder.ReferenceHolder
import shark.ValueHolder.ShortHolder
import java.io.Closeable
import java.io.File

internal class HprofExplorerScreen(
  private val heapDumpFile: File
) : Screen() {
  override fun createView(container: ViewGroup) =
    container.inflate(R.layout.leak_canary_hprof_explorer).apply {
      container.activity.title = resources.getString(R.string.leak_canary_loading_title)

      var closeable: Closeable? = null

      addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
        override fun onViewAttachedToWindow(view: View) {
        }

        override fun onViewDetachedFromWindow(view: View) {
          Io.execute {
            closeable?.close()
          }
        }
      })

      executeOnIo {
        val graph = heapDumpFile.openHeapGraph()
        closeable = graph
        updateUi {
          container.activity.title =
            resources.getString(R.string.leak_canary_explore_heap_dump)
          val titleView = findViewById<TextView>(R.id.leak_canary_explorer_title)
          val searchView = findViewById<View>(R.id.leak_canary_search_button)
          val listView = findViewById<ListView>(R.id.leak_canary_explorer_list)
          titleView.visibility = VISIBLE
          searchView.visibility = VISIBLE
          listView.visibility = VISIBLE
          searchView.setOnClickListener {
            val input = EditText(context)
            AlertDialog.Builder(context)
              .setIcon(android.R.drawable.ic_dialog_alert)
              .setTitle("Type a fully qualified class name")
              .setView(input)
              .setPositiveButton(android.R.string.ok) { _, _ ->
                executeOnIo {
                  val partialClassName = input.text.toString()
                  val matchingClasses = graph.classes
                    .filter { partialClassName in it.name }
                    .toList()

                  if (matchingClasses.isEmpty()) {
                    updateUi {
                      Toast.makeText(
                        context, "No class matching [$partialClassName]", Toast.LENGTH_LONG
                      )
                        .show()
                    }
                  } else {
                    updateUi {
                      titleView.text =
                        "${matchingClasses.size} classes matching [$partialClassName]"
                      listView.adapter = SimpleListAdapter(
                        R.layout.leak_canary_simple_row, matchingClasses
                      ) { view, position ->
                        val itemTitleView = view.findViewById<TextView>(R.id.leak_canary_row_text)
                        itemTitleView.text = matchingClasses[position].name
                      }
                      listView.setOnItemClickListener { _, _, position, _ ->
                        val selectedClass = matchingClasses[position]
                        showClass(titleView, listView, selectedClass)
                      }
                    }
                  }
                }
              }
              .setNegativeButton(android.R.string.cancel, null)
              .show()
          }
        }
      }
    }

  private fun View.showClass(
    titleView: TextView,
    listView: ListView,
    selectedClass: HeapClass
  ) {
    executeOnIo {
      val className = selectedClass.name
      val instances = selectedClass.directInstances.toList()
      val staticFields = selectedClass.readStaticFields()
        .fieldsAsString()
      updateUi {
        titleView.text =
          "Class $className (${instances.size} instances)"
        listView.adapter = SimpleListAdapter(
          R.layout.leak_canary_simple_row, staticFields + instances
        ) { view, position ->
          val itemTitleView =
            view.findViewById<TextView>(R.id.leak_canary_row_text)
          if (position < staticFields.size) {
            itemTitleView.text = staticFields[position].second
          } else {
            itemTitleView.text = "@${instances[position - staticFields.size].objectId}"
          }
        }
        listView.setOnItemClickListener { _, _, position, _ ->
          if (position < staticFields.size) {
            val staticField = staticFields[position].first
            onHeapValueClicked(titleView, listView, staticField.value)
          } else {
            val instance = instances[position - staticFields.size]
            showInstance(titleView, listView, instance)
          }
        }
      }
    }
  }

  private fun View.showInstance(
    titleView: TextView,
    listView: ListView,
    instance: HeapInstance
  ) {
    executeOnIo {
      val fields = instance.readFields()
        .fieldsAsString()
      val className = instance.instanceClassName
      updateUi {
        titleView.text = "Instance @${instance.objectId} of class $className"
        listView.adapter = SimpleListAdapter(
          R.layout.leak_canary_simple_row, fields
        ) { view, position ->
          val itemTitleView =
            view.findViewById<TextView>(R.id.leak_canary_row_text)
          itemTitleView.text = fields[position].second
        }
        listView.setOnItemClickListener { _, _, position, _ ->
          val field = fields[position].first
          onHeapValueClicked(titleView, listView, field.value)
        }
      }
    }
  }

  private fun View.showObjectArray(
    titleView: TextView,
    listView: ListView,
    instance: HeapObjectArray
  ) {
    executeOnIo {
      val elements = instance.readElements()
        .mapIndexed { index: Int, element: HeapValue ->
          element to "[$index] = ${element.heapValueAsString()}"
        }
        .toList()
      val arrayClassName = instance.arrayClassName
      val className = arrayClassName.substring(0, arrayClassName.length - 2)
      updateUi {
        titleView.text = "Array $className[${elements.size}]"
        listView.adapter = SimpleListAdapter(
          R.layout.leak_canary_simple_row, elements
        ) { view, position ->
          val itemTitleView =
            view.findViewById<TextView>(R.id.leak_canary_row_text)
          itemTitleView.text = elements[position].second
        }
        listView.setOnItemClickListener { _, _, position, _ ->
          val element = elements[position].first
          onHeapValueClicked(titleView, listView, element)
        }
      }
    }
  }

  private fun View.showPrimitiveArray(
    titleView: TextView,
    listView: ListView,
    instance: HeapPrimitiveArray
  ) {
    executeOnIo {
      val (type, values) = when (val record = instance.readRecord()) {
        is BooleanArrayDump -> "boolean" to record.array.map { it.toString() }
        is CharArrayDump -> "char" to record.array.map { "'$it'" }
        is FloatArrayDump -> "float" to record.array.map { it.toString() }
        is DoubleArrayDump -> "double" to record.array.map { it.toString() }
        is ByteArrayDump -> "byte" to record.array.map { it.toString() }
        is ShortArrayDump -> "short" to record.array.map { it.toString() }
        is IntArrayDump -> "int" to record.array.map { it.toString() }
        is LongArrayDump -> "long" to record.array.map { it.toString() }
      }
      updateUi {
        titleView.text = "Array $type[${values.size}]"
        listView.adapter = SimpleListAdapter(
          R.layout.leak_canary_simple_row, values
        ) { view, position ->
          val itemTitleView =
            view.findViewById<TextView>(R.id.leak_canary_row_text)
          itemTitleView.text = "$type ${values[position]}"
        }
        listView.setOnItemClickListener { _, _, _, _ ->
        }
      }
    }
  }

  private fun View.onHeapValueClicked(
    titleView: TextView,
    listView: ListView,
    heapValue: HeapValue
  ) {
    if (heapValue.isNonNullReference) {
      when (val objectRecord = heapValue.asObject!!) {
        is HeapInstance -> {
          showInstance(titleView, listView, objectRecord)
        }
        is HeapClass -> {
          showClass(titleView, listView, objectRecord)
        }
        is HeapObjectArray -> {
          showObjectArray(titleView, listView, objectRecord)
        }
        is HeapPrimitiveArray -> {
          showPrimitiveArray(titleView, listView, objectRecord)
        }
      }
    }
  }

  private fun Sequence<HeapField>.fieldsAsString(): List<Pair<HeapField, String>> {
    return map { field ->
      field to "${field.declaringClass.simpleName}.${field.name} = ${field.value.heapValueAsString()}"
    }
      .toList()
  }

  private fun HeapValue.heapValueAsString(): String {
    return when (val heapValue = holder) {
      is ReferenceHolder -> {
        if (isNullReference) {
          "null"
        } else {
          when (val objectRecord = asObject!!) {
            is HeapInstance -> {
              if (objectRecord instanceOf "java.lang.String") {
                "${objectRecord.instanceClassName}@${heapValue.value} \"${objectRecord.readAsJavaString()!!}\""
              } else {
                "${objectRecord.instanceClassName}@${heapValue.value}"
              }
            }
            is HeapClass -> {
              "Class ${objectRecord.name}"
            }
            is HeapObjectArray -> {
              objectRecord.arrayClassName
            }
            is HeapPrimitiveArray -> objectRecord.arrayClassName
          }
        }
      }
      is BooleanHolder -> "boolean ${heapValue.value}"
      is CharHolder -> "char ${heapValue.value}"
      is FloatHolder -> "float ${heapValue.value}"
      is DoubleHolder -> "double ${heapValue.value}"
      is ByteHolder -> "byte ${heapValue.value}"
      is ShortHolder -> "short ${heapValue.value}"
      is IntHolder -> "int ${heapValue.value}"
      is LongHolder -> "long ${heapValue.value}"
    }
  }
}

package leakcanary.internal.activity.screen

import android.text.Html
import android.text.SpannableStringBuilder
import android.util.Patterns
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.AdapterView.OnItemSelectedListener
import android.widget.ListView
import android.widget.Spinner
import android.widget.TextView
import com.squareup.leakcanary.core.R
import leakcanary.internal.DisplayLeakAdapter
import leakcanary.internal.SquigglySpan
import leakcanary.internal.activity.db.HeapAnalysisTable
import leakcanary.internal.activity.db.LeakTable
import leakcanary.internal.activity.db.LeakTable.LeakProjection
import leakcanary.internal.activity.db.LeakTable.LeakTraceProjection
import leakcanary.internal.activity.db.executeOnDb
import leakcanary.internal.activity.share
import leakcanary.internal.activity.shareHeapDump
import leakcanary.internal.activity.shareToStackOverflow
import leakcanary.internal.activity.ui.SimpleListAdapter
import leakcanary.internal.activity.ui.TimeFormatter
import leakcanary.internal.activity.ui.UiUtils.replaceUrlSpanWithAction
import leakcanary.internal.navigation.Screen
import leakcanary.internal.navigation.activity
import leakcanary.internal.navigation.goTo
import leakcanary.internal.navigation.inflate
import shark.HeapAnalysisSuccess
import shark.LeakTrace
import shark.LibraryLeak
import shark.SharkLog

internal class LeakScreen(
  private val leakSignature: String,
  private val selectedHeapAnalysisId: Long? = null
) : Screen() {
  override fun createView(container: ViewGroup) =
    container.inflate(R.layout.leak_canary_leak_screen)
      .apply {
        activity.title = resources.getString(R.string.leak_canary_loading_title)
        executeOnDb {
          val leak = LeakTable.retrieveLeakBySignature(db, leakSignature)

          if (leak == null) {
            updateUi {
              activity.title = resources.getString(R.string.leak_canary_leak_not_found)
            }
          } else {
            val selectedLeakIndex =
              if (selectedHeapAnalysisId == null) 0 else leak.leakTraces.indexOfFirst { it.heapAnalysisId == selectedHeapAnalysisId }

            if (selectedLeakIndex != -1) {
              val heapAnalysisId = leak.leakTraces[selectedLeakIndex].heapAnalysisId
              val selectedHeapAnalysis =
                HeapAnalysisTable.retrieve<HeapAnalysisSuccess>(db, heapAnalysisId)!!

              updateUi {
                onLeaksRetrieved(leak, selectedLeakIndex, selectedHeapAnalysis)
              }
            } else {
              // This can happen if a delete was enqueued and is slow and the user tapped on a leak
              // row before the deletion is perform and the UI update that leaves the screen
              // executes.
              updateUi {
                activity.title = "Selected heap analysis deleted"
              }
            }
            LeakTable.markAsRead(db, leakSignature)
          }
        }
      }

  private fun View.onLeaksRetrieved(
    leak: LeakProjection,
    selectedLeakTraceIndex: Int,
    selectedHeapAnalysis: HeapAnalysisSuccess
  ) {
    val isLibraryLeak = leak.isLibraryLeak
    val isNew = leak.isNew
    val newChipView = findViewById<TextView>(R.id.leak_canary_chip_new)
    val libraryLeakChipView = findViewById<TextView>(R.id.leak_canary_chip_library_leak)
    newChipView.visibility = if (isNew) View.VISIBLE else View.GONE
    libraryLeakChipView.visibility = if (isLibraryLeak) View.VISIBLE else View.GONE

    activity.title = String.format(
      resources.getQuantityText(
        R.plurals.leak_canary_group_screen_title, leak.leakTraces.size
      )
        .toString(), leak.leakTraces.size, leak.shortDescription
    )

    val singleLeakTraceRow = findViewById<View>(R.id.leak_canary_single_leak_trace_row)
    val spinner = findViewById<Spinner>(R.id.leak_canary_spinner)

    if (leak.leakTraces.size == 1) {
      spinner.visibility = View.GONE

      val leakTrace = leak.leakTraces.first()

      bindSimpleRow(singleLeakTraceRow, leakTrace)
      onLeakTraceSelected(selectedHeapAnalysis, leakTrace.heapAnalysisId, leakTrace.leakTraceIndex)
    } else {
      singleLeakTraceRow.visibility = View.GONE

      spinner.adapter =
        SimpleListAdapter(R.layout.leak_canary_simple_row, leak.leakTraces) { view, position ->
          bindSimpleRow(view, leak.leakTraces[position])
        }

      var lastSelectedLeakTraceIndex = selectedLeakTraceIndex
      var lastSelectedHeapAnalysis = selectedHeapAnalysis

      spinner.onItemSelectedListener = object : OnItemSelectedListener {
        override fun onNothingSelected(parent: AdapterView<*>?) {
        }

        override fun onItemSelected(
          parent: AdapterView<*>?,
          view: View?,
          position: Int,
          id: Long
        ) {
          val selectedLeakTrace = leak.leakTraces[position]

          val selectedHeapAnalysisId = selectedLeakTrace.heapAnalysisId
          val lastSelectedHeapAnalysisId =
            leak.leakTraces[lastSelectedLeakTraceIndex].heapAnalysisId

          if (selectedHeapAnalysisId != lastSelectedHeapAnalysisId) {
            executeOnDb {
              val newSelectedHeapAnalysis =
                HeapAnalysisTable.retrieve<HeapAnalysisSuccess>(db, selectedHeapAnalysisId)!!
              updateUi {
                lastSelectedLeakTraceIndex = position
                lastSelectedHeapAnalysis = newSelectedHeapAnalysis
                onLeakTraceSelected(
                  newSelectedHeapAnalysis, selectedHeapAnalysisId,
                  selectedLeakTrace.leakTraceIndex
                )
              }
            }
          } else {
            lastSelectedLeakTraceIndex = position
            onLeakTraceSelected(
              lastSelectedHeapAnalysis, selectedHeapAnalysisId, selectedLeakTrace.leakTraceIndex
            )
          }
        }
      }
      spinner.setSelection(selectedLeakTraceIndex)
    }
  }

  private fun bindSimpleRow(
    view: View,
    leakTrace: LeakTraceProjection
  ) {
    val titleView = view.findViewById<TextView>(R.id.leak_canary_row_text)
    val timeView = view.findViewById<TextView>(R.id.leak_canary_row_small_text)

    titleView.text =
      view.resources.getString(R.string.leak_canary_class_has_leaked, leakTrace.classSimpleName)
    timeView.text = TimeFormatter.formatTimestamp(view.context, leakTrace.createdAtTimeMillis)
  }

  private fun parseLinks(str: String): String {
    val words = str.split(" ")
    var parsedString = ""
    for (word in words) {
      parsedString += if (Patterns.WEB_URL.matcher(word)
          .matches()
      ) {
        "${word}";
      } else {
        word
      }
      if (words.indexOf(word) != words.size - 1) parsedString += " "
    }
    return parsedString
  }

  private fun View.onLeakTraceSelected(
    analysis: HeapAnalysisSuccess,
    heapAnalysisId: Long,
    leakTraceIndex: Int
  ) {
    val selectedLeak = analysis.allLeaks.first { it.signature == leakSignature }
    val leakTrace = selectedLeak.leakTraces[leakTraceIndex]

    val listView = findViewById<ListView>(R.id.leak_canary_list)
    listView.alpha = 0f
    listView.animate()
      .alpha(1f)

    val titleText = """
      Open Heap Dump

      Share leak trace as text or on Stack Overflow

      Print leak trace to Logcat (tag: LeakCanary)

      Share Heap Dump file

      References underlined are the likely causes of the leak.
      Learn more at https://squ.re/leaks
    """.trimIndent() + if (selectedLeak is LibraryLeak) "

" +
      "A Library Leak is a leak caused by a known bug in 3rd party code that you do not have control over. " +
      "(Learn More)

" +
      "Leak pattern: ${selectedLeak.pattern}

" +
      "Description: ${parseLinks(selectedLeak.description)}" else ""

    val title = Html.fromHtml(titleText) as SpannableStringBuilder
    SquigglySpan.replaceUnderlineSpans(title, context)

    replaceUrlSpanWithAction(title) { urlSpan ->
      when (urlSpan) {
        "share" -> {
          {
            share(LeakTraceWrapper.wrap(leakToString(leakTrace, analysis), 80))
          }
        }
        "share_stack_overflow" -> {
          {
            shareToStackOverflow(LeakTraceWrapper.wrap(leakToString(leakTrace, analysis), 80))
          }
        }
        "print" -> {
          {
            SharkLog.d {
              "\u200B\n" + LeakTraceWrapper.wrap(
                leakToString(leakTrace, analysis), 120
              )
            }
          }
        }
        "open_analysis" -> {
          {
            goTo(HeapDumpScreen(heapAnalysisId))
          }
        }
        "share_hprof" -> {
          {
            shareHeapDump(analysis.heapDumpFile)
          }
        }
        else -> null
      }
    }

    val adapter = DisplayLeakAdapter(context, leakTrace, title)
    listView.adapter = adapter
  }

  private fun leakToString(
    leakTrace: LeakTrace,
    analysis: HeapAnalysisSuccess
  ) = """$leakTrace

METADATA

${
    if (analysis.metadata.isNotEmpty()) {
      analysis.metadata
        .map { "${it.key}: ${it.value}" }
        .joinToString("\n")
    } else {
      ""
    }
  }
Analysis duration: ${analysis.analysisDurationMillis} ms"""
}

package leakcanary.internal.activity.screen

/**
 * Performs word wrapping of leak traces.
 */
internal object LeakTraceWrapper {
  private const val SPACE = '\u0020'
  private const val TILDE = '\u007E'
  private const val PERIOD = '\u002E'
  private const val ZERO_SPACE_WIDTH = '\u200B'

  /**
   * This implements a greedy wrapping algorithm.
   *
   * Each line that is longer than [maxWidth], is wrapped by taking the maximum amount of words that fit
   * within the bounds delimited by [maxWidth]. This is done by walking back from the character at [maxWidth]
   * position, until the first separator is found (a [SPACE] or [PERIOD]).
   *
   * Additionally, [Underline] characters are tracked and added when necessary.
   *
   * Finally, all lines start with an offset which includes a decorator character and some level of
   * indentation.
   */
  fun wrap(
    sourceMultilineString: String,
    maxWidth: Int
  ): String {
    // Lines without terminating line separators
    val linesNotWrapped = sourceMultilineString.lines()

    val linesWrapped = mutableListOf<String>()

    for (currentLineIndex in linesNotWrapped.indices) {
      val currentLine = linesNotWrapped[currentLineIndex]

      if (TILDE in currentLine) {
        check(currentLineIndex > 0) {
          "A $TILDE character cannot be placed on the first line of a leak trace"
        }
        continue
      }

      val nextLineWithUnderline = if (currentLineIndex < linesNotWrapped.lastIndex) {
        linesNotWrapped[currentLineIndex + 1].run { if (TILDE in this) this else null }
      } else null

      val currentLineTrimmed = currentLine.trimEnd()
      if (currentLineTrimmed.length <= maxWidth) {
        linesWrapped += currentLineTrimmed
        if (nextLineWithUnderline != null) {
          linesWrapped += nextLineWithUnderline
        }
      } else {
        linesWrapped += wrapLine(currentLineTrimmed, nextLineWithUnderline, maxWidth)
      }
    }
    return linesWrapped.joinToString(separator = "\n") { it.trimEnd() }
  }

  private fun wrapLine(
    currentLine: String,
    nextLineWithUnderline: String?,
    maxWidth: Int
  ): List<String> {

    val twoCharPrefixes = mapOf(
      "├─" to "│ ",
      "│ " to "│ ",
      "╰→" to "$ZERO_SPACE_WIDTH ",
      "$ZERO_SPACE_WIDTH " to "$ZERO_SPACE_WIDTH "
    )

    val twoCharPrefix = currentLine.substring(0, 2)
    val prefixPastFirstLine: String
    val prefixFirstLine: String
    if (twoCharPrefix in twoCharPrefixes) {
      val indexOfFirstNonWhitespace =
        2 + currentLine.substring(2).indexOfFirst { !it.isWhitespace() }
      prefixFirstLine = currentLine.substring(0, indexOfFirstNonWhitespace)
      prefixPastFirstLine =
        twoCharPrefixes[twoCharPrefix] + currentLine.substring(2, indexOfFirstNonWhitespace)
    } else {
      prefixFirstLine = ""
      prefixPastFirstLine = ""
    }

    var lineRemainingChars = currentLine.substring(prefixFirstLine.length)

    val maxWidthWithoutOffset = maxWidth - prefixFirstLine.length

    val lineWrapped = mutableListOf<String>()
    var periodsFound = 0

    var updatedUnderlineStart: Int
    val underlineStart: Int

    if (nextLineWithUnderline != null) {
      underlineStart = nextLineWithUnderline.indexOf(TILDE)
      updatedUnderlineStart = underlineStart - prefixFirstLine.length
    } else {
      underlineStart = -1
      updatedUnderlineStart = -1
    }

    var underlinedLineIndex = -1
    while (lineRemainingChars.isNotEmpty() && lineRemainingChars.length > maxWidthWithoutOffset) {
      val stringBeforeLimit = lineRemainingChars.substring(0, maxWidthWithoutOffset)

      val lastIndexOfSpace = stringBeforeLimit.lastIndexOf(SPACE)
      val lastIndexOfPeriod = stringBeforeLimit.lastIndexOf(PERIOD)

      val lastIndexOfCurrentLine = lastIndexOfSpace.coerceAtLeast(lastIndexOfPeriod).let {
        if (it == -1) {
          stringBeforeLimit.lastIndex
        } else {
          it
        }
      }

      if (lastIndexOfCurrentLine == lastIndexOfPeriod) {
        periodsFound++
      }

      val wrapIndex = lastIndexOfCurrentLine + 1

      // remove spaces at the end if any
      lineWrapped += stringBeforeLimit.substring(0, wrapIndex).trimEnd()

      // This line has an underline and we haven't find its new position after wrapping yet.
      if (nextLineWithUnderline != null && underlinedLineIndex == -1) {
        if (lastIndexOfCurrentLine < updatedUnderlineStart) {
          updatedUnderlineStart -= wrapIndex
        } else {
          underlinedLineIndex = lineWrapped.lastIndex
        }
      }

      lineRemainingChars = lineRemainingChars.substring(wrapIndex, lineRemainingChars.length)
    }

    // there are still residual words to be added, if we exit the loop with a non-empty line
    if (lineRemainingChars.isNotEmpty()) {
      lineWrapped += lineRemainingChars
    }

    if (nextLineWithUnderline != null) {
      if (underlinedLineIndex == -1) {
        underlinedLineIndex = lineWrapped.lastIndex
      }
      val underlineEnd = nextLineWithUnderline.lastIndexOf(TILDE)
      val underlineLength = underlineEnd - underlineStart + 1

      val spacesBeforeTilde = "$SPACE".repeat(updatedUnderlineStart)
      val underlineTildes = "$TILDE".repeat(underlineLength)
      lineWrapped.add(underlinedLineIndex + 1, "$spacesBeforeTilde$underlineTildes")
    }

    return lineWrapped.mapIndexed { index: Int, line: String ->
      (if (index == 0) {
        prefixFirstLine
      } else {
        prefixPastFirstLine
      } + line).trimEnd()
    }
  }
}

package leakcanary.internal.activity.screen

import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.ListView
import android.widget.TextView
import com.squareup.leakcanary.core.R
import leakcanary.internal.activity.db.HeapAnalysisTable
import leakcanary.internal.activity.db.LeakTable
import leakcanary.internal.activity.db.LeakTable.AllLeaksProjection
import leakcanary.internal.activity.db.executeOnDb
import leakcanary.internal.activity.ui.SimpleListAdapter
import leakcanary.internal.activity.ui.TimeFormatter
import leakcanary.internal.navigation.NavigatingActivity
import leakcanary.internal.navigation.Screen
import leakcanary.internal.navigation.activity
import leakcanary.internal.navigation.goTo
import leakcanary.internal.navigation.inflate
import leakcanary.internal.navigation.onScreenExiting

internal class LeaksScreen : Screen() {
  override fun createView(container: ViewGroup) =
    container.inflate(R.layout.leak_canary_list).apply {

      val unsubscribeRefresh = HeapAnalysisTable.onUpdate {
        activity<NavigatingActivity>().refreshCurrentScreen()
      }

      onScreenExiting { unsubscribeRefresh() }

      executeOnDb {
        val projections = LeakTable.retrieveAllLeaks(db)
        updateUi { onGroupsRetrieved(projections) }
      }
    }

  private fun View.onGroupsRetrieved(projections: List<AllLeaksProjection>) {
    activity.title = resources.getQuantityString(
      R.plurals.leak_canary_distinct_leaks,
      projections.size, projections.size
    )

    val listView = findViewById<ListView>(R.id.leak_canary_list)

    listView.adapter =
      SimpleListAdapter(R.layout.leak_canary_leak_row, projections) { view, position ->
        val countView = view.findViewById<TextView>(R.id.leak_canary_count_text)
        val descriptionView = view.findViewById<TextView>(R.id.leak_canary_leak_text)
        val timeView = view.findViewById<TextView>(R.id.leak_canary_time_text)
        val newChipView = view.findViewById<TextView>(R.id.leak_canary_chip_new)
        val libraryLeakChipView = view.findViewById<TextView>(R.id.leak_canary_chip_library_leak)

        val projection = projections[position]
        countView.isEnabled = projection.isNew

        newChipView.visibility = if (projection.isNew) VISIBLE else GONE
        libraryLeakChipView.visibility = if (projection.isLibraryLeak) VISIBLE else GONE

        countView.text = projection.leakTraceCount.toString()
        descriptionView.text = projection.shortDescription

        val formattedDate =
          TimeFormatter.formatTimestamp(view.context, projection.createdAtTimeMillis)
        timeView.text =
          resources.getString(R.string.leak_canary_group_list_time_label, formattedDate)
      }

    listView.setOnItemClickListener { _, _, position, _ ->
      goTo(LeakScreen(projections[position].signature))
    }
  }
}

package leakcanary.internal.activity.screen

import android.content.Intent
import android.graphics.Bitmap
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Environment
import android.os.Environment.DIRECTORY_DOWNLOADS
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.ImageView
import android.widget.Toast
import com.squareup.leakcanary.core.R
import leakcanary.internal.InternalLeakCanary
import leakcanary.internal.LeakCanaryFileProvider
import leakcanary.internal.activity.db.executeOnIo
import leakcanary.internal.navigation.Screen
import leakcanary.internal.navigation.activity
import leakcanary.internal.navigation.inflate
import leakcanary.internal.navigation.onCreateOptionsMenu
import leakcanary.internal.utils.humanReadableByteCount
import shark.SharkLog
import java.io.File
import java.io.FileOutputStream
import java.io.IOException

internal class RenderHeapDumpScreen(
  private val heapDumpFile: File
) : Screen() {

  override fun createView(container: ViewGroup) =
    container.inflate(R.layout.leak_canary_heap_render).apply {
      container.activity.title = resources.getString(R.string.leak_canary_loading_title)

      executeOnIo {
        val byteCount = humanReadableByteCount(heapDumpFile.length(), si = true)
        updateUi {
          container.activity.title =
            resources.getString(R.string.leak_canary_heap_dump_screen_title, byteCount)
        }
      }

      val loadingView = findViewById<View>(R.id.leak_canary_loading)
      val imageView = findViewById<ImageView>(R.id.leak_canary_heap_rendering)

      viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
        override fun onGlobalLayout() {

          executeOnIo {
            val bitmap = HeapDumpRenderer.render(
              context, heapDumpFile, measuredWidth, measuredHeight, 0
            )
            updateUi {
              imageView.setImageBitmap(bitmap)
              loadingView.visibility = View.GONE
              imageView.visibility = View.VISIBLE
            }
          }
          if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
            viewTreeObserver.removeOnGlobalLayoutListener(this)
          } else {
            viewTreeObserver.removeGlobalOnLayoutListener(this)
          }
        }
      })

      onCreateOptionsMenu { menu ->
        menu.add(R.string.leak_canary_options_menu_generate_hq_bitmap)
          .setOnMenuItemClickListener {
            val leakDirectoryProvider = InternalLeakCanary.createLeakDirectoryProvider(context)
            if (!leakDirectoryProvider.hasStoragePermission()) {
              Toast.makeText(
                context,
                R.string.leak_canary_options_menu_permission_toast,
                Toast.LENGTH_LONG
              )
                .show()
              leakDirectoryProvider.requestWritePermissionNotification()
            } else {
              Toast.makeText(
                context,
                R.string.leak_canary_generating_hq_bitmap_toast_notice,
                Toast.LENGTH_LONG
              )
                .show()
              executeOnIo {
                val bitmap = HeapDumpRenderer.render(context, heapDumpFile, 2048, 0, 4)
                @Suppress("DEPRECATION") val storageDir =
                  Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS)

                val imageFile = File(storageDir, "${heapDumpFile.name}.png")
                val saved = savePng(imageFile, bitmap)
                if (saved) {
                  SharkLog.d { "Png saved at $imageFile" }
                  imageFile.setReadable(true, false)
                  val imageUri = LeakCanaryFileProvider.getUriForFile(
                    activity,
                    "com.squareup.leakcanary.fileprovider." + activity.packageName,
                    imageFile
                  )

                  updateUi {
                    val intent = Intent(Intent.ACTION_SEND)
                    intent.type = "image/png"
                    intent.putExtra(Intent.EXTRA_STREAM, imageUri)
                    activity.startActivity(
                      Intent.createChooser(
                        intent,
                        resources.getString(
                          R.string.leak_canary_share_heap_dump_bitmap_screen_title
                        )
                      )
                    )
                  }
                } else {
                  updateUi {
                    Toast.makeText(
                      context,
                      R.string.leak_canary_generating_hq_bitmap_toast_failure_notice,
                      Toast.LENGTH_LONG
                    )
                      .show()
                  }
                }
              }
            }
            true
          }
      }
    }

  fun savePng(
    imageFile: File,
    source: Bitmap
  ): Boolean {
    var outStream: FileOutputStream? = null
    return try {
      outStream = FileOutputStream(imageFile)
      source.compress(Bitmap.CompressFormat.PNG, 100, outStream)
      true
    } catch (e: IOException) {
      false
    } finally {
      outStream?.close()
    }
  }
}

package leakcanary.internal.activity.ui

import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import leakcanary.internal.navigation.inflate

internal class SimpleListAdapter<T>(
  private val rowResId: Int,
  private val items: List<T>,
  private val bindView: SimpleListAdapter<T>.(View, Int) -> Unit
) : BaseAdapter() {
  override fun getView(
    position: Int,
    convertView: View?,
    parent: ViewGroup
  ): View {
    val view = convertView ?: parent.inflate(rowResId)
    bindView(view, position)
    return view
  }

  override fun getItem(position: Int) = items[position]

  override fun getItemId(position: Int) = position.toLong()

  override fun getCount() = items.size
}

package leakcanary.internal.activity.ui

import android.content.Context
import android.text.format.DateUtils

internal object TimeFormatter {

  private const val MINUTE_MILLIS = 60 * 1000
  private const val TWO_MINUTES_MILLIS = 2 * MINUTE_MILLIS
  private const val FIFTY_MINUTES_MILLIS = 50 * MINUTE_MILLIS
  private const val NINETY_MINUTES_MILLIS = 90 * MINUTE_MILLIS
  private const val HOUR_MILLIS = 60 * MINUTE_MILLIS
  private const val DAY_MILLIS = 24 * HOUR_MILLIS
  private const val TWO_DAYS_MILLIS = 48 * HOUR_MILLIS

  fun formatTimestamp(
    context: Context,
    timestampMillis: Long
  ): String {
    // Based on https://stackoverflow.com/a/13018647
    val nowMillis = System.currentTimeMillis()
    return when (val diff = nowMillis - timestampMillis) {
      in 0..MINUTE_MILLIS -> {
        "just now"
      }
      in MINUTE_MILLIS..TWO_MINUTES_MILLIS -> {
        "a minute ago"
      }
      in TWO_MINUTES_MILLIS..FIFTY_MINUTES_MILLIS -> {
        "${diff / MINUTE_MILLIS} minutes ago"
      }
      in FIFTY_MINUTES_MILLIS..NINETY_MINUTES_MILLIS -> {
        "an hour ago"
      }
      in NINETY_MINUTES_MILLIS..DAY_MILLIS -> {
        "${diff / HOUR_MILLIS} hours ago"
      }
      in DAY_MILLIS..TWO_DAYS_MILLIS -> {
        "yesterday"
      }
      else -> {
        DateUtils.formatDateTime(
          context, timestampMillis,
          DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE
        )
      }
    }
  }
}

package leakcanary.internal.activity.ui

import android.text.SpannableStringBuilder
import android.text.style.ClickableSpan
import android.text.style.URLSpan
import android.view.View

internal object UiUtils {

  internal fun replaceUrlSpanWithAction(
    title: SpannableStringBuilder,
    urlAction: (String) -> (() -> Unit)?
  ) {
    val urlSpans = title.getSpans(0, title.length, URLSpan::class.java)
    for (span in urlSpans) {
      val action: (() -> Unit)? = urlAction(span.url)
      if (action != null) {
        val start = title.getSpanStart(span)
        val end = title.getSpanEnd(span)
        val flags = title.getSpanFlags(span)
        title.removeSpan(span)
        val newSpan = object : ClickableSpan() {
          override fun onClick(widget: View) {
            action()
          }
        }
        title.setSpan(newSpan, start, end, flags)
      }
    }
  }
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.squareup.leakcanary.deobfuscation

import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import java.io.File

@CacheableTask
open class CopyObfuscationMappingFileTask : DefaultTask() {

  @Input
  var variantName: String = ""

  @InputFile
  @PathSensitive(PathSensitivity.RELATIVE)
  var mappingFile: File? = null

  @InputDirectory
  @PathSensitive(PathSensitivity.RELATIVE)
  var mergeAssetsDirectory: File? = null

  @get:OutputFile
  val leakCanaryAssetsOutputFile: File
    get() = File(mergeAssetsDirectory, "leakCanaryObfuscationMapping.txt")

  init {
    description = "Puts obfuscation mapping file in assets directory."
  }

  @TaskAction
  fun copyObfuscationMappingFile() {
    val mapping = validateMappingFile()
    validateMergeAssetsDir()
    mapping.copyTo(leakCanaryAssetsOutputFile, overwrite = true)
  }

  private fun validateMappingFile(): File {
    val mapping = mappingFile
    if (mapping == null || !mapping.exists()) {
      throw GradleException(
        """
        The plugin was configured to be applied to a variant which doesn't define
        an obfuscation mapping file: make sure that isMinified is true for variant: $variantName.
        """
      )
    }
    return mapping
  }

  private fun validateMergeAssetsDir() {
    mergeAssetsDirectory?.let { mergeAssetsDir ->
      if (!mergeAssetsDir.exists()) {
        val mergeAssetsDirCreated = mergeAssetsDir.mkdirs()
        if (!mergeAssetsDirCreated) {
          throw GradleException(
            "Obfuscation mapping destination dir doesn't exist and it's impossible to create it."
          )
        }
      }
    } ?: throw GradleException("Obfuscation mapping is null.")
  }
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.squareup.leakcanary.deobfuscation

import com.android.build.gradle.api.BaseVariant

/**
 * Extension for the gradle plugin. It allows the user to configure the plugin.
 */
open class LeakCanaryDeobfuscationExtension {

  /**
   * Variant filtering function. It should be overriden to tell LeakCanary for which obfuscated
   * variants it should copy the mapping file into apk. For example this:
   * ```
   * filterObfuscatedVariants { variant ->
   *   variant.name == "debug"
   * }
   * ```
   * means that debug variant should be deobfuscated by LeakCanary when displaying leaks.
   *
   * Default value is *false* so no variant will have the obfuscation mapping file copied.
   */
  var filterObfuscatedVariants: (BaseVariant) -> Boolean = { false }
}

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.squareup.leakcanary.deobfuscation

import com.android.build.gradle.AppExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.api.ApplicationVariant
import com.android.build.gradle.api.BaseVariant
import com.android.build.gradle.api.LibraryVariant
import org.gradle.api.Action
import org.gradle.api.DefaultTask
import org.gradle.api.DomainObjectSet
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.UnknownTaskException
import org.gradle.api.plugins.AppliedPlugin
import org.gradle.api.tasks.TaskProvider

class LeakCanaryLeakDeobfuscationPlugin : Plugin<Project> {

  override fun apply(project: Project) {
    val leakCanaryPluginAction = Action<AppliedPlugin> {
      val leakCanaryExtension = createLeakCanaryExtension(project)
      val variants = findAndroidVariants(project)
      variants.all { variant ->
        if (leakCanaryExtension.filterObfuscatedVariants(variant)) {
          setupTasks(project, variant)
        }
      }
    }

    project.pluginManager.withPlugin("com.android.application", leakCanaryPluginAction)
    project.pluginManager.withPlugin("com.android.library", leakCanaryPluginAction)
  }

  private fun findAndroidVariants(project: Project): DomainObjectSet<BaseVariant> {
    return try {
      when (val extension = project.extensions.getByType(BaseExtension::class.java)) {
        is AppExtension -> extension.applicationVariants as DomainObjectSet<BaseVariant>
        is LibraryExtension -> extension.libraryVariants as DomainObjectSet<BaseVariant>
        else -> throwNoAndroidPluginException()
      }
    } catch (e: Exception) {
      throwNoAndroidPluginException()
    }
  }

  private fun createLeakCanaryExtension(project: Project): LeakCanaryDeobfuscationExtension {
    return project.extensions.create("leakCanary", LeakCanaryDeobfuscationExtension::class.java)
  }

  private fun setupTasks(
    project: Project,
    variant: BaseVariant
  ) {
    val copyObfuscationMappingFileTaskProvider = project.tasks.register(
      "leakCanaryCopyObfuscationMappingFor${variant.name.capitalize()}",
      CopyObfuscationMappingFileTask::class.java
    ) {
      it.variantName = variant.name
      it.mappingFile = variant.mappingFile
      it.mergeAssetsDirectory = variant.mergeAssetsProvider.get().outputDir.get().asFile

      val mappingGeneratingTaskProvider =
        findTaskProviderOrNull(
          project,
          "transformClassesAndResourcesWithR8For${variant.name.capitalize()}"
        ) ?: findTaskProviderOrNull(
          project,
          "transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}"
        ) ?: findTaskProviderOrNull(
          project,
          "minify${variant.name.capitalize()}WithR8"
        ) ?: findTaskProviderOrNull(
          project,
          "minify${variant.name.capitalize()}WithProguard"
        ) ?: throwMissingMinifiedVariantException()

      it.dependsOn(mappingGeneratingTaskProvider)
      it.dependsOn(variant.mergeAssetsProvider)
    }

    getPackageTaskProvider(variant).configure {
      it.dependsOn(copyObfuscationMappingFileTaskProvider)
    }
  }

  private fun findTaskProviderOrNull(
    project: Project,
    taskName: String
  ): TaskProvider<Task>? {
    return try {
      project.tasks.named(taskName)
    } catch (proguardTaskNotFoundException: UnknownTaskException) {
      null
    }
  }

  private fun getPackageTaskProvider(variant: BaseVariant): TaskProvider<out DefaultTask> {
    return when (variant) {
      is LibraryVariant -> variant.packageLibraryProvider
      is ApplicationVariant -> variant.packageApplicationProvider
      else -> throwNoAndroidPluginException()
    }
  }

  private fun throwNoAndroidPluginException(): Nothing {
    throw GradleException(
      "LeakCanary deobfuscation plugin can be used only in Android application or library module."
    )
  }

  private fun throwMissingMinifiedVariantException(): Nothing {
    throw GradleException(
      """
        LeakCanary deobfuscation plugin couldn't find any variant with minification enabled.
        Please make sure that there is at least 1 minified variant in your project.
      """
    )
  }
}