feat(driver): optional launcher_activity on Launch RPC for multi-alias apps

This commit is contained in:
pj committed 2026-04-18 01:59:36 +07:00
1 parent 6d81e24e71
commit c4cf0ff530
11 files changed
+69 -64

No files matched your search

@@ -1,7 +1,7 @@
package dev.uatu.sidecar
interface DriverBackend {
fun launch(bundleId: String, clearState: Boolean)
fun launch(bundleId: String, launcherActivity: String, clearState: Boolean)
fun terminate(bundleId: String)
fun tap(x: Int, y: Int)
fun tapSelector(selector: String)
@@ -30,47 +30,22 @@ class StubDriverBackend(private val platform: String) : DriverBackend {
@Volatile var lastInputText: String? = null
private set
override fun launch(bundleId: String, clearState: Boolean) {
override fun launch(bundleId: String, launcherActivity: String, clearState: Boolean) {
launchCount++
lastBundleId = bundleId
if (clearState) {
runAdb(listOf("shell", "pm", "clear", bundleId))
}
val launcherComponent = resolveLauncherActivity(bundleId)
if (launcherComponent != null) {
runAdb(listOf("shell", "am", "start", "-n", launcherComponent))
if (launcherActivity.isNotEmpty()) {
val component = if (launcherActivity.contains('/')) launcherActivity else "$bundleId/$launcherActivity"
runAdb(listOf("shell", "am", "start", "-n", component))
} else {
// Fallback to monkey (less reliable on emulators without physical keys).
// `monkey` uses PackageManager.getLaunchIntentForPackage, which
// picks the canonical default launcher.
runAdb(listOf("shell", "monkey", "-p", bundleId, "-c", "android.intent.category.LAUNCHER", "1"))
}
}
private fun resolveLauncherActivity(bundleId: String): String? {
return try {
val process = ProcessBuilder(listOf("adb", "shell", "pm", "dump", bundleId))
.redirectErrorStream(true).start()
process.waitFor()
val output = process.inputStream.bufferedReader().readText()
// pm dump emits stanzas like:
// <hash> <pkg>/<activity> filter <hash>
// Action: "android.intent.action.MAIN"
// Category: "android.intent.category.LAUNCHER"
// Pick the first <pkg>/<activity> followed by both MAIN + LAUNCHER.
val componentPattern = Regex("\\s+\\S+\\s+($bundleId/[\\w.\$]+)\\s+filter\\b")
val lines = output.lines()
for (index in lines.indices) {
val match = componentPattern.find(lines[index]) ?: continue
val window = lines.subList(index, minOf(index + 12, lines.size)).joinToString("\n")
if (window.contains("android.intent.action.MAIN") && window.contains("android.intent.category.LAUNCHER")) {
return match.groupValues[1]
}
}
null
} catch (_: Exception) {
null
}
}
override fun terminate(bundleId: String) {
runAdb(listOf("shell", "am", "force-stop", bundleId))
lastBundleId = null
@@ -95,9 +70,10 @@ class StubDriverBackend(private val platform: String) : DriverBackend {
private fun runAdb(arguments: List<String>) {
try {
val command = ProcessBuilder(listOf("adb") + arguments).redirectErrorStream(true).start()
// Drain output before waiting so a large write doesn't block the child.
command.inputStream.bufferedReader().readText()
command.waitFor()
} catch (cause: Exception) {
// Best-effort: log and continue.
println("adb ${arguments.joinToString(" ")} failed: $cause")
}
}
@@ -24,7 +24,7 @@ class DriverService(
override fun launch(request: LaunchRequest, responseObserver: StreamObserver<Empty>) {
runRpc(responseObserver) {
backend.launch(request.bundleId, request.clearState)
backend.launch(request.bundleId, request.launcherActivity, request.clearState)
launchedBundleId.set(request.bundleId)
Empty.getDefaultInstance()
}