Add runWithAvd method to AvdManager

This is the first step towards removing
android-device-provider-gradle UTP plugin by
consolidating AVD launcher implementation in
AGP.

Bug: 431284686
Test: AvdSnapshotHandlerTest
Change-Id: I641763297f2fc0c42bb8f3637a6c4b2002e6fd30
diff --git a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdComponents.kt b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdComponents.kt
index 81297a2..17bb9a8 100644
--- a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdComponents.kt
+++ b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdComponents.kt
@@ -174,10 +174,30 @@
         avdManager.get().loadSnapshotIfNeeded(deviceName, emulatorGpuMode)
     }
 
+    /**
+     * Manages and runs an Android Virtual Device (AVD) for a given operation.
+     *
+     * This function starts the specified AVD, waits for it to come online, and then executes the
+     * [onDeviceReady] callback with the device's serial number. The function is blocking and will
+     * not return until the callback has completed.
+     *
+     * The [onDeviceReady] callback is invoked on the same thread that called this method.
+     *
+     * @param deviceName The name of the AVD to provision. This AVD must have been created beforehand.
+     * @param emulatorGpuMode The GPU mode to use when starting the emulator.
+     * @param onDeviceReady A block of code to execute once the device is ready. It is provided with the
+     * online device's serial number.
+     * @throws RuntimeException if the device cannot be provisioned or fails to start.
+     */
+    fun runWithAvd(deviceName: String, emulatorGpuMode: String,
+        onDeviceReady: (onlineDeviceSerial: String) -> Unit) {
+        avdManager.get().runWithAvd(deviceName, emulatorGpuMode, onDeviceReady)
+    }
+
     /** Closes all active emulators having an id with the given prefix. This should be used to close
      * emulators that may remain after a crashed UTP test run.
      *
-     * @param idPrefix the prefix that is looke for to close the active emulators. All emulators
+     * @param idPrefix the prefix that is looked for to close the active emulators. All emulators
      * that have an id not starting with this prefix are ignored.
      */
     fun closeOpenEmulators(idPrefix: String) {
diff --git a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdManager.kt b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdManager.kt
index 88d681b..eb22833 100644
--- a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdManager.kt
+++ b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdManager.kt
@@ -231,6 +231,28 @@
     }
 
     /**
+     * Starts an Android Virtual device and [onDeviceReady] callback is invoked once the device
+     * becomes online and system services on the device are started.
+     *
+     * This method blocks execution and [onDeviceReady] callback is invoked from the caller's thread.
+     */
+    fun runWithAvd(deviceName: String, emulatorGpuFlag: String,
+        onDeviceReady: (onlineDeviceSerial: String) -> Unit) {
+        runWithMultiProcessLocking(deviceName) {
+            deviceLockManager.lock(1).use {
+                snapshotHandler.startEmulatorThenStop(
+                    false,
+                    deviceName,
+                    avdFolder,
+                    emulatorGpuFlag,
+                    logger,
+                    onDeviceReady
+                )
+            }
+        }
+    }
+
+    /**
      * Returns the names of all avds currently in the shared avd folder.
      */
     fun allAvds(): List<String> {
diff --git a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdSnapshotHandler.kt b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdSnapshotHandler.kt
index 404db81..da7cfe2 100644
--- a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdSnapshotHandler.kt
+++ b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdSnapshotHandler.kt
@@ -16,24 +16,23 @@
 
 package com.android.build.gradle.internal
 
-import com.android.build.gradle.internal.testing.getEmulatorMetadata
 import com.android.build.gradle.internal.testing.AdbHelper
 import com.android.build.gradle.internal.testing.EmulatorVersionMetadata
 import com.android.build.gradle.internal.testing.QemuExecutor
+import com.android.build.gradle.internal.testing.getEmulatorMetadata
 import com.android.sdklib.internal.avd.AvdManager
 import com.android.testing.utils.createSetupDeviceId
-import com.android.utils.FileUtils
 import com.android.utils.GrabProcessOutput
 import com.android.utils.ILogger
-import java.io.File
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicBoolean
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.Executor
-import java.util.concurrent.Executors
 import org.gradle.api.file.Directory
 import org.gradle.api.provider.Provider
+import java.io.File
 import java.io.IOException
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
 
 private const val EMULATOR_EXECUTABLE = "emulator"
 private const val DEFAULT_DEVICE_BOOT_AND_SNAPSHOT_CHECK_TIMEOUT_SEC = 600L
@@ -55,7 +54,7 @@
     private val emulatorDir: Provider<Directory>,
     private val qemuExecutor: QemuExecutor,
     private val extraWaitAfterBootCompleteMs: Long = WAIT_AFTER_BOOT_MS,
-    private val executor: Executor = Executors.newSingleThreadExecutor(),
+    private val executor: ExecutorService = Executors.newSingleThreadExecutor(),
     private val metadataFactory: (File) -> EmulatorVersionMetadata = ::getEmulatorMetadata,
     private val processFactory: (List<String>) -> ProcessBuilder = { ProcessBuilder(it) }) {
 
@@ -65,6 +64,9 @@
         metadataFactory(emulatorDirectory)
     }
 
+    private val timeoutSeconds: Long
+        get() = deviceBootAndSnapshotCheckTimeoutSec ?: DEFAULT_DEVICE_BOOT_AND_SNAPSHOT_CHECK_TIMEOUT_SEC
+
     /**
      * Checks whether the emulator directory contains a valid emulator executable, and returns it.
      *
@@ -173,21 +175,15 @@
             logger.warning("Timed out trying to check $snapshotName for $avdName is loadable.")
         }
         if (!timeout) {
-            val timeoutSec =
-                deviceBootAndSnapshotCheckTimeoutSec ?:
-                DEFAULT_DEVICE_BOOT_AND_SNAPSHOT_CHECK_TIMEOUT_SEC
-            outputProcessed.await(timeoutSec, TimeUnit.SECONDS)
+            outputProcessed.await(timeoutSeconds, TimeUnit.SECONDS)
         }
         return success.get()
     }
 
     private fun Process.waitUntilTimeout(logger: ILogger, onTimeout: () -> Unit) {
-        val timeoutSec =
-            deviceBootAndSnapshotCheckTimeoutSec ?:
-            DEFAULT_DEVICE_BOOT_AND_SNAPSHOT_CHECK_TIMEOUT_SEC
-        if (timeoutSec > 0) {
-            logger.verbose("Waiting for a process to complete (timeout $timeoutSec seconds)")
-            if (!waitFor(timeoutSec, TimeUnit.SECONDS)) {
+        if (timeoutSeconds > 0) {
+            logger.verbose("Waiting for a process to complete (timeout $timeoutSeconds seconds)")
+            if (!waitFor(timeoutSeconds, TimeUnit.SECONDS)) {
                 onTimeout()
             }
         } else {
@@ -293,12 +289,19 @@
         }
     }
 
-    private fun startEmulatorThenStop(
+    /**
+     * Starts an Android Virtual device and [onDeviceReady] callback is invoked once the device
+     * becomes online and system services on the device are started.
+     *
+     * This method blocks execution and [onDeviceReady] callback is invoked from the caller's thread.
+     */
+    fun startEmulatorThenStop(
         createSnapshot: Boolean,
         avdName: String,
         avdLocation: File,
         emulatorGpuFlag: String,
-        logger: ILogger
+        logger: ILogger,
+        onDeviceReady: (onlineDeviceSerial: String) -> Unit = {},
     ) {
         val deviceId = createSetupDeviceId(avdName)
 
@@ -316,17 +319,14 @@
             )
         )
         processBuilder.environment()["ANDROID_AVD_HOME"] = avdLocation.absolutePath
-        processBuilder.environment()["ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL"] = (
-                deviceBootAndSnapshotCheckTimeoutSec ?:
-                DEFAULT_DEVICE_BOOT_AND_SNAPSHOT_CHECK_TIMEOUT_SEC
-                ).toString()
+        processBuilder.environment()["ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL"] = timeoutSeconds.toString()
         val emulatorProcess = processBuilder.start()
         val bootCompleted = AtomicBoolean(false)
         // need to process both stderr and stdout
         val outputProcessed = CountDownLatch(2)
         val emulatorErrorList = mutableListOf<String>()
         try {
-            executor.execute {
+            val deviceSerialFuture = executor.submit<String> {
                 var emulatorSerial: String? = null
                 while(emulatorProcess.isAlive) {
                     try {
@@ -341,7 +341,7 @@
                 if (emulatorSerial == null) {
                     // It is possible for the emulator process to return unexpectedly
                     // and the emulatorSerial to not be set.
-                    return@execute
+                    return@submit null
                 }
                 logger.verbose("$avdName is attached to adb ($emulatorSerial).")
 
@@ -372,11 +372,12 @@
                 if (emulatorProcess.isAlive) {
                     logger.verbose("$avdName is ready to take a snapshot.")
                     bootCompleted.set(true)
-                    adbHelper.killDevice(emulatorSerial)
+                    return@submit emulatorSerial
                 } else {
                     logger.warning(
                         "Emulator process exited unexpectedly with the return code " +
                         "${emulatorProcess.exitValue()}.")
+                    return@submit null
                 }
             }
 
@@ -403,6 +404,12 @@
                 }
             )
 
+            val deviceSerial = deviceSerialFuture.get(timeoutSeconds, TimeUnit.SECONDS)
+            if (deviceSerial != null) {
+                onDeviceReady(deviceSerial)
+                adbHelper.killDevice(deviceSerial)
+            }
+
             emulatorProcess.waitUntilTimeout(logger) {
                 logger.warning("Snapshot creation timed out. Closing emulator.")
                 throw EmulatorSnapshotCannotCreatedException("""
@@ -416,15 +423,9 @@
             emulatorProcess.destroy()
         }
 
-
-
         if (!bootCompleted.get()) {
             // wait for processing to complete for output of process
-
-            val timeoutSec =
-                deviceBootAndSnapshotCheckTimeoutSec ?:
-                DEFAULT_DEVICE_BOOT_AND_SNAPSHOT_CHECK_TIMEOUT_SEC
-            outputProcessed.await(timeoutSec, TimeUnit.SECONDS)
+            outputProcessed.await(timeoutSeconds, TimeUnit.SECONDS)
 
             throw EmulatorSnapshotCannotCreatedException(
                 """
diff --git a/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/AvdSnapshotHandlerTest.kt b/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/AvdSnapshotHandlerTest.kt
index 40de670..9c106dd 100644
--- a/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/AvdSnapshotHandlerTest.kt
+++ b/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/AvdSnapshotHandlerTest.kt
@@ -22,10 +22,6 @@
 import com.android.build.gradle.internal.testing.QemuExecutor
 import com.android.sdklib.internal.avd.AvdInfo
 import com.android.sdklib.internal.avd.AvdManager
-import org.mockito.kotlin.any
-import org.mockito.kotlin.eq
-import org.mockito.kotlin.mock
-import com.android.utils.FileUtils
 import com.android.utils.ILogger
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.MoreExecutors
@@ -38,15 +34,14 @@
 import org.junit.rules.TemporaryFolder
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.kotlin.any
 import org.mockito.Mockito.contains
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
-import org.mockito.junit.MockitoJUnit
 import java.io.File
 import java.io.InputStream
-import java.lang.RuntimeException
 
 @RunWith(JUnit4::class)
 class AvdSnapshotHandlerTest {
@@ -109,7 +104,7 @@
                 emulatorDirectoryProvider,
                 qemuExecutor,
                 extraWaitAfterBootCompleteMs = 0L,
-                MoreExecutors.directExecutor(),
+                MoreExecutors.newDirectExecutorService(),
                 { _ -> EmulatorVersionMetadata(true) }
         ) { commands ->
             if (commands.contains("-check-snapshot-loadable")) {
@@ -138,7 +133,7 @@
                 emulatorDirectoryProvider,
                 qemuExecutor,
                 extraWaitAfterBootCompleteMs = 0L,
-                MoreExecutors.directExecutor(),
+                MoreExecutors.newDirectExecutorService(),
                 { _ -> EmulatorVersionMetadata(true) }
         ) { _ -> createMockProcessBuilder() }
 
@@ -182,7 +177,7 @@
             emulatorDirectoryProvider,
             qemuExecutor,
             extraWaitAfterBootCompleteMs = 0L,
-            MoreExecutors.directExecutor(),
+            MoreExecutors.newDirectExecutorService(),
             { _ -> EmulatorVersionMetadata(true) }
         ) { commands ->
             assertThat(commands).contains("-verbose")
@@ -222,7 +217,7 @@
             emulatorDirectoryProvider,
             qemuExecutor,
             extraWaitAfterBootCompleteMs = 0L,
-            MoreExecutors.directExecutor(),
+            MoreExecutors.newDirectExecutorService(),
             { _ -> EmulatorVersionMetadata(true) }
         ) { commands ->
             // disabling full kernel logging should disable verbose logging in setup actions.
@@ -256,4 +251,31 @@
             emulatorError
         )
     }
+
+    @Test
+    fun startEmulatorThenStop() {
+        val env = mutableMapOf<String, String>()
+        val handler = AvdSnapshotHandler(
+            showFullEmulatorKernelLogging = true,
+            deviceBootAndSnapshotCheckTimeoutSec = 1234,
+            mockAdbHelper,
+            emulatorDirectoryProvider,
+            qemuExecutor,
+            extraWaitAfterBootCompleteMs = 0L,
+            MoreExecutors.newDirectExecutorService(),
+            { _ -> EmulatorVersionMetadata(true) }
+        ) { _ -> createMockProcessBuilder() }
+
+        var onDeviceReadyIsCalled = false
+        handler.startEmulatorThenStop(
+            createSnapshot = false,
+            "myTestAvdName",
+            avdDirectory,
+            emulatorGpuFlag = "",
+            mockLogger) {
+            onDeviceReadyIsCalled = true
+        }
+
+        assertThat(onDeviceReadyIsCalled).isTrue()
+    }
 }