Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* The MIT License (MIT)
*
* Modified work copyright (c) 2022-2024 ndtp
* Modified work copyright (c) 2022-2026 ndtp
* Original work copyright (c) 2019 Shopify Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
Expand All @@ -26,7 +26,6 @@
package dev.testify

import dev.testify.TestifyPlugin.Companion.EVALUATED_SETTINGS
import dev.testify.internal.Adb
import dev.testify.internal.Style.Description
import dev.testify.internal.android
import dev.testify.internal.isVerbose
Expand Down Expand Up @@ -81,8 +80,6 @@ class TestifyPlugin : Plugin<Project> {
if (project.isVerbose) println(Description, "Adding androidTestImplementation($dependency)")
project.dependencies.add("androidTestImplementation", dependency)
}

Adb.init(project)
}

private fun Project.addManifestPlaceholders(settings: TestifySettings) {
Expand Down
51 changes: 18 additions & 33 deletions Plugins/Gradle/src/main/kotlin/dev/testify/internal/Adb.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* The MIT License (MIT)
*
* Modified work copyright (c) 2022-2024 ndtp
* Modified work copyright (c) 2022-2026 ndtp
* Original work copyright (c) 2019 Shopify Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
Expand All @@ -25,18 +25,29 @@

package dev.testify.internal

import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import dev.testify.internal.StreamData.BufferedStream
import dev.testify.internal.Style.Description
import org.gradle.api.GradleException
import org.gradle.api.Project

class Adb {
class Adb(
private val adbService: AdbService
) {

private val arguments = ArrayList<String>()
private var streamData: StreamData? = null

private val adbPath: String
get() = adbService.adbPath

private val deviceTargetIndex: Int
get() = adbService.deviceTargetIndex

private val verbose: Boolean
get() = adbService.verbose

private val forcedUser: Int?
get() = adbService.forcedUser

fun emulator(): Adb {
arguments.add("-e")
return this
Expand Down Expand Up @@ -72,7 +83,7 @@ class Adb {
if (forcedUser != null) {
arguments("--user", "$forcedUser")
} else {
val user = Device.user
val user = Device.user(adbService)
if (user.isNotEmpty() && (user.toIntOrNull() ?: 0) > 0) {
arguments("--user", user)
}
Expand All @@ -97,7 +108,7 @@ class Adb {

fun execute(targetsDevice: Boolean = true): String {
if (targetsDevice) {
val deviceTarget = Device.targets[deviceTargetIndex]
val deviceTarget = Device.targets(adbService)[deviceTargetIndex]
if (deviceTarget != null) {
arguments.add(0, "-s")
arguments.add(1, deviceTarget)
Expand All @@ -121,32 +132,6 @@ class Adb {

return this
}

companion object {
private var adbPathProvider: (() -> String)? = null
private var verbose: Boolean = false
var forcedUser: Int? = null
private var deviceTargetIndex: Int = 0

fun init(project: Project) {
adbPathProvider = {
val androidComponents = project.extensions.findByType(
ApplicationAndroidComponentsExtension::class.java
) ?: project.extensions.findByType(
LibraryAndroidComponentsExtension::class.java
)
androidComponents?.sdkComponents?.adb?.get()?.asFile?.absolutePath
?: throw GradleException("adb not found via androidComponents.sdkComponents")
}
deviceTargetIndex = (project.properties["device"] as? String)?.toInt() ?: 0
verbose = project.isVerbose
forcedUser = project.user
}

private val adbPath: String
get() = adbPathProvider?.invoke()
?: throw GradleException("Adb.init() must be called before using Adb")
}
}

typealias AdbParam = Pair<String, String>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2026 ndtp
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package dev.testify.internal

import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters

abstract class AdbService : BuildService<AdbService.Params> {
interface Params : BuildServiceParameters {
val adbPath: Property<String>
val verbose: Property<Boolean>
val forcedUser: Property<Int>
val deviceTargetIndex: Property<Int>
}

val adbPath: String
get() = parameters.adbPath.get()

val verbose: Boolean
get() = parameters.verbose.get()

val forcedUser: Int?
get() = parameters.forcedUser.orNull

val deviceTargetIndex: Int
get() = parameters.deviceTargetIndex.get()
}

internal fun Project.getAdbServiceProvider(): Provider<AdbService> =
gradle.sharedServices.registerIfAbsent("adbService", AdbService::class.java) {
val androidComponents =
extensions.findByType(ApplicationAndroidComponentsExtension::class.java)
?: extensions.findByType(LibraryAndroidComponentsExtension::class.java)
?: throw GradleException("?")
it.parameters.adbPath.set(
androidComponents?.sdkComponents?.adb?.get()?.asFile?.absolutePath
?: throw GradleException("adb not found via androidComponents.sdkComponents")
)
it.parameters.verbose.set(project.isVerbose)
it.parameters.forcedUser.set(project.user)
it.parameters.deviceTargetIndex.set((project.properties["device"] as? String)?.toInt() ?: 0)
}
169 changes: 81 additions & 88 deletions Plugins/Gradle/src/main/kotlin/dev/testify/internal/Device.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* The MIT License (MIT)
*
* Modified work copyright (c) 2022-2024 ndtp
* Modified work copyright (c) 2022-2026 ndtp
* Original work copyright (c) 2019 Shopify Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
Expand All @@ -27,119 +27,112 @@ package dev.testify.internal

object Device {

private val version: Int
get() {
return Adb().getprop("ro.build.version.sdk").toInt()
}
fun version(adbService: AdbService): Int {
return Adb(adbService).getprop(adbService, "ro.build.version.sdk").toInt()
}

val locale: String
get() {
return when {
version in 21..22 -> {
var language = Adb().getprop("persist.sys.language")
if (language.isBlank()) {
language = "en"
}
var region = Adb().getprop("persist.sys.country")
if (region.isBlank()) {
region = "US"
}
"${language}_$region"
fun locale(adbService: AdbService): String {
return when {
version(adbService) in 21..22 -> {
var language = Adb(adbService).getprop(adbService, "persist.sys.language")
if (language.isBlank()) {
language = "en"
}

version >= 23 -> {
var result = Adb().getprop("persist.sys.locale").trim().replace("-", "_")
if (result.isBlank()) {
result = "en_US"
}
return result
var region = Adb(adbService).getprop(adbService, "persist.sys.country")
if (region.isBlank()) {
region = "US"
}
"${language}_$region"
}

else -> "unsupported"
version(adbService) >= 23 -> {
var result = Adb(adbService).getprop(adbService, "persist.sys.locale").trim().replace("-", "_")
if (result.isBlank()) {
result = "en_US"
}
return result
}
}

val timeZone: String
get() {
return Adb().getprop("persist.sys.timezone")
else -> "unsupported"
}
}

private val displayDensity: Int
get() {
val densityLine = Adb()
.shell()
.arguments(
"wm",
"density"
)
.execute().trim()
return if (densityLine.contains("Override density", true)) {
densityLine.split(":").last().trim().toInt()
} else {
densityLine.substring("Physical density: ".length).trim().toInt()
}
}
fun timeZone(adbService: AdbService): String {
return Adb(adbService).getprop(adbService, "persist.sys.timezone")
}

private val displaySize: String
get() {
val sizeLine = Adb()
.shell()
.arguments(
"wm",
"size"
)
.execute().trim()
return sizeLine.substring("Physical size: ".length).trim()
fun displayDensity(adbService: AdbService): Int {
val densityLine = Adb(adbService)
.shell()
.arguments(
"wm",
"density"
)
.execute().trim()
return if (densityLine.contains("Override density", true)) {
densityLine.split(":").last().trim().toInt()
} else {
densityLine.substring("Physical density: ".length).trim().toInt()
}
}

internal val user: String
get() {
val user = Adb()
.shell()
.arguments(
"am",
"get-current-user"
)
.execute().trim()
return user.ifEmpty { "0" }
}
fun displaySize(adbService: AdbService): String {
val sizeLine = Adb(adbService)
.shell()
.arguments(
"wm",
"size"
)
.execute().trim()
return sizeLine.substring("Physical size: ".length).trim()
}

internal fun user(adbService: AdbService): String {
val user = Adb(adbService)
.shell()
.arguments(
"am",
"get-current-user"
)
.execute().trim()
return user.ifEmpty { "0" }
}

internal fun deviceKey(): String {
return "$version-$displaySize@${displayDensity}dp-$locale"
internal fun deviceKey(adbService: AdbService): String {
return "${version(adbService)}-${displaySize(adbService)}@${displayDensity(adbService)}dp-${locale(adbService)}"
}

private fun Adb.getprop(prop: String): String {
private fun Adb.getprop(adbService: AdbService, prop: String): String {
shell()
argument("getprop")
argument(prop)
return execute().trim()
}

val isEmpty: Boolean
get() = (count == 0)
fun isEmpty(adbService: AdbService): Boolean {
return count(adbService) == 0
}

val count: Int
get() {
val result = Adb()
.argument("devices")
.execute(targetsDevice = false)
fun count(adbService: AdbService): Int {
val result = Adb(adbService)
.argument("devices")
.execute(targetsDevice = false)

return result.lines().count {
it.isNotBlank() && !it.contains("List of devices attached")
}
return result.lines().count {
it.isNotBlank() && !it.contains("List of devices attached")
}
}

val targets: Map<Int, String>
get() {
val map = HashMap<Int, String>()
enumerateDevices().mapIndexed { index, s ->
map[index] = s
}
return map
fun targets(adbService: AdbService): Map<Int, String> {
val map = HashMap<Int, String>()
enumerateDevices(adbService).mapIndexed { index, s ->
map[index] = s
}
return map
}

private fun enumerateDevices(): List<String> {
val result = Adb()
private fun enumerateDevices(adbService: AdbService): List<String> {
val result = Adb(adbService)
.argument("devices")
.execute(targetsDevice = false)

Expand Down
Loading