DeviceUtil.kt
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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.
*/
@file:JvmName("DeviceUtil")
package androidx.test.espresso.device.common
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import java.util.regex.Pattern
/** Collection of utility methods for getting information about the test device. */
/**
* Detects if the test is running on an emulator or a real device using some heuristics based on the
* device properties.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun isTestDeviceAnEmulator(): Boolean {
val qemu: String? = System.getProperty("ro.kernel.qemu", "?")
return qemu.equals("1") ||
Build.HARDWARE.contains("goldfish") ||
Build.HARDWARE.contains("ranchu")
}
/**
* Detects if the test is running on Robolectric using some heuristics based on the device
* properties.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun isRobolectricTest(): Boolean {
return Build.FINGERPRINT.equals("robolectric")
}
/**
* Returns the API level of the current test device.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun getDeviceApiLevel(): Int {
return Build.VERSION.SDK_INT
}
/**
* Maps device state names to identifiers by processing the output of "device_state print-states"
*
* The output of "cmd device_state print-states" contains names and identifiers of supported device
* states, as shown below.
* [ DeviceState{identifier=1, name='CLOSED'}, DeviceState{identifier=2, name='HALF_OPENED'}, DeviceState{identifier=3, name='OPENED'}, ]
* This method returns a map where the keys are the device state names and the values are the device
* state identifiers.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun getMapOfDeviceStateNamesToIdentifiers(): MutableMap<String, String> {
if (getDeviceApiLevel() < 24) {
// Executing shell commands requires API 24+. There are no foldable devices with API 23 or
// below, so return an empty map.
return mutableMapOf()
}
val deviceStateNameToIdentifier: MutableMap<String, String> = mutableMapOf()
// Regex pattern that matches supported states, e.g. DeviceState{identifier=1, name='CLOSED',
// app_accessible=true}
val DEVICE_STATE_PATTERN_EXTRA_PARAMS: Pattern =
Pattern.compile("DeviceState\{identifier=(?<identifier>\d+), name='(?<name>\w+?)'.+?\}")
// Regex pattern that matches supported states, e.g. DeviceState{identifier=1, name='CLOSED'}
val DEVICE_STATE_PATTERN_NO_EXTRA_PARAMS: Pattern =
Pattern.compile("DeviceState\{identifier=(?<identifier>\d+), name='(?<name>\w+?)'\}")
val supportedDeviceStates = listOf("CLOSED", "HALF_OPENED", "OPENED")
val printedStates = executeShellCommand("cmd device_state print-states")
val lines = printedStates.split("\n")
for (line in lines) {
val matcherExtraParams = DEVICE_STATE_PATTERN_EXTRA_PARAMS.matcher(line)
val matcherNoExtraParams = DEVICE_STATE_PATTERN_NO_EXTRA_PARAMS.matcher(line)
var deviceStateName: String? = null
var deviceStateIdentifier: String? = null
if (matcherExtraParams.find()) {
deviceStateName = matcherExtraParams.group("name")
deviceStateIdentifier = matcherExtraParams.group("identifier")
} else if (matcherNoExtraParams.find()) {
deviceStateName = matcherNoExtraParams.group("name")
deviceStateIdentifier = matcherNoExtraParams.group("identifier")
}
if (
deviceStateName != null &&
deviceStateIdentifier != null &&
deviceStateName in supportedDeviceStates
) {
deviceStateNameToIdentifier.put(deviceStateName, deviceStateIdentifier)
}
}
return deviceStateNameToIdentifier
}
/**
* Returns a Pair containing the current width and height of the test device in pixels
*
* @hide
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun calculateCurrentDisplayWidthAndHeightPx(): Pair<Int, Int> {
// "wm size" will output a string with the format
// "Physical size: WxH
// Override size: WxH"
val output = executeShellCommand("wm size")
var subStringToFind = "Override size: "
if (output.contains(subStringToFind)) {
val displaySizes =
output.substring(output.indexOf(subStringToFind) + subStringToFind.length).trim().split("x")
val widthPx = displaySizes.get(0).toInt()
val heightPx = displaySizes.get(1).toInt()
return Pair(widthPx, heightPx)
} else {
// If the display size has not been overriden, the "wm size" output will only contain physical
// size
subStringToFind = "Physical size: "
val displaySizes =
output.substring(output.indexOf(subStringToFind) + subStringToFind.length).trim().split("x")
val widthPx = displaySizes.get(0).toInt()
val heightPx = displaySizes.get(1).split("\n").get(0).toInt()
return Pair(widthPx, heightPx)
}
}