Solutions for app Android Dev Quest by Richard Zadorozny
https://play.google.com/store/apps/details?id=quest.androiddev
[This document is created in haste, some solutions might not be complete or insufficient]
Quiz 1 - show layout bounds
Quiz 2 - running services
Quiz 3 - debug gpu overdraw
Quiz 4 - show hardware layer updates
Quiz 5 - simulate secondary display
Quiz 6 - Pointer location
Quiz 7 - show hardware layer updates + show layout bounds + simulate secondary display
Quiz 8 - Mock location - locaedit app and Marion Island Meteorological Station
Quiz 9 - Answer in logcat with radio buffer
Quiz 10 - nc localhost [portnumber]
Quiz 11- adb shell am startservice -n quest.androiddev/.morgannode adb shell am broadcast -a quest.androiddev.READY_TO_WORK -p quest.androiddev adb shell am broadcast -a quest.androiddev.READY_TO_WORK -p quest.androiddev -c android.intent.category.INFO -t message/global
Quiz 12 - adb shell am crash quest.androiddev
Quiz 13 - adb shell input tap x y adb shell input swipe x1 y1 x2 y2
Quiz 14- Read the file at path content query --url content://.. content call .. url .. method ..arg Echo result to verify.xxx.txt path seen on swipe up
Quiz 15- adb shell am broadcast -a HERE_KITTY_KITTY adb shell input tap ... adb shell input swipe ...
Quiz 16: Register New Agent Approach: Instrumented test that launches the app and clicks the "Register" button. . Install APK with adb install -g for granting dangerous permission {necessary}
private const val QUEST_PACKAGE = "quest.androiddev"
private const val REGISTER_ACTION = "quest.androiddev.action.AGENT_REGISTER"
private const val REGISTER_PERMISSION = "quest.androiddev.permission.AGENT_INSTRUMENTS"
private const val USERNAME = "your_username"
@Test
fun registerNewAgent() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val intent = Intent(REGISTER_ACTION).apply {
`package` = QUEST_PACKAGE
putExtra("EXTRA_USERNAME", USERNAME)
}
context.sendBroadcast(intent)
}
Quiz 17: Invisible Node + Progressions Note: below code might not be fully correct the solution has progressions eg fist you need to click the code then call custom action on it like galvanize etc.
// Custom accessibility action IDs from the quest app
private const val ACTION_GALVANIZE = 2131296273
private const val ACTION_SET_PROGRESS = 16908349 // android.R.id.accessibilityActionSetProgress
@Test
fun invisibleNode_allSteps() {
val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
// Step 1: Click
var node = findInvisibleNode(uiAutomation) ?: error("Invisible node not found")
node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
Thread.sleep(1000)
// Step 2: Galvanize (re-fetch node)
node = findInvisibleNode(uiAutomation) ?: error("Invisible node not found")
node.performAction(ACTION_GALVANIZE)
Thread.sleep(1000)
// Step 3: Set Progress to 100 (re-fetch node)
node = findInvisibleNode(uiAutomation) ?: error("Invisible node not found")
val args = Bundle().apply {
putFloat(AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE, 100f)
}
node.performAction(ACTION_SET_PROGRESS, args)
}
private fun findInvisibleNode(uiAutomation: UiAutomation): AccessibilityNodeInfo? {
uiAutomation.serviceInfo = AccessibilityServiceInfo().apply {
flags = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS or
AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
}
Thread.sleep(500)
val root = uiAutomation.rootInActiveWindow ?: return null
val queue = ArrayDeque<AccessibilityNodeInfo>().apply { add(root) }
while (queue.isNotEmpty()) {
val node = queue.removeFirst()
if (node.contentDescription?.toString()?.contains("Invisible", ignoreCase = true) == true) {
return node
}
for (i in 0 until node.childCount) {
node.getChild(i)?.let(queue::add)
}
}
return null
}
Quiz 18: AIDL + Unlock Slots Note: Call openSlot(20) → triggers "error 28" → call openSlot(28) → reveals unlock_payload in getScreenData() → call submit(unlock_payload) Prerequisites: AIDL file at app/src/main/aidl/com/qualityserviceagents/ICoreAccessService.aidl buildFeatures { aidl = true } in build.gradle.kts in AndroidManifest.xml
@Test
fun coreAccessService_solve() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val latch = CountDownLatch(1)
var service: ICoreAccessService? = null
val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
service = ICoreAccessService.Stub.asInterface(binder)
latch.countDown()
}
override fun onServiceDisconnected(name: ComponentName?) {}
}
val intent = Intent().apply {
component = ComponentName("quest.androiddev",
"com.qualityserviceagents.CoreAccessService")
}
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
latch.await(10, TimeUnit.SECONDS)
// Step 1: Open slot 20 to trigger error 28
service!!.openSlot(20)
// Step 2: Open slot 28 to reveal unlock_payload
service!!.openSlot(28)
// Step 3: Get screen data with unlock_payload
val screenData = service!!.screenData
val unlockPayload = screenData.getString("unlock_payload")
// Step 4: Submit unlock_payload
service!!.submit(unlockPayload)
context.unbindService(connection)
}
Quiz 19: Agent Chat - Receive Code via Callback Note: Register ICoreAccessCallback with the service. When onResult(payload) fires, submit the payload back.
@Test
fun solveAgentChat() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val latch = CountDownLatch(1)
var service: ICoreAccessService? = null
val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
service = ICoreAccessService.Stub.asInterface(binder)
latch.countDown()
}
override fun onServiceDisconnected(name: ComponentName?) {}
}
val intent = Intent().apply {
component = ComponentName("quest.androiddev",
"com.qualityserviceagents.CoreAccessService")
}
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
latch.await(10, TimeUnit.SECONDS)
val codeLatch = CountDownLatch(1)
var receivedCode: String? = null
val callback = object : ICoreAccessCallback.Stub() {
override fun onResult(payload: String?) {
receivedCode = payload
codeLatch.countDown()
}
}
service?.addCallback(callback)
// Wait for code (may take time during chat)
codeLatch.await(30, TimeUnit.SECONDS)
// Submit received code
receivedCode?.let { service?.submit(it) }
context.unbindService(connection)
}
Quiz 20: Rocket Fire (Typing Defense)
Note: this has some complications some events need multiple keys eg "ME" "GO" eg update solution accordingly
@Test
fun solve() {
val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
val letterQueue = ConcurrentLinkedQueue<Char>()
val listener = UiAutomation.OnAccessibilityEventListener { event ->
if (event?.packageName == "quest.androiddev" &&
event.className?.contains("TypingWizard") == true &&
event.contentDescription == "new letters") {
// IMPORTANT: iterate all characters, not just single-char events!
event.text.firstOrNull()?.toString()?.forEach { char ->
if (char.isLetter()) letterQueue.add(char.uppercaseChar())
}
}
}
uiAutomation.setOnAccessibilityEventListener(listener)
// Main typing loop
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < 300_000) {
val letter = letterQueue.poll() ?: continue
val keyCode = KeyEvent.KEYCODE_A + (letter - 'A')
val now = SystemClock.uptimeMillis()
val down = KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0)
val up = KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0)
uiAutomation.injectInputEvent(down, true)
uiAutomation.injectInputEvent(up, true)
}
}
Quiz 21: Boss Quiz (All Combined) https://youtu.be/rr-CA1I_YMk?si=g9Z2BZjjiD4E_zsD
Note: All three tasks run simultaneously: Typing falling letters (continuous) Receiving AIDL callback codes and submitting them Finding "morgan.node" and performing "transcend" action
@Test
fun solveWithCallback() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
val uiAutomation = instrumentation.uiAutomation
val context = instrumentation.targetContext
// ========== AIDL SERVICE SETUP ==========
val serviceLatch = CountDownLatch(1)
var service: ICoreAccessService? = null
val callbackCodes = ConcurrentLinkedQueue<String>()
val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
service = ICoreAccessService.Stub.asInterface(binder)
serviceLatch.countDown()
}
override fun onServiceDisconnected(name: ComponentName?) { service = null }
}
val serviceIntent = Intent().apply {
component = ComponentName("quest.androiddev", "com.qualityserviceagents.CoreAccessService")
}
context.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE)
serviceLatch.await(10, TimeUnit.SECONDS)
// Register callback
val callback = object : ICoreAccessCallback.Stub() {
override fun onResult(payload: String?) {
if (!payload.isNullOrEmpty()) callbackCodes.add(payload)
}
}
service?.addCallback(callback)
// ========== ACCESSIBILITY EVENT SETUP ==========
val letterQueue = ConcurrentLinkedQueue<Char>()
val morganTranscended = AtomicBoolean(false)
val listener = UiAutomation.OnAccessibilityEventListener { event ->
if (event?.packageName == "quest.androiddev" &&
event.className?.contains("TypingWizard") == true &&
event.contentDescription == "new letters") {
event.text.firstOrNull()?.toString()?.forEach { char ->
if (char.isLetter()) letterQueue.add(char.uppercaseChar())
}
}
}
uiAutomation.setOnAccessibilityEventListener(listener)
// ========== MAIN LOOP ==========
val startTime = System.currentTimeMillis()
var lastMorganScan = 0L
while (System.currentTimeMillis() - startTime < 300_000) {
// 1. Process AIDL callback codes
callbackCodes.poll()?.let { code ->
service?.submit(code)
}
// 2. Type letters
letterQueue.poll()?.let { letter ->
val keyCode = KeyEvent.KEYCODE_A + (letter - 'A')
val now = SystemClock.uptimeMillis()
uiAutomation.injectInputEvent(KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0), true)
uiAutomation.injectInputEvent(KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0), true)
}
// 3. Scan for Morgan Node every 2 seconds
if (!morganTranscended.get() && System.currentTimeMillis() - lastMorganScan > 2000) {
lastMorganScan = System.currentTimeMillis()
findMorganNode(uiAutomation)?.let { morganNode ->
// Find and perform "transcend" action
morganNode.actionList?.find {
it.label?.toString()?.contains("transcend", ignoreCase = true) == true
}?.let { action ->
if (morganNode.performAction(action.id)) morganTranscended.set(true)
} ?: run {
// Fallback: try all custom actions
morganNode.actionList?.filter { it.id > 0x10000 }?.forEach { action ->
morganNode.performAction(action.id)
}
}
morganNode.recycle()
}
}
}
context.unbindService(connection)
}
private fun findMorganNode(uiAutomation: UiAutomation): AccessibilityNodeInfo? {
val root = uiAutomation.rootInActiveWindow ?: return null
val queue = ArrayDeque<AccessibilityNodeInfo>().apply { add(root) }
while (queue.isNotEmpty()) {
val node = queue.removeFirst()
val desc = node.contentDescription?.toString() ?: ""
if (desc.contains("morgan", ignoreCase = true)) return node
for (i in 0 until node.childCount) {
node.getChild(i)?.let(queue::add)
}
}
return null
}