blob: 5acfa6a4d2d49a77cf92713d5af501c8c6fe25dc [file] [log] [blame]
/*
* 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.
*/
package com.android.tools.agent.appinspection
import android.content.Context
import android.content.res.Resources
import android.graphics.Picture
import android.view.Surface
import android.view.View
import android.view.ViewGroup
import android.view.ViewRootImpl
import android.view.WindowManagerGlobal
import android.widget.TextView
import com.android.tools.agent.appinspection.proto.StringTable
import com.android.tools.agent.appinspection.testutils.FrameworkStateRule
import com.android.tools.agent.appinspection.testutils.MainLooperRule
import com.android.tools.agent.appinspection.testutils.inspection.InspectorRule
import com.android.tools.agent.appinspection.util.ThreadUtils
import com.android.tools.agent.appinspection.util.decompress
import com.google.common.truth.Truth.assertThat
import layoutinspector.view.inspection.LayoutInspectorViewProtocol.Command
import layoutinspector.view.inspection.LayoutInspectorViewProtocol.Event
import layoutinspector.view.inspection.LayoutInspectorViewProtocol.Response
import layoutinspector.view.inspection.LayoutInspectorViewProtocol.Screenshot
import layoutinspector.view.inspection.LayoutInspectorViewProtocol.StopFetchCommand
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.ArrayBlockingQueue
class ViewLayoutInspectorTest {
@get:Rule
val mainLooperRule = MainLooperRule()
@get:Rule
val inspectorRule = InspectorRule()
@get:Rule
val frameworkRule = FrameworkStateRule()
@Test
fun canStartAndStopInspector() = createViewInspector { viewInspector ->
val responseQueue = ArrayBlockingQueue<ByteArray>(1)
inspectorRule.commandCallback.replyListeners.add { bytes ->
responseQueue.add(bytes)
}
val startFetchCommand = Command.newBuilder().apply {
startFetchCommandBuilder.apply {
continuous = true
}
}.build()
viewInspector.onReceiveCommand(
startFetchCommand.toByteArray(),
inspectorRule.commandCallback
)
responseQueue.take().let { bytes ->
val response = Response.parseFrom(bytes)
assertThat(response.specializedCase).isEqualTo(Response.SpecializedCase.START_FETCH_RESPONSE)
}
val stopFetchCommand = Command.newBuilder().apply {
stopFetchCommand = StopFetchCommand.getDefaultInstance()
}.build()
viewInspector.onReceiveCommand(
stopFetchCommand.toByteArray(),
inspectorRule.commandCallback
)
responseQueue.take().let { bytes ->
val response = Response.parseFrom(bytes)
assertThat(response.specializedCase).isEqualTo(Response.SpecializedCase.STOP_FETCH_RESPONSE)
}
}
@Test
fun canCaptureTreeInContinuousMode() = createViewInspector { viewInspector ->
val eventQueue = ArrayBlockingQueue<ByteArray>(2)
inspectorRule.connection.eventListeners.add { bytes ->
eventQueue.add(bytes)
}
val resourceNames = mutableMapOf<Int, String>()
val resources = Resources(resourceNames)
val context = Context("view.inspector.test", resources)
val tree1 = View(context).apply { setAttachInfo(View.AttachInfo() )}
val tree2 = View(context).apply { setAttachInfo(View.AttachInfo() )}
val tree3 = View(context).apply { setAttachInfo(View.AttachInfo() )}
WindowManagerGlobal.getInstance().rootViews.addAll(listOf(tree1, tree2))
val startFetchCommand = Command.newBuilder().apply {
startFetchCommandBuilder.apply {
continuous = true
}
}.build()
viewInspector.onReceiveCommand(
startFetchCommand.toByteArray(),
inspectorRule.commandCallback
)
ThreadUtils.runOnMainThread { }.get() // Wait for startCommand to finish initializing
assertThat(eventQueue).isEmpty()
val tree1FakePicture1 = Picture(byteArrayOf(1, 1))
val tree1FakePicture2 = Picture(byteArrayOf(1, 2))
val tree1FakePicture3 = Picture(byteArrayOf(1, 3))
val tree1FakePicture4 = Picture(byteArrayOf(1, 4))
val tree2FakePicture = Picture(byteArrayOf(2))
val tree3FakePicture = Picture(byteArrayOf(3))
tree1.forcePictureCapture(tree1FakePicture1)
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.ROOTS_EVENT)
assertThat(event.rootsEvent.idsList).containsExactly(
tree1.uniqueDrawingId,
tree2.uniqueDrawingId
)
}
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.LAYOUT_EVENT)
event.layoutEvent.let { layoutEvent ->
assertThat(layoutEvent.rootView.id).isEqualTo(tree1.uniqueDrawingId)
assertThat(layoutEvent.screenshot.type).isEqualTo(Screenshot.Type.SKP)
assertThat(layoutEvent.screenshot.bytes.toByteArray()).isEqualTo(tree1FakePicture1.bytes)
}
}
tree2.forcePictureCapture(tree2FakePicture)
// Roots event not resent, as roots haven't changed
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.LAYOUT_EVENT)
event.layoutEvent.let { layoutEvent ->
assertThat(layoutEvent.rootView.id).isEqualTo(tree2.uniqueDrawingId)
assertThat(layoutEvent.screenshot.bytes.toByteArray()).isEqualTo(tree2FakePicture.bytes)
}
}
WindowManagerGlobal.getInstance().rootViews.add(tree3)
tree1.forcePictureCapture(tree1FakePicture2)
// As a side-effect, this capture discovers the newly added third tree
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.ROOTS_EVENT)
assertThat(event.rootsEvent.idsList).containsExactly(
tree1.uniqueDrawingId,
tree2.uniqueDrawingId,
tree3.uniqueDrawingId
)
}
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.LAYOUT_EVENT)
event.layoutEvent.let { layoutEvent ->
assertThat(layoutEvent.rootView.id).isEqualTo(tree1.uniqueDrawingId)
assertThat(layoutEvent.screenshot.bytes.toByteArray()).isEqualTo(tree1FakePicture2.bytes)
}
}
// Roots changed - this should generate a new roots event
WindowManagerGlobal.getInstance().rootViews.remove(tree2)
tree1.forcePictureCapture(tree1FakePicture3)
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.ROOTS_EVENT)
assertThat(event.rootsEvent.idsList).containsExactly(
tree1.uniqueDrawingId,
tree3.uniqueDrawingId
)
}
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.LAYOUT_EVENT)
event.layoutEvent.let { layoutEvent ->
assertThat(layoutEvent.rootView.id).isEqualTo(tree1.uniqueDrawingId)
assertThat(layoutEvent.screenshot.bytes.toByteArray()).isEqualTo(tree1FakePicture3.bytes)
}
}
val stopFetchCommand = Command.newBuilder().apply {
stopFetchCommand = StopFetchCommand.getDefaultInstance()
}.build()
viewInspector.onReceiveCommand(
stopFetchCommand.toByteArray(),
inspectorRule.commandCallback
)
ThreadUtils.runOnMainThread { }.get() // Wait for the stop command to run its course
// Normally, stopping the inspector triggers invalidate calls, but in fake android, those
// do nothing. Instead, we emulate this by manually firing capture events.
tree1.forcePictureCapture(tree1FakePicture4)
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.LAYOUT_EVENT)
event.layoutEvent.let { layoutEvent ->
assertThat(layoutEvent.rootView.id).isEqualTo(tree1.uniqueDrawingId)
assertThat(layoutEvent.screenshot.bytes.toByteArray()).isEqualTo(tree1FakePicture4.bytes)
}
}
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.PROPERTIES_EVENT)
assertThat(event.propertiesEvent.rootId).isEqualTo(tree1.uniqueDrawingId)
}
tree3.forcePictureCapture(tree3FakePicture)
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.LAYOUT_EVENT)
event.layoutEvent.let { layoutEvent ->
assertThat(layoutEvent.rootView.id).isEqualTo(tree3.uniqueDrawingId)
assertThat(layoutEvent.screenshot.bytes.toByteArray()).isEqualTo(tree3FakePicture.bytes)
}
}
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.PROPERTIES_EVENT)
assertThat(event.propertiesEvent.rootId).isEqualTo(tree3.uniqueDrawingId)
}
}
@Test
fun nodeBoundsCapturedAsExpected() = createViewInspector { viewInspector ->
val eventQueue = ArrayBlockingQueue<ByteArray>(2)
inspectorRule.connection.eventListeners.add { bytes ->
eventQueue.add(bytes)
}
val resourceNames = mutableMapOf<Int, String>()
val resources = Resources(resourceNames)
val context = Context("view.inspector.test", resources)
val mainScreen = ViewGroup(context).apply {
setAttachInfo(View.AttachInfo())
width = 400
height = 800
}
val floatingDialog = ViewGroup(context).apply {
setAttachInfo(View.AttachInfo())
width = 300
height = 200
}
val stubPicture = Picture(byteArrayOf(0))
// Used for root offset
floatingDialog.locationInSurface.apply {
x = 10
y = 20
}
// Used for absolution position of dialog root
floatingDialog.locationOnScreen.apply {
x = 80
y = 200
}
mainScreen.addView(ViewGroup(context).apply {
scrollX = 5
scrollY = 100
left = 20
top = 30
width = 40
height = 50
addView(View(context).apply {
left = 40
top = 10
width = 20
height = 30
})
addView(View(context).apply {
left = 40
top = 10
width = 20
height = 30
setTransformedPoints(floatArrayOf(10f, 20f, 30f, 40f, 50f, 60f, 70f, 80f))
})
})
floatingDialog.addView(ViewGroup(context).apply {
scrollX = 5
scrollY = 100
left = 20
top = 30
width = 40
height = 50
addView(View(context).apply {
left = 40
top = 10
width = 20
height = 30
})
addView(View(context).apply {
left = 40
top = 10
width = 20
height = 30
setTransformedPoints(floatArrayOf(10f, 20f, 30f, 40f, 50f, 60f, 70f, 80f))
})
})
WindowManagerGlobal.getInstance().rootViews.addAll(listOf(mainScreen, floatingDialog))
val startFetchCommand = Command.newBuilder().apply {
startFetchCommandBuilder.apply {
continuous = true
}
}.build()
viewInspector.onReceiveCommand(
startFetchCommand.toByteArray(),
inspectorRule.commandCallback
)
ThreadUtils.runOnMainThread { }.get() // Wait for startCommand to finish initializing
assertThat(eventQueue).isEmpty()
mainScreen.forcePictureCapture(stubPicture)
eventQueue.take().let { bytes ->
// In this test, we don't care that much about this event, but we consume io get to the
// layout event
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.ROOTS_EVENT)
}
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.LAYOUT_EVENT)
event.layoutEvent.let { layoutEvent ->
layoutEvent.rootOffset.let { rootOffset ->
assertThat(rootOffset.x).isEqualTo(0)
assertThat(rootOffset.y).isEqualTo(0)
}
val root = layoutEvent.rootView
val parent = root.getChildren(0)
val child0 = parent.getChildren(0)
val child1 = parent.getChildren(1)
assertThat(root.id).isEqualTo(mainScreen.uniqueDrawingId)
root.bounds.layout.let { rect ->
assertThat(rect.x).isEqualTo(0)
assertThat(rect.y).isEqualTo(0)
assertThat(rect.w).isEqualTo(400)
assertThat(rect.h).isEqualTo(800)
}
parent.bounds.layout.let { rect ->
assertThat(rect.x).isEqualTo(20)
assertThat(rect.y).isEqualTo(30)
assertThat(rect.w).isEqualTo(40)
assertThat(rect.h).isEqualTo(50)
}
child0.bounds.layout.let { rect ->
assertThat(rect.x).isEqualTo(55)
assertThat(rect.y).isEqualTo(-60)
assertThat(rect.w).isEqualTo(20)
assertThat(rect.h).isEqualTo(30)
}
child1.bounds.render.let { quad ->
assertThat(quad.x0).isEqualTo(10)
assertThat(quad.y0).isEqualTo(20)
assertThat(quad.x1).isEqualTo(30)
assertThat(quad.y1).isEqualTo(40)
assertThat(quad.x2).isEqualTo(50)
assertThat(quad.y2).isEqualTo(60)
assertThat(quad.x3).isEqualTo(70)
assertThat(quad.y3).isEqualTo(80)
}
}
}
floatingDialog.forcePictureCapture(stubPicture)
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.LAYOUT_EVENT)
event.layoutEvent.let { layoutEvent ->
layoutEvent.rootOffset.let { rootOffset ->
assertThat(rootOffset.x).isEqualTo(10)
assertThat(rootOffset.y).isEqualTo(20)
}
val root = layoutEvent.rootView
val parent = root.getChildren(0)
val child0 = parent.getChildren(0)
val child1 = parent.getChildren(1)
assertThat(root.id).isEqualTo(floatingDialog.uniqueDrawingId)
root.bounds.layout.let { rect ->
assertThat(rect.x).isEqualTo(80)
assertThat(rect.y).isEqualTo(200)
assertThat(rect.w).isEqualTo(300)
assertThat(rect.h).isEqualTo(200)
}
parent.bounds.layout.let { rect ->
assertThat(rect.x).isEqualTo(100)
assertThat(rect.y).isEqualTo(230)
assertThat(rect.w).isEqualTo(40)
assertThat(rect.h).isEqualTo(50)
}
child0.bounds.layout.let { rect ->
assertThat(rect.x).isEqualTo(135)
assertThat(rect.y).isEqualTo(140)
assertThat(rect.w).isEqualTo(20)
assertThat(rect.h).isEqualTo(30)
}
child1.bounds.render.let { quad ->
assertThat(quad.x0).isEqualTo(10)
assertThat(quad.y0).isEqualTo(20)
assertThat(quad.x1).isEqualTo(30)
assertThat(quad.y1).isEqualTo(40)
assertThat(quad.x2).isEqualTo(50)
assertThat(quad.y2).isEqualTo(60)
assertThat(quad.x3).isEqualTo(70)
assertThat(quad.y3).isEqualTo(80)
}
}
}
}
// TODO: Add test for testing snapshot mode (which will require adding more support for fetching
// view properties in fake-android.
@Test
fun canFetchPropertiesForView() = createViewInspector { viewInspector ->
val responseQueue = ArrayBlockingQueue<ByteArray>(1)
inspectorRule.commandCallback.replyListeners.add { bytes ->
responseQueue.add(bytes)
}
val resourceNames = mutableMapOf<Int, String>()
val resources = Resources(resourceNames)
val context = Context("view.inspector.test", resources)
val root = ViewGroup(context).apply {
setAttachInfo(View.AttachInfo())
addView(View(context))
addView(TextView(context, "Placeholder Text"))
}
WindowManagerGlobal.getInstance().rootViews.addAll(listOf(root))
run { // Search for properties for View
val viewChild = root.getChildAt(0)
val getPropertiesCommand = Command.newBuilder().apply {
getPropertiesCommandBuilder.apply {
rootViewId = root.uniqueDrawingId
viewId = viewChild.uniqueDrawingId
}
}.build()
viewInspector.onReceiveCommand(
getPropertiesCommand.toByteArray(),
inspectorRule.commandCallback
)
responseQueue.take().let { bytes ->
val response = Response.parseFrom(bytes)
assertThat(response.specializedCase).isEqualTo(Response.SpecializedCase.GET_PROPERTIES_RESPONSE)
response.getPropertiesResponse.let { propertiesResponse ->
val strings = StringTable.fromStringEntries(propertiesResponse.stringsList)
val propertyGroup = propertiesResponse.propertyGroup
assertThat(propertyGroup.viewId).isEqualTo(viewChild.uniqueDrawingId)
assertThat(propertyGroup.propertyList.map { strings[it.name] }).containsExactly(
"visibility", "layout_width", "layout_height"
)
}
}
}
run { // Search for properties for TextView
val textChild = root.getChildAt(1)
val getPropertiesCommand = Command.newBuilder().apply {
getPropertiesCommandBuilder.apply {
rootViewId = root.uniqueDrawingId
viewId = textChild.uniqueDrawingId
}
}.build()
viewInspector.onReceiveCommand(
getPropertiesCommand.toByteArray(),
inspectorRule.commandCallback
)
responseQueue.take().let { bytes ->
val response = Response.parseFrom(bytes)
assertThat(response.specializedCase).isEqualTo(Response.SpecializedCase.GET_PROPERTIES_RESPONSE)
response.getPropertiesResponse.let { propertiesResponse ->
val strings = StringTable.fromStringEntries(propertiesResponse.stringsList)
val propertyGroup = propertiesResponse.propertyGroup
assertThat(propertyGroup.viewId).isEqualTo(textChild.uniqueDrawingId)
assertThat(propertyGroup.propertyList.map { strings[it.name] }).containsExactly(
"text", "visibility", "layout_width", "layout_height"
)
}
}
}
}
@Test
fun settingScreenshotTypeAffectsCaptureOutput() = createViewInspector { viewInspector ->
val responseQueue = ArrayBlockingQueue<ByteArray>(1)
inspectorRule.commandCallback.replyListeners.add { bytes ->
responseQueue.add(bytes)
}
val eventQueue = ArrayBlockingQueue<ByteArray>(2)
inspectorRule.connection.eventListeners.add { bytes ->
eventQueue.add(bytes)
}
val fakeBitmapHeader = byteArrayOf(1, 2, 3) // trailed by 0s
val fakePicture1 = Picture(byteArrayOf(2, 1)) // Will be ignored because of BITMAP mode
val fakePicture2 = Picture(byteArrayOf(2, 2))
val resourceNames = mutableMapOf<Int, String>()
val resources = Resources(resourceNames)
val context = Context("view.inspector.test", resources)
val scale = 2
val root = ViewGroup(context).apply {
width = 100
height = 200
setAttachInfo(View.AttachInfo())
}
WindowManagerGlobal.getInstance().rootViews.addAll(listOf(root))
val startFetchCommand = Command.newBuilder().apply {
startFetchCommandBuilder.apply {
continuous = true
}
}.build()
viewInspector.onReceiveCommand(
startFetchCommand.toByteArray(),
inspectorRule.commandCallback
)
responseQueue.take().let { bytes ->
val response = Response.parseFrom(bytes)
assertThat(response.specializedCase).isEqualTo(Response.SpecializedCase.START_FETCH_RESPONSE)
}
ThreadUtils.runOnMainThread { }.get() // Wait for startCommand to finish initializing
assertThat(eventQueue).isEmpty()
run { // Start first by setting type to BITMAP
val updateScreenshotTypeCommand = Command.newBuilder().apply {
updateScreenshotTypeCommandBuilder.apply {
type = Screenshot.Type.BITMAP
this.scale = scale.toFloat()
}
}.build()
viewInspector.onReceiveCommand(
updateScreenshotTypeCommand.toByteArray(),
inspectorRule.commandCallback
)
responseQueue.take().let { bytes ->
val response = Response.parseFrom(bytes)
assertThat(response.specializedCase).isEqualTo(Response.SpecializedCase.UPDATE_SCREENSHOT_TYPE_RESPONSE)
}
root.viewRootImpl = ViewRootImpl()
root.viewRootImpl.mSurface = Surface()
root.viewRootImpl.mSurface.bitmapBytes = fakeBitmapHeader
root.forcePictureCapture(fakePicture1)
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.ROOTS_EVENT)
}
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.LAYOUT_EVENT)
event.layoutEvent.screenshot.let { screenshot ->
assertThat(screenshot.type).isEqualTo(Screenshot.Type.BITMAP)
val decompressedBytes = screenshot.bytes.toByteArray().decompress()
// The full screenshot byte array is width * height, normally all zeroed out,
// so if we just check the first few bytes to make sure they match our header,
// that's enough to know that all the data went through correctly.
assertThat(decompressedBytes.take(fakeBitmapHeader.size)).isEqualTo(
fakeBitmapHeader.asList()
)
}
}
}
run { // Now set type back to SKP
val updateScreenshotTypeCommand = Command.newBuilder().apply {
updateScreenshotTypeCommandBuilder.apply {
type = Screenshot.Type.SKP
}
}.build()
viewInspector.onReceiveCommand(
updateScreenshotTypeCommand.toByteArray(),
inspectorRule.commandCallback
)
responseQueue.take().let { bytes ->
val response = Response.parseFrom(bytes)
assertThat(response.specializedCase).isEqualTo(Response.SpecializedCase.UPDATE_SCREENSHOT_TYPE_RESPONSE)
}
root.forcePictureCapture(fakePicture2)
eventQueue.take().let { bytes ->
val event = Event.parseFrom(bytes)
assertThat(event.specializedCase).isEqualTo(Event.SpecializedCase.LAYOUT_EVENT)
event.layoutEvent.screenshot.let { screenshot ->
assertThat(screenshot.type).isEqualTo(Screenshot.Type.SKP)
assertThat(screenshot.bytes.toByteArray()).isEqualTo(fakePicture2.bytes)
}
}
}
}
// TODO: Add test for filtering system views and properties
private fun createViewInspector(block: (ViewLayoutInspector) -> Unit) {
// We could just create the view inspector directly, but using the factory mimics what
// actually happens in production.
val factory = ViewLayoutInspectorFactory()
val viewInspector =
factory.createInspector(inspectorRule.connection, inspectorRule.environment)
block(viewInspector)
viewInspector.onDispose()
}
}