From 838ec726cca74a97829107f139fa21559d05d6de Mon Sep 17 00:00:00 2001 From: Pierre-Yves Ricau Date: Sat, 7 Sep 2024 14:47:27 -0700 Subject: [PATCH] dump view hierarchy with ui automator to find other apps --- .github/workflows/android.yml | 4 -- .../PapaTestInstrumentationRunner.kt | 70 +++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 11ffc5e..4770447 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -87,11 +87,7 @@ jobs: chmod 777 emulator.log # allow writing to log file adb logcat >> emulator.log & # pipe all logcat messages into log file as a background process adb shell settings put global package_verifier_user_consent -1 - adb shell "screenrecord --bugreport /data/local/tmp/testRecording.mp4 & echo \$! > /data/local/tmp/screenrecord_pid.txt" & ./gradlew connectedCheck --no-build-cache --no-daemon --stacktrace - adb shell "kill -2 \$(cat /data/local/tmp/screenrecord_pid.txt)" - sleep 1 - adb pull /data/local/tmp/testRecording.mp4 . - name: Upload results if: ${{ always() }} uses: actions/upload-artifact@v3 diff --git a/papa/src/androidTest/java/papa/test/utilities/PapaTestInstrumentationRunner.kt b/papa/src/androidTest/java/papa/test/utilities/PapaTestInstrumentationRunner.kt index 04585f3..b274602 100644 --- a/papa/src/androidTest/java/papa/test/utilities/PapaTestInstrumentationRunner.kt +++ b/papa/src/androidTest/java/papa/test/utilities/PapaTestInstrumentationRunner.kt @@ -5,8 +5,13 @@ import androidx.test.espresso.Espresso import androidx.test.espresso.base.DefaultFailureHandler import androidx.test.platform.app.InstrumentationRegistry import androidx.test.runner.AndroidJUnitRunner +import androidx.test.uiautomator.UiDevice +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserFactory import radiography.Radiography import radiography.ViewStateRenderers.DefaultsIncludingPii +import java.io.ByteArrayOutputStream +import java.io.StringReader class PapaTestInstrumentationRunner : AndroidJUnitRunner() { @@ -18,6 +23,66 @@ class PapaTestInstrumentationRunner : AndroidJUnitRunner() { try { defaultFailureHandler.handle(error, viewMatcher) } catch (decoratedError: Throwable) { + val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + val os = ByteArrayOutputStream() + uiDevice.dumpWindowHierarchy(os) + val uiAutomatorWindowHierarchy = os.toString() + val factory = XmlPullParserFactory.newInstance() + val xpp = factory.newPullParser() + xpp.setInput(StringReader(uiAutomatorWindowHierarchy)) + val result = StringBuilder() + + var eventType = xpp.eventType + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + when (xpp.name) { + "hierarchy" -> { + result.appendLine( + "view hierarchy with screen rotation ${xpp.getAttributeValue(null, "rotation")}" + ) + } + "node" -> { + val interestingAttributes = listOf( + "text", "resource-id", "checked", "enabled", "focused", "selected", "bounds", + "visible-to-user", "package" + ) + val className = xpp.getAttributeValue(null, "class").substringAfterLast(".") + val attributes = (0 until xpp.attributeCount) + .asSequence() + .mapNotNull { index -> + val name = xpp.getAttributeName(index) + val value = xpp.getAttributeValue(index) + if (value.isNullOrBlank() || name !in interestingAttributes) { + return@mapNotNull null + } + when (name) { + "checked" -> if (value == "true") "checked" else null + "focused" -> if (value == "true") "focused" else null + "selected" -> if (value == "true") "selected" else null + "enabled" -> if (value == "true") null else "disabled" + "visible-to-user" -> if (value == "true") null else "invisible" + "text" -> "text:\"$value\"" + "package" -> { + // Root view nodes have depth 2 (depth starts at 1 with the "hierarchy" node) + if (xpp.depth == 2) { + "app-package:$value" + } else { + null + } + } + "resource-id" -> "id:${value.substringAfter(":id/")}" + else -> "$name:$value" + } + }.toList().joinToString(separator = ", ") + result.append("│") + .append(" ".repeat(xpp.depth - 2)) + .appendLine("$className { $attributes }") + } + else -> error("Unexpected tag ${xpp.name}") + } + } + eventType = xpp.next() + } val detailMessageField = Throwable::class.java.getDeclaredField("detailMessage") val previouslyAccessible = detailMessageField.isAccessible try { @@ -30,6 +95,11 @@ class PapaTestInstrumentationRunner : AndroidJUnitRunner() { val hierarchy = Radiography.scan(viewStateRenderers = DefaultsIncludingPii) // Notice the plural: there's one view hierarchy per window. message += "\nView hierarchies:\n$hierarchy" + message += "\nUI Automator window hierarchy better:\n$result" + message += "\nUI Automator window hierarchy:\n${ + uiAutomatorWindowHierarchy.lines() + .joinToString("\n") { "-$it" } + }" detailMessageField[decoratedError] = message } finally { detailMessageField.isAccessible = previouslyAccessible