Skip to content

Commit

Permalink
Feature: launch arguments for Android (mobile-dev-inc#972)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmitry-zaitsev authored Apr 26, 2023
1 parent a2fb751 commit 3e55506
Show file tree
Hide file tree
Showing 28 changed files with 324 additions and 51 deletions.
3 changes: 3 additions & 0 deletions maestro-android/src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@

<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"
tools:ignore="MockLocation,ProtectedPermissions"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission"/>


</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dev.mobile.maestro
import android.app.UiAutomation
import android.content.Context
import android.content.Context.LOCATION_SERVICE
import android.content.Intent
import android.graphics.Bitmap
import android.location.Criteria
import android.location.Location
Expand Down Expand Up @@ -53,6 +54,7 @@ import maestro_android.checkWindowUpdatingResponse
import maestro_android.deviceInfo
import maestro_android.eraseAllTextResponse
import maestro_android.inputTextResponse
import maestro_android.launchAppResponse
import maestro_android.screenshotResponse
import maestro_android.setLocationResponse
import maestro_android.tapResponse
Expand Down Expand Up @@ -102,6 +104,36 @@ class Service(
private var locationCounter = 0
private val toastAccessibilityListener = ToastAccessibilityListener.start(uiAutomation)

override fun launchApp(
request: MaestroAndroid.LaunchAppRequest,
responseObserver: StreamObserver<MaestroAndroid.LaunchAppResponse>
) {
val context = InstrumentationRegistry.getInstrumentation().targetContext

val intent = context.packageManager.getLaunchIntentForPackage(request.packageName)

if (intent == null) {
Log.e("Maestro", "No launcher intent found for package ${request.packageName}")
responseObserver.onError(RuntimeException("No launcher intent found for package ${request.packageName}"))
return
}

request.argumentsList
.forEach {
when (it.type) {
String::class.java.name -> intent.putExtra(it.key, it.value)
Boolean::class.java.name -> intent.putExtra(it.key, it.value.toBoolean())
Int::class.java.name -> intent.putExtra(it.key, it.value.toInt())
Double::class.java.name -> intent.putExtra(it.key, it.value.toDouble())
else -> intent.putExtra(it.key, it.value)
}
}
context.startActivity(intent)

responseObserver.onNext(launchAppResponse { })
responseObserver.onCompleted()
}

override fun deviceInfo(
request: MaestroAndroid.DeviceInfoRequest,
responseObserver: StreamObserver<MaestroAndroid.DeviceInfo>
Expand Down Expand Up @@ -347,4 +379,8 @@ class Service(
private fun keyPressShiftedToEvents(uiDevice: UiDevice, keyCode: Int) {
uiDevice.pressKeyCode(keyCode, META_SHIFT_LEFT_ON)
}

companion object {
private const val LENGTH_KEY_VALUE_PAIR = 2
}
}
2 changes: 2 additions & 0 deletions maestro-android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"
tools:ignore="MockLocation,ProtectedPermissions"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission"/>

<application android:usesCleartextTraffic="true" />

Expand Down
2 changes: 1 addition & 1 deletion maestro-client/src/main/java/maestro/Driver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ interface Driver {

fun launchApp(
appId: String,
launchArguments: List<String>,
launchArguments: Map<String, Any>,
sessionId: UUID? = null,
)

Expand Down
2 changes: 1 addition & 1 deletion maestro-client/src/main/java/maestro/Maestro.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class Maestro(private val driver: Driver) : AutoCloseable {

fun launchApp(
appId: String,
launchArguments: List<String> = emptyList(),
launchArguments: Map<String, Any> = emptyMap(),
stopIfRunning: Boolean = true
) {
LOGGER.info("Launching app $appId")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package maestro.android

import maestro_android.MaestroAndroid

object AndroidLaunchArguments {

fun Map<String, Any>.toAndroidLaunchArguments(): List<MaestroAndroid.ArgumentValue> {
return toList().map {
when (val value = it.second) {
is Boolean -> MaestroAndroid.ArgumentValue.newBuilder()
.setKey(it.first)
.setValue(value.toString())
.setType(Boolean::class.java.name)
.build()
is Int -> MaestroAndroid.ArgumentValue.newBuilder()
.setKey(it.first)
.setValue(value.toString())
.setType(Int::class.java.name)
.build()
is Double -> MaestroAndroid.ArgumentValue.newBuilder()
.setKey(it.first)
.setValue(value.toString())
.setType(Double::class.java.name)
.build()
is String -> MaestroAndroid.ArgumentValue.newBuilder()
.setKey(it.first)
.setValue(value.toString())
.setType(String::class.java.name)
.build()
else -> MaestroAndroid.ArgumentValue.newBuilder()
.setKey(it.first)
.setValue(value.toString())
.setType(String::class.java.name)
.build()
}
}
}
}
31 changes: 12 additions & 19 deletions maestro-client/src/main/java/maestro/drivers/AndroidDriver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ import maestro.UiElement
import maestro.UiElement.Companion.toUiElementOrNull
import maestro.ViewHierarchy
import maestro.android.AndroidAppFiles
import maestro.android.asManifest
import maestro.android.resolveLauncherActivity
import maestro.android.AndroidLaunchArguments.toAndroidLaunchArguments
import maestro.utils.MaestroTimer
import maestro.utils.ScreenshotUtils
import maestro.utils.StringUtils.toRegexSafe
Expand All @@ -48,6 +47,7 @@ import maestro_android.checkWindowUpdatingRequest
import maestro_android.deviceInfoRequest
import maestro_android.eraseAllTextRequest
import maestro_android.inputTextRequest
import maestro_android.launchAppRequest
import maestro_android.screenshotRequest
import maestro_android.setLocationRequest
import maestro_android.tapRequest
Expand All @@ -60,7 +60,6 @@ import okio.source
import org.slf4j.LoggerFactory
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.xml.sax.SAXException
import java.io.File
import java.io.IOException
import java.util.UUID
Expand Down Expand Up @@ -171,28 +170,22 @@ class AndroidDriver(

override fun launchApp(
appId: String,
launchArguments: List<String>,
launchArguments: Map<String, Any>,
sessionId: UUID?,
) {
if (!isPackageInstalled(appId)) {
throw IllegalArgumentException("Package $appId is not installed")
}

try {
val apkFile = AndroidAppFiles.getApkFile(dadb, appId)
val manifest = apkFile.asManifest()
runCatching {
val sessionUUID = sessionId ?: UUID.randomUUID()
dadb.shell("setprop debug.maestro.sessionId $sessionUUID")
val launcherActivity = manifest.resolveLauncherActivity(appId)
val shellResponse = dadb.shell("am start-activity -n $appId/${launcherActivity}")
if (shellResponse.errorOutput.isNotEmpty()) shell("monkey --pct-syskeys 0 -p $appId 1")
}.onFailure { shell("monkey --pct-syskeys 0 -p $appId 1") }
} catch (ioException: IOException) {
shell("monkey --pct-syskeys 0 -p $appId 1")
} catch (saxException: SAXException) {
shell("monkey --pct-syskeys 0 -p $appId 1")
}
val arguments = launchArguments.toAndroidLaunchArguments()
val sessionUUID = sessionId ?: UUID.randomUUID()
dadb.shell("setprop debug.maestro.sessionId $sessionUUID")
blockingStub.launchApp(
launchAppRequest {
this.packageName = appId
this.arguments.addAll(arguments)
}
) ?: throw IllegalStateException("Maestro driver failed to launch app")
}

override fun stopApp(appId: String) {
Expand Down
2 changes: 1 addition & 1 deletion maestro-client/src/main/java/maestro/drivers/IOSDriver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ class IOSDriver(

override fun launchApp(
appId: String,
launchArguments: List<String>,
launchArguments: Map<String, Any>,
sessionId: UUID?,
) {
iosDevice.launch(appId, launchArguments, sessionId)
Expand Down
2 changes: 1 addition & 1 deletion maestro-client/src/main/java/maestro/drivers/WebDriver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class WebDriver(val isStudio: Boolean) : Driver {

override fun launchApp(
appId: String,
launchArguments: List<String>,
launchArguments: Map<String, Any>,
sessionId: UUID?,
) {
open()
Expand Down
Binary file modified maestro-client/src/main/resources/maestro-app.apk
Binary file not shown.
Binary file modified maestro-client/src/main/resources/maestro-server.apk
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package maestro.android

import com.google.common.truth.Truth.assertThat
import maestro.android.AndroidLaunchArguments.toAndroidLaunchArguments
import maestro_android.MaestroAndroid
import org.junit.jupiter.api.Test

class AndroidLaunchArgumentsTest {

@Test
fun `it correctly parses to android launch arguments`() {
// given
val arguments = mapOf<String, Any>(
"isMaestro" to true,
"cartValue" to 4,
"cartValueDouble" to 4.4,
"cartColor" to "Hello this is cart value which is orange"
)

// when
val launchArguments = arguments.toAndroidLaunchArguments()

// then
assertThat(launchArguments).isEqualTo(
listOf(
provideArgumentValue("isMaestro", "true", Boolean::class.java.name),
provideArgumentValue("cartValue", "4", Int::class.java.name),
provideArgumentValue("cartValueDouble", "4.4", Double::class.java.name),
provideArgumentValue("cartColor", "Hello this is cart value which is orange", String::class.java.name)
)
)
}

private fun provideArgumentValue(key: String, value: String, type: String): MaestroAndroid.ArgumentValue {
return MaestroAndroid.ArgumentValue.newBuilder()
.setKey(key)
.setValue(value)
.setType(type)
.build()
}
}
28 changes: 28 additions & 0 deletions maestro-ios-driver/src/main/kotlin/util/IOSLaunchArguments.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package util

object IOSLaunchArguments {

fun Map<String, Any>.toIOSLaunchArguments(): List<String> {
if (isEmpty()) return emptyList()

val iOSLaunchArgumentsMap = mutableMapOf<String, Any>()
forEach { (key, value) ->
if (value is Boolean) {
iOSLaunchArgumentsMap[key] = value
} else {
if (!key.startsWith("-")) {
iOSLaunchArgumentsMap["-$key"] = value
} else {
iOSLaunchArgumentsMap[key] = value
}
}
}
val iOSLaunchArguments = mutableListOf<String>()
iOSLaunchArgumentsMap.toList().map { "${it.first}:${it.second}" }
.forEach {
iOSLaunchArguments += it.split(":")
}

return iOSLaunchArguments
}
}
51 changes: 51 additions & 0 deletions maestro-ios-driver/src/test/kotlin/IOSLaunchArgumentsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
import util.IOSLaunchArguments.toIOSLaunchArguments

class IOSLaunchArgumentsTest {

@Test
fun `boolean params with one key are not touched`() {
// given
val launchArguments = mapOf("isCartScreen" to true)

// when
val iOSLaunchArguments = launchArguments.toIOSLaunchArguments()

// then
assertThat(iOSLaunchArguments).isEqualTo(listOf("isCartScreen", "true"))
}

@Test
fun `key-value pair without prefixed '-' sign are transformed`() {
// given
val launchArguments = mapOf<String, Any>(
"isCartScreen" to false,
"cartValue" to 3
)

// when
val iOSLaunchArguments = launchArguments.toIOSLaunchArguments()

// then
assertThat(iOSLaunchArguments).isEqualTo(listOf("isCartScreen", "false", "-cartValue", "3"))
}

@Test
fun `key-value pair with prefixed '-' sign are not changed`() {
// given
val launchArguments = mapOf<String, Any>(
"isCartScreen" to false,
"cartValue" to 3,
"-cartColor" to "Orange"
)

// when
val iOSLaunchArguments = launchArguments.toIOSLaunchArguments()

// then
assertThat(iOSLaunchArguments).isEqualTo(
listOf("isCartScreen", "false", "-cartValue", "3", "-cartColor", "Orange")
)
}
}
2 changes: 1 addition & 1 deletion maestro-ios/src/main/java/ios/IOSDevice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ interface IOSDevice : AutoCloseable {
*/
fun launch(
id: String,
launchArguments: List<String>,
launchArguments: Map<String, Any>,
maestroSessionId: UUID?,
): Result<Unit, Throwable>

Expand Down
2 changes: 1 addition & 1 deletion maestro-ios/src/main/java/ios/LocalIOSDevice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class LocalIOSDevice(

override fun launch(
id: String,
launchArguments: List<String>,
launchArguments: Map<String, Any>,
maestroSessionId: UUID?
): Result<Unit, Throwable> {
return simctlIOSDevice.launch(id, launchArguments, maestroSessionId)
Expand Down
6 changes: 1 addition & 5 deletions maestro-ios/src/main/java/ios/idb/IdbIOSDevice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ class IdbIOSDevice(

override fun launch(
id: String,
launchArguments: List<String>,
launchArguments: Map<String, Any>,
maestroSessionId: UUID?,
): Result<Unit, Throwable> {
return runWithRestartRecovery {
Expand All @@ -392,10 +392,6 @@ class IdbIOSDevice(
launchRequest {
start = idb.LaunchRequestKt.start {
bundleId = id

if (launchArguments.isNotEmpty()) {
appArgs.addAll(launchArguments)
}
}
}
)
Expand Down
Loading

0 comments on commit 3e55506

Please sign in to comment.