Convert leakcanary-android-instrumentation Java files to Kotlin

See #1205
diff --git a/leakcanary-android-instrumentation/build.gradle b/leakcanary-android-instrumentation/build.gradle
index 51396dd..9b5b017 100644
--- a/leakcanary-android-instrumentation/build.gradle
+++ b/leakcanary-android-instrumentation/build.gradle
@@ -1,8 +1,11 @@
 apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
 
 dependencies {
+  api deps.kotlin.stdLib.common
   api project(':leakcanary-android')
   implementation 'androidx.test:runner:1.1.1'
+  implementation deps.kotlin.reflect
 }
 
 android {
diff --git a/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationLeakDetectorTest.java b/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationLeakDetectorTest.java
index 0a2a582..6814c53 100644
--- a/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationLeakDetectorTest.java
+++ b/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationLeakDetectorTest.java
@@ -29,13 +29,13 @@
     InstrumentationLeakDetector leakDetector = new InstrumentationLeakDetector();
     InstrumentationLeakResults results = leakDetector.detectLeaks();
 
-    if (results.detectedLeaks.size() != 1) {
-      throw new AssertionError("Expected exactly one leak, not " + results.detectedLeaks.size());
+    if (results.getDetectedLeaks().size() != 1) {
+      throw new AssertionError("Expected exactly one leak, not " + results.getDetectedLeaks().size());
     }
 
-    InstrumentationLeakResults.Result firstResult = results.detectedLeaks.get(0);
+    InstrumentationLeakResults.Result firstResult = results.getDetectedLeaks().get(0);
 
-    String leakingClassName = firstResult.analysisResult.getClassName();
+    String leakingClassName = firstResult.getAnalysisResult().getClassName();
 
     if (!leakingClassName.equals(Date.class.getName())) {
       throw new AssertionError("Expected a leak of Date, not " + leakingClassName);
diff --git a/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationTestApplication.java b/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationTestApplication.java
index c534699..8258e79 100644
--- a/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationTestApplication.java
+++ b/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationTestApplication.java
@@ -5,7 +5,7 @@
 public class InstrumentationTestApplication extends Application {
   @Override public void onCreate() {
     super.onCreate();
-    InstrumentationLeakDetector.instrumentationRefWatcher(this)
+    InstrumentationLeakDetector.Companion.instrumentationRefWatcher(this)
         .buildAndInstall();
   }
 }
diff --git a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/FailTestOnLeakRunListener.java b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/FailTestOnLeakRunListener.java
deleted file mode 100644
index 721c4be..0000000
--- a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/FailTestOnLeakRunListener.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * 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 com.squareup.leakcanary;
-
-import android.app.Instrumentation;
-import android.content.Context;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.test.internal.runner.listener.InstrumentationResultPrinter;
-import java.util.List;
-import org.junit.runner.Description;
-import org.junit.runner.Result;
-import org.junit.runner.notification.Failure;
-import org.junit.runner.notification.RunListener;
-
-import static androidx.test.internal.runner.listener.InstrumentationResultPrinter.REPORT_VALUE_RESULT_FAILURE;
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-import static com.squareup.leakcanary.Preconditions.checkNotNull;
-
-/**
- * <p>A JUnit {@link RunListener} for detecting memory leaks in Android instrumentation tests. It
- * waits for the end of a test, and if the test succeeds then it will look for leaking
- * references, trigger a heap dump if needed and perform an analysis.
- * <p> {@link FailTestOnLeakRunListener} can be subclassed to override
- * {@link #skipLeakDetectionReason(Description)}, {@link #reportLeaks(InstrumentationLeakResults)}
- * or {@link #buildLeakDetectedMessage(List)}
- *
- * @see InstrumentationLeakDetector
- */
-public class FailTestOnLeakRunListener extends RunListener {
-
-  private static final String SEPARATOR = "######################################\n";
-  private Bundle bundle;
-
-  private String skipLeakDetectionReason;
-
-  @Override public final void testStarted(Description description) {
-    skipLeakDetectionReason = skipLeakDetectionReason(description);
-    if (skipLeakDetectionReason != null) {
-      return;
-    }
-    String testClass = description.getClassName();
-    String testName = description.getMethodName();
-
-    bundle = new Bundle();
-    bundle.putString(Instrumentation.REPORT_KEY_IDENTIFIER,
-        FailTestOnLeakRunListener.class.getName());
-    bundle.putString(InstrumentationResultPrinter.REPORT_KEY_NAME_CLASS, testClass);
-    bundle.putString(InstrumentationResultPrinter.REPORT_KEY_NAME_TEST, testName);
-  }
-
-  /**
-   * Can be overridden to skip leak detection based on the description provided when a test
-   * is started. Returns null to continue leak detection, or a string describing the reason for
-   * skipping otherwise.
-   */
-  protected @Nullable String skipLeakDetectionReason(@NonNull Description description) {
-    return null;
-  }
-
-  @Override public final void testFailure(Failure failure) {
-    skipLeakDetectionReason = "failed";
-  }
-
-  @Override public final void testIgnored(Description description) {
-    skipLeakDetectionReason = "was ignored";
-  }
-
-  @Override public final void testAssumptionFailure(Failure failure) {
-    skipLeakDetectionReason = "had an assumption failure";
-  }
-
-  @Override public final void testFinished(Description description) {
-    detectLeaks();
-    LeakCanary.installedRefWatcher().clearWatchedReferences();
-  }
-
-  @Override public final void testRunStarted(Description description) {
-  }
-
-  @Override public final void testRunFinished(Result result) {
-  }
-
-  private void detectLeaks() {
-    if (skipLeakDetectionReason != null) {
-      CanaryLog.d("Skipping leak detection because the test %s", skipLeakDetectionReason);
-      skipLeakDetectionReason = null;
-      return;
-    }
-
-    InstrumentationLeakDetector leakDetector = new InstrumentationLeakDetector();
-    InstrumentationLeakResults results = leakDetector.detectLeaks();
-
-    reportLeaks(results);
-  }
-
-  /** Can be overridden to report leaks in a different way or do additional reporting. */
-  protected void reportLeaks(@NonNull InstrumentationLeakResults results) {
-    if (!results.detectedLeaks.isEmpty()) {
-      String message =
-          checkNotNull(buildLeakDetectedMessage(results.detectedLeaks), "buildLeakDetectedMessage");
-
-      bundle.putString(InstrumentationResultPrinter.REPORT_KEY_STACK, message);
-      getInstrumentation().sendStatus(REPORT_VALUE_RESULT_FAILURE, bundle);
-    }
-  }
-
-  /** Can be overridden to customize the failure string message. */
-  protected @NonNull String buildLeakDetectedMessage(
-      @NonNull List<InstrumentationLeakResults.Result> detectedLeaks) {
-    StringBuilder failureMessage = new StringBuilder();
-    failureMessage.append(
-        "Test failed because memory leaks were detected, see leak traces below.\n");
-    failureMessage.append(SEPARATOR);
-
-    Context context = getInstrumentation().getContext();
-    for (InstrumentationLeakResults.Result detectedLeak : detectedLeaks) {
-      failureMessage.append(
-          LeakCanary.leakInfo(context, detectedLeak.heapDump, detectedLeak.analysisResult, true));
-      failureMessage.append(SEPARATOR);
-    }
-
-    return failureMessage.toString();
-  }
-}
diff --git a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/FailTestOnLeakRunListener.kt b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/FailTestOnLeakRunListener.kt
new file mode 100644
index 0000000..6541959
--- /dev/null
+++ b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/FailTestOnLeakRunListener.kt
@@ -0,0 +1,144 @@
+/*
+ * 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 com.squareup.leakcanary
+
+import android.app.Instrumentation
+import android.os.Bundle
+import androidx.test.internal.runner.listener.InstrumentationResultPrinter
+import androidx.test.internal.runner.listener.InstrumentationResultPrinter.REPORT_VALUE_RESULT_FAILURE
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.squareup.leakcanary.Preconditions.checkNotNull
+import org.junit.runner.Description
+import org.junit.runner.Result
+import org.junit.runner.notification.Failure
+import org.junit.runner.notification.RunListener
+
+/**
+ *
+ * A JUnit [RunListener] for detecting memory leaks in Android instrumentation tests. It
+ * waits for the end of a test, and if the test succeeds then it will look for leaking
+ * references, trigger a heap dump if needed and perform an analysis.
+ *
+ *  [FailTestOnLeakRunListener] can be subclassed to override
+ * [skipLeakDetectionReason], [reportLeaks]
+ * or [buildLeakDetectedMessage]
+ *
+ * @see InstrumentationLeakDetector
+ */
+open class FailTestOnLeakRunListener : RunListener() {
+  private lateinit var bundle: Bundle
+  private var skipLeakDetectionReason: String? = null
+
+  override fun testStarted(description: Description) {
+    skipLeakDetectionReason = skipLeakDetectionReason(description)
+    if (skipLeakDetectionReason != null) {
+      return
+    }
+    val testClass = description.className
+    val testName = description.methodName
+
+    bundle = Bundle()
+    bundle.putString(
+        Instrumentation.REPORT_KEY_IDENTIFIER, FailTestOnLeakRunListener::class.java.name
+    )
+    bundle.putString(InstrumentationResultPrinter.REPORT_KEY_NAME_CLASS, testClass)
+    bundle.putString(InstrumentationResultPrinter.REPORT_KEY_NAME_TEST, testName)
+  }
+
+  /**
+   * Can be overridden to skip leak detection based on the description provided when a test
+   * is started. Returns 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) {
+    detectLeaks()
+    LeakCanary.installedRefWatcher()
+        .clearWatchedReferences()
+  }
+
+  override fun testRunStarted(description: Description) {}
+
+  override fun testRunFinished(result: Result) {}
+
+  private fun detectLeaks() {
+    if (skipLeakDetectionReason != null) {
+      CanaryLog.d("Skipping leak detection because the test %s", skipLeakDetectionReason)
+      skipLeakDetectionReason = null
+      return
+    }
+
+    val leakDetector = InstrumentationLeakDetector()
+    val results = leakDetector.detectLeaks()
+
+    reportLeaks(results)
+  }
+
+  /** Can be overridden to report leaks in a different way or do additional reporting.  */
+  protected open fun reportLeaks(results: InstrumentationLeakResults) {
+    if (results.detectedLeaks.isNotEmpty()) {
+      val message =
+        checkNotNull(
+            buildLeakDetectedMessage(results.detectedLeaks), "buildLeakDetectedMessage"
+        )
+
+      bundle.putString(InstrumentationResultPrinter.REPORT_KEY_STACK, message)
+      getInstrumentation().sendStatus(REPORT_VALUE_RESULT_FAILURE, bundle)
+    }
+  }
+
+  /** Can be overridden to customize the failure string message.  */
+  protected open fun buildLeakDetectedMessage(
+    detectedLeaks: List<InstrumentationLeakResults.Result>
+  ): String {
+    val failureMessage = StringBuilder()
+    failureMessage.append(
+        "Test failed because memory leaks were detected, see leak traces below.\n"
+    )
+    failureMessage.append(SEPARATOR)
+
+    val context = getInstrumentation().context
+    detectedLeaks.forEach { detectedLeak ->
+      failureMessage.append(
+          LeakCanary.leakInfo(
+              context, detectedLeak.heapDump, detectedLeak.analysisResult, true
+          )
+      )
+      failureMessage.append(SEPARATOR)
+    }
+
+    return failureMessage.toString()
+  }
+
+  companion object {
+    private const val SEPARATOR = "######################################\n"
+  }
+}
diff --git a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakDetector.java b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakDetector.java
deleted file mode 100644
index f1b6375..0000000
--- a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakDetector.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * 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 com.squareup.leakcanary;
-
-import android.app.Application;
-import android.app.Instrumentation;
-import android.content.Context;
-import android.os.Debug;
-import android.os.SystemClock;
-import androidx.annotation.NonNull;
-import com.squareup.leakcanary.internal.LeakCanaryInternals;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.CopyOnWriteArrayList;
-import org.junit.runner.notification.RunListener;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-/**
- * <p>{@link InstrumentationLeakDetector} can be used to detect memory leaks in instrumentation
- * tests.
- *
- * <p>To use it, you need to:
- * <ul>
- * <li>Install a custom RefWatcher that will not trigger heapdumps while the tests run.</li>
- * <li>Add an instrumentation test listener (a {@link RunListener}) that will invoke
- * {@link #detectLeaks()}</li>
- * </ul>
- *
- * <h3>Installing the instrumentation RefWatcher</h3>
- *
- * <p>For {@link #detectLeaks()} to work correctly, the {@link RefWatcher} must keep track of
- * references but not trigger any heap dump until this {@link #detectLeaks()} runs, otherwise an
- * analysis in progress might prevent this listener from performing its own analysis.
- *
- * <p>Create and install the {@link RefWatcher} instance using
- * {@link #instrumentationRefWatcher(Application)} instead of
- * {@link LeakCanary#install(Application)} or {@link LeakCanary#refWatcher(Context)}.
- * <pre><code>
- * public class InstrumentationExampleApplication extends ExampleApplication {
- *   {@literal @}Override protected void setupLeakCanary() {
- *     InstrumentationLeakDetector.instrumentationRefWatcher(this)
- *         .buildAndInstall();
- *   }
- * }
- * </code></pre>
- *
- * <h3>Add an intrumentation test listener</h3>
- *
- * <p>LeakCanary provides {@link FailTestOnLeakRunListener}, but you should feel free to implement
- * your own {@link RunListener} and call {@link #detectLeaks()} directly if you need a more custom
- * behavior (for instance running it only once per test suite, or reporting to a backend).</p>
- *
- * <p>All you need to do is add the following to the defaultConfig of your build.gradle:
- *
- * <pre><code>testInstrumentationRunnerArgument "listener", "com.squareup.leakcanary.FailTestOnLeakRunListener"</code></pre>
- *
- * <p>Then you can run your instrumentation tests via Gradle as usually, and they will fail when
- * a memory leak is detected:
- *
- * <pre><code>./gradlew leakcanary-sample:connectedCheck</code></pre>
- *
- * <p>If instead you want to run UI tests via adb, add a <em>listener</em> execution argument to
- * your command line for running the UI tests:
- * <code>-e listener com.squareup.leakcanary.FailTestOnLeakRunListener</code>. The full command line
- * should look something like this:
- * <pre><code>adb shell am instrument \\
- * -w com.android.foo/android.support.test.runner.AndroidJUnitRunner \\
- * -e listener com.squareup.leakcanary.FailTestOnLeakRunListener
- * </code></pre>
- *
- * <h3>Rationale</h3>
- * Instead of using the {@link FailTestOnLeakRunListener}, one could simply enable LeakCanary in
- * instrumentation tests.
- *
- * <p>This approach would have two disadvantages:
- * <ul>
- * <li>Heap dumps freeze the VM, and the leak analysis is IO and CPU heavy. This can slow down
- * the test and introduce flakiness</li>
- * <li>The leak analysis is asynchronous by default, and happens in a separate process. This means
- * the tests could finish and the process die before the analysis is finished.</li>
- * </ul>
- *
- * <p>The approach taken here is to collect all references 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 leaking, and then we iterate on the leaking references in the
- * heap dump and provide all result in a {@link InstrumentationLeakResults}.
- */
-public final class InstrumentationLeakDetector {
-
-  /**
-   * Returns a new {@link} AndroidRefWatcherBuilder that will create a {@link RefWatcher} suitable
-   * for instrumentation tests. This {@link RefWatcher} will never trigger a heap dump. This should
-   * be installed from the test application class, and should be used in combination with a
-   * {@link RunListener} that calls {@link #detectLeaks()}, for instance
-   * {@link FailTestOnLeakRunListener}.
-   */
-  public static @NonNull AndroidRefWatcherBuilder instrumentationRefWatcher(
-      @NonNull Application application) {
-    return LeakCanary.refWatcher(application)
-        .watchExecutor(new WatchExecutor() {
-          // Storing weak refs to ensure they make it to the queue.
-          final List<Retryable> trackedReferences = new CopyOnWriteArrayList<>();
-
-          @Override public void execute(Retryable retryable) {
-            trackedReferences.add(retryable);
-          }
-        });
-  }
-
-  public @NonNull InstrumentationLeakResults detectLeaks() {
-    Instrumentation instrumentation = getInstrumentation();
-    Context context = instrumentation.getTargetContext();
-    RefWatcher refWatcher = LeakCanary.installedRefWatcher();
-    Set<String> retainedKeys = refWatcher.getRetainedKeys();
-
-    if (refWatcher.isEmpty()) {
-      return InstrumentationLeakResults.NONE;
-    }
-
-    instrumentation.waitForIdleSync();
-    if (refWatcher.isEmpty()) {
-      return InstrumentationLeakResults.NONE;
-    }
-
-    GcTrigger.DEFAULT.runGc();
-    if (refWatcher.isEmpty()) {
-      return InstrumentationLeakResults.NONE;
-    }
-
-    // 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.isEmpty()) {
-      return InstrumentationLeakResults.NONE;
-    }
-
-    // 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);
-    GcTrigger.DEFAULT.runGc();
-
-    if (refWatcher.isEmpty()) {
-      return InstrumentationLeakResults.NONE;
-    }
-
-    // We're always reusing the same file since we only execute this once at a time.
-    File heapDumpFile = new File(context.getFilesDir(), "instrumentation_tests_heapdump.hprof");
-    try {
-      Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
-    } catch (Exception e) {
-      CanaryLog.d(e, "Could not dump heap");
-      return InstrumentationLeakResults.NONE;
-    }
-
-    HeapDump.Builder heapDumpBuilder = LeakCanaryInternals.installedHeapDumpBuilder;
-    HeapAnalyzer heapAnalyzer =
-        new HeapAnalyzer(heapDumpBuilder.excludedRefs, AnalyzerProgressListener.Companion.getNONE(),
-            heapDumpBuilder.reachabilityInspectorClasses);
-
-    List<TrackedReference> trackedReferences = heapAnalyzer.findTrackedReferences(heapDumpFile);
-
-    List<InstrumentationLeakResults.Result> detectedLeaks = new ArrayList<>();
-    List<InstrumentationLeakResults.Result> excludedLeaks = new ArrayList<>();
-    List<InstrumentationLeakResults.Result> failures = new ArrayList<>();
-
-    for (TrackedReference trackedReference : trackedReferences) {
-      // Ignore any Weak Reference that this test does not care about.
-      if (!retainedKeys.contains(trackedReference.getKey())) {
-        continue;
-      }
-
-      HeapDump heapDump = HeapDump.builder()
-          .heapDumpFile(heapDumpFile)
-          .excludedRefs(heapDumpBuilder.excludedRefs)
-          .reachabilityInspectorClasses(heapDumpBuilder.reachabilityInspectorClasses)
-          .build();
-
-      AnalysisResult analysisResult =
-          heapAnalyzer.checkForLeak(heapDumpFile, trackedReference.getKey(), false);
-
-      InstrumentationLeakResults.Result leakResult =
-          new InstrumentationLeakResults.Result(heapDump, analysisResult);
-
-      if (analysisResult.getLeakFound()) {
-        if (!analysisResult.getExcludedLeak()) {
-          detectedLeaks.add(leakResult);
-        } else {
-          excludedLeaks.add(leakResult);
-        }
-      } else if (analysisResult.getFailure() != null) {
-        failures.add(leakResult);
-      }
-    }
-
-    CanaryLog.d("Found %d proper leaks, %d excluded leaks and %d leak analysis failures",
-        detectedLeaks.size(),
-        excludedLeaks.size(),
-        failures.size());
-
-    return new InstrumentationLeakResults(detectedLeaks, excludedLeaks, failures);
-  }
-}
diff --git a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakDetector.kt b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakDetector.kt
new file mode 100644
index 0000000..0d544a8
--- /dev/null
+++ b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakDetector.kt
@@ -0,0 +1,216 @@
+/*
+ * 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 com.squareup.leakcanary
+
+import android.app.Application
+import android.os.Debug
+import android.os.SystemClock
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.squareup.leakcanary.InstrumentationLeakDetector.Companion.instrumentationRefWatcher
+import com.squareup.leakcanary.internal.LeakCanaryInternals
+import org.junit.runner.notification.RunListener
+import java.io.File
+import java.util.concurrent.CopyOnWriteArrayList
+
+/**
+ * [InstrumentationLeakDetector] can be used to detect memory leaks in instrumentation tests.
+ *
+ * To use it, you need to:
+ *
+ *  * Install a custom RefWatcher that will not trigger heapdumps while the tests run.
+ *  * Add an instrumentation test listener (a [RunListener]) that will invoke
+ * [detectLeaks]
+ *
+ * ### Installing the instrumentation RefWatcher
+ *
+ * For [detectLeaks] to work correctly, the [RefWatcher] must keep track of
+ * references but not trigger any heap dump until this [detectLeaks] runs, otherwise an
+ * analysis in progress might prevent this listener from performing its own analysis.
+ *
+ * Create and install the [RefWatcher] instance using
+ * [instrumentationRefWatcher] instead of
+ * [LeakCanary.install] or [LeakCanary.refWatcher].
+ *
+ * ```
+ * public class InstrumentationExampleApplication extends ExampleApplication {
+ *  @Override protected void setupLeakCanary() {
+ *    InstrumentationLeakDetector.instrumentationRefWatcher(this)
+ *      .buildAndInstall();
+ *  }
+ * }
+ * ```
+ *
+ * ### Add an instrumentation test listener
+ *
+ * LeakCanary provides [FailTestOnLeakRunListener], but you should feel free to implement
+ * your own [RunListener] and call [.detectLeaks] directly if you need a more custom
+ * behavior (for instance running it only once per test suite, or reporting to a backend).
+ *
+ * All you need to do is add the following to the defaultConfig of your build.gradle:
+ *
+ * `testInstrumentationRunnerArgument "listener", "com.squareup.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 com.squareup.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 com.squareup.leakcanary.FailTestOnLeakRunListener
+ * ```
+ *
+ * ### Rationale
+ * Instead of using the [FailTestOnLeakRunListener], 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, and happens in a separate process. This means
+ * the tests could finish and the process die before the analysis is finished.
+ *
+ * The approach taken here is to collect all references 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 leaking, and then we iterate on the leaking references in the
+ * heap dump and provide all result in a [InstrumentationLeakResults].
+ */
+class InstrumentationLeakDetector {
+
+  fun detectLeaks(): InstrumentationLeakResults {
+    val instrumentation = getInstrumentation()
+    val context = instrumentation.targetContext
+    val refWatcher = LeakCanary.installedRefWatcher()
+    val retainedKeys = refWatcher.retainedKeys
+
+    if (refWatcher.isEmpty) {
+      return InstrumentationLeakResults.NONE
+    }
+
+    instrumentation.waitForIdleSync()
+    if (refWatcher.isEmpty) {
+      return InstrumentationLeakResults.NONE
+    }
+
+    GcTrigger.DEFAULT.runGc()
+    if (refWatcher.isEmpty) {
+      return InstrumentationLeakResults.NONE
+    }
+
+    // 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.isEmpty) {
+      return InstrumentationLeakResults.NONE
+    }
+
+    // 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)
+    GcTrigger.DEFAULT.runGc()
+
+    if (refWatcher.isEmpty) {
+      return InstrumentationLeakResults.NONE
+    }
+
+    // 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")
+    try {
+      Debug.dumpHprofData(heapDumpFile.absolutePath)
+    } catch (e: Exception) {
+      CanaryLog.d(e, "Could not dump heap")
+      return InstrumentationLeakResults.NONE
+    }
+
+    val heapDumpBuilder = LeakCanaryInternals.installedHeapDumpBuilder
+    val heapAnalyzer = HeapAnalyzer(
+        heapDumpBuilder.excludedRefs, AnalyzerProgressListener.NONE,
+        heapDumpBuilder.reachabilityInspectorClasses
+    )
+
+    val trackedReferences = heapAnalyzer.findTrackedReferences(heapDumpFile)
+
+    val detectedLeaks = mutableListOf<InstrumentationLeakResults.Result>()
+    val excludedLeaks = mutableListOf<InstrumentationLeakResults.Result>()
+    val failures = mutableListOf<InstrumentationLeakResults.Result>()
+
+
+    trackedReferences
+        // Ignore any Weak Reference that this test does not care about.
+        .filter { retainedKeys.contains(it.key) }
+        .forEach { trackedReference ->
+          val heapDump = HeapDump.builder()
+              .heapDumpFile(heapDumpFile)
+              .excludedRefs(heapDumpBuilder.excludedRefs)
+              .reachabilityInspectorClasses(heapDumpBuilder.reachabilityInspectorClasses)
+              .build()
+
+          val analysisResult =
+            heapAnalyzer.checkForLeak(heapDumpFile, trackedReference.key, false)
+
+          val leakResult = InstrumentationLeakResults.Result(heapDump, analysisResult)
+
+          if (analysisResult.leakFound) {
+            if (!analysisResult.excludedLeak) {
+              detectedLeaks.add(leakResult)
+            } else {
+              excludedLeaks.add(leakResult)
+            }
+          } else if (analysisResult.failure != null) {
+            failures.add(leakResult)
+          }
+        }
+
+    CanaryLog.d(
+        "Found %d proper leaks, %d excluded leaks and %d leak analysis failures",
+        detectedLeaks.size,
+        excludedLeaks.size,
+        failures.size
+    )
+
+    return InstrumentationLeakResults(detectedLeaks, excludedLeaks, failures)
+  }
+
+  companion object {
+    /**
+     * Returns a new [AndroidRefWatcherBuilder] that will create a [RefWatcher] suitable
+     * for instrumentation tests. This [RefWatcher] will never trigger a heap dump. This should
+     * be installed from the test application class, and should be used in combination with a
+     * [RunListener] that calls [detectLeaks], for instance
+     * [FailTestOnLeakRunListener].
+     */
+    fun instrumentationRefWatcher(application: Application): AndroidRefWatcherBuilder {
+      return LeakCanary.refWatcher(application)
+          .watchExecutor(object : WatchExecutor {
+            // Storing weak refs to ensure they make it to the queue.
+            val trackedReferences: MutableList<Retryable> = CopyOnWriteArrayList()
+
+            override fun execute(retryable: Retryable) {
+              trackedReferences.add(retryable)
+            }
+          })
+    }
+  }
+}
diff --git a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakResults.java b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakResults.java
deleted file mode 100644
index e783663..0000000
--- a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakResults.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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 com.squareup.leakcanary;
-
-import androidx.annotation.NonNull;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import static java.util.Collections.unmodifiableList;
-
-public final class InstrumentationLeakResults {
-
-  @NonNull public static final InstrumentationLeakResults NONE =
-      new InstrumentationLeakResults(Collections.<Result>emptyList(),
-          Collections.<Result>emptyList(), Collections.<Result>emptyList());
-
-  /** Proper leaks found during instrumentation tests. */
-  @NonNull public final List<Result> detectedLeaks;
-
-  /**
-   * Excluded leaks found during instrumentation tests, based on {@link
-   * RefWatcherBuilder#excludedRefs}
-   */
-  @NonNull public final List<Result> excludedLeaks;
-
-  /**
-   * Leak analysis failures that happened when we tried to detect leaks.
-   */
-  @NonNull public final List<Result> failures;
-
-  public InstrumentationLeakResults(@NonNull List<Result> detectedLeaks,
-      @NonNull List<Result> excludedLeaks, @NonNull List<Result> failures) {
-    this.detectedLeaks = unmodifiableList(new ArrayList<>(detectedLeaks));
-    this.excludedLeaks = unmodifiableList(new ArrayList<>(excludedLeaks));
-    this.failures = unmodifiableList(new ArrayList<>(failures));
-  }
-
-  public static final class Result {
-    @NonNull public final HeapDump heapDump;
-    @NonNull public final AnalysisResult analysisResult;
-
-    public Result(@NonNull HeapDump heapDump, @NonNull AnalysisResult analysisResult) {
-      this.heapDump = heapDump;
-      this.analysisResult = analysisResult;
-    }
-  }
-}
diff --git a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakResults.kt b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakResults.kt
new file mode 100644
index 0000000..50fbb14
--- /dev/null
+++ b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakResults.kt
@@ -0,0 +1,40 @@
+/*
+ * 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 com.squareup.leakcanary
+
+/**
+ * @param detectedLeaks Proper leaks found during instrumentation tests.
+ *
+ * @param excludedLeaks Excluded leaks found during instrumentation tests,
+ *   based on [RefWatcherBuilder.excludedRefs]
+ *
+ * @param failures Leak analysis failures that happened when we tried to detect leaks.
+ */
+class InstrumentationLeakResults(
+  val detectedLeaks: List<Result>,
+  val excludedLeaks: List<Result>,
+  val failures: List<Result>
+) {
+
+  class Result(
+    val heapDump: HeapDump,
+    val analysisResult: AnalysisResult
+  )
+
+  companion object {
+    val NONE = InstrumentationLeakResults(emptyList(), emptyList(), emptyList())
+  }
+}
diff --git a/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/FragmentLeakTest.java b/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/FragmentLeakTest.java
index dd5c56c..e73a2eb 100644
--- a/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/FragmentLeakTest.java
+++ b/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/FragmentLeakTest.java
@@ -6,8 +6,8 @@
 import android.os.Bundle;
 import android.os.Looper;
 import android.os.MessageQueue;
-import androidx.test.rule.ActivityTestRule;
 import android.view.View;
+import androidx.test.rule.ActivityTestRule;
 import com.squareup.leakcanary.InstrumentationLeakDetector;
 import com.squareup.leakcanary.InstrumentationLeakResults;
 import com.squareup.leakcanary.LeakCanary;
@@ -94,20 +94,20 @@
     InstrumentationLeakDetector leakDetector = new InstrumentationLeakDetector();
     InstrumentationLeakResults results = leakDetector.detectLeaks();
 
-    if (results.detectedLeaks.size() != 1) {
+    if (results.getDetectedLeaks().size() != 1) {
       throw new AssertionError(
-          "Expected exactly one leak, not " + results.detectedLeaks.size() + resultsAsString(
-              results.detectedLeaks));
+          "Expected exactly one leak, not " + results.getDetectedLeaks().size() + resultsAsString(
+              results.getDetectedLeaks()));
     }
 
-    InstrumentationLeakResults.Result firstResult = results.detectedLeaks.get(0);
+    InstrumentationLeakResults.Result firstResult = results.getDetectedLeaks().get(0);
 
-    String leakingClassName = firstResult.analysisResult.getClassName();
+    String leakingClassName = firstResult.getAnalysisResult().getClassName();
 
     if (!leakingClassName.equals(expectedLeakClass.getName())) {
       throw new AssertionError(
           "Expected a leak of " + expectedLeakClass + ", not " + leakingClassName + resultsAsString(
-              results.detectedLeaks));
+              results.getDetectedLeaks()));
     }
   }
 
@@ -117,7 +117,7 @@
     message.append("\nLeaks found:\n##################\n");
     for (InstrumentationLeakResults.Result detectedLeak : results) {
       message.append(
-          LeakCanary.leakInfo(context, detectedLeak.heapDump, detectedLeak.analysisResult,
+          LeakCanary.leakInfo(context, detectedLeak.getHeapDump(), detectedLeak.getAnalysisResult(),
               false));
     }
     message.append("\n##################\n");
diff --git a/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/InstrumentationTestApplication.java b/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/InstrumentationTestApplication.java
index bd0321f..0674573 100644
--- a/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/InstrumentationTestApplication.java
+++ b/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/InstrumentationTestApplication.java
@@ -6,7 +6,7 @@
 public class InstrumentationTestApplication extends Application {
   @Override public void onCreate() {
     super.onCreate();
-    InstrumentationLeakDetector.instrumentationRefWatcher(this)
+    InstrumentationLeakDetector.Companion.instrumentationRefWatcher(this)
         .buildAndInstall();
   }
 }