Implement MotionTool Library using ViewCapture library
Bug: 255929005
Test: atest frameworks/libs/systemui/motiontoollib/tests/com/android/app/motiontool
Change-Id: Ie5a847124cfe99831e7ea8f2e9b99eacbe905760
diff --git a/motiontoollib/.gitignore b/motiontoollib/.gitignore
new file mode 100644
index 0000000..6213826
--- /dev/null
+++ b/motiontoollib/.gitignore
@@ -0,0 +1,13 @@
+*.iml
+.project
+.classpath
+.project.properties
+gen/
+bin/
+.idea/
+.gradle/
+local.properties
+gradle/
+build/
+gradlew*
+.DS_Store
diff --git a/motiontoollib/Android.bp b/motiontoollib/Android.bp
new file mode 100644
index 0000000..911be1d
--- /dev/null
+++ b/motiontoollib/Android.bp
@@ -0,0 +1,80 @@
+// Copyright (C) 2022 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "motion_tool_proto",
+ srcs: ["src/com/android/app/motiontool/proto/*.proto"],
+ proto: {
+ type: "nano",
+ local_include_dirs:[
+ "src/com/android/app/motiontool/proto"
+ ],
+ include_dirs: [
+ "frameworks/libs/systemui/viewcapturelib/src/com/android/app/viewcapture/proto"
+ ],
+ },
+ static_libs: [
+ "libprotobuf-java-nano",
+ "view_capture_proto",
+ ],
+ java_version: "1.8",
+}
+
+android_library {
+ name: "motion_tool_lib",
+ manifest: "AndroidManifest.xml",
+ platform_apis: true,
+ min_sdk_version: "26",
+
+ static_libs: [
+ "androidx.core_core",
+ "view_capture",
+ "motion_tool_proto",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt"
+ ],
+}
+
+android_test {
+ name: "motion_tool_lib_tests",
+ manifest: "tests/AndroidManifest.xml",
+ platform_apis: true,
+ min_sdk_version: "26",
+
+ static_libs: [
+ "androidx.core_core",
+ "view_capture",
+ "motion_tool_proto",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "testables"
+ ],
+ srcs: [
+ "**/*.java",
+ "**/*.kt"
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ test_suites: ["device-tests"],
+}
+
diff --git a/motiontoollib/AndroidManifest.xml b/motiontoollib/AndroidManifest.xml
new file mode 100644
index 0000000..3b8a656
--- /dev/null
+++ b/motiontoollib/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2022 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.app.motiontool">
+</manifest>
diff --git a/motiontoollib/TEST_MAPPING b/motiontoollib/TEST_MAPPING
new file mode 100644
index 0000000..22a9e6b
--- /dev/null
+++ b/motiontoollib/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+ "presubmit": [
+ {
+ "name": "motion_tool_lib_tests",
+ "options": [
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ }
+ ]
+}
diff --git a/motiontoollib/build.gradle b/motiontoollib/build.gradle
new file mode 100644
index 0000000..7a75751
--- /dev/null
+++ b/motiontoollib/build.gradle
@@ -0,0 +1,64 @@
+plugins {
+ id 'sysuigradleproject.android-library-conventions'
+ id 'org.jetbrains.kotlin.android'
+ id 'com.google.protobuf'
+}
+
+final String PROTOS_DIR = "${ANDROID_TOP}/frameworks/libs/systemui/motiontoollib/src/com/android/app/motiontool/proto"
+
+android {
+ compileSdk TARGET_SDK.toInteger()
+ buildToolsVersion = BUILD_TOOLS_VERSION
+
+ defaultConfig {
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ defaultConfig {
+ minSdkVersion TARGET_SDK.toInteger()
+ targetSdkVersion TARGET_SDK.toInteger()
+ }
+
+ sourceSets {
+ main {
+ java.srcDirs = ['src']
+ manifest.srcFile 'AndroidManifest.xml'
+ proto.srcDirs = ["${PROTOS_DIR}"]
+ }
+ androidTest {
+ java.srcDirs = ["tests"]
+ manifest.srcFile "tests/AndroidManifest.xml"
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
+
+dependencies {
+ implementation "androidx.core:core:1.9.0"
+ implementation PROTOBUF_DEPENDENCY
+ api project(":ViewCaptureLib")
+ androidTestImplementation project(':SharedTestLib')
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation "androidx.test:rules:1.4.0"
+
+}
+
+protobuf {
+ // Configure the protoc executable
+ protoc {
+ artifact = "com.google.protobuf:protoc:3.0.0${PROTO_ARCH_SUFFIX}"
+ generateProtoTasks {
+ all().each { task ->
+ task.builtins {
+ remove java
+ javanano {
+ option "enum_style=c"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt b/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt
new file mode 100644
index 0000000..f11c848
--- /dev/null
+++ b/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2022 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.app.motiontool
+
+import android.ddm.DdmHandle
+import com.android.app.motiontool.nano.*
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException
+import com.google.protobuf.nano.MessageNano
+import org.apache.harmony.dalvik.ddmc.Chunk
+import org.apache.harmony.dalvik.ddmc.ChunkHandler
+import org.apache.harmony.dalvik.ddmc.DdmServer
+
+/**
+ * This class handles the 'MOTO' type DDM requests (defined in [motion_tool.proto]).
+ *
+ * It executes some validity checks and forwards valid requests to the [MotionToolManager]. It
+ * requires a [MotionToolsRequest] as parameter and returns a [MotionToolsResponse]. Failures will
+ * return a [MotionToolsResponse] with the [error][MotionToolsResponse.error] field set instead of
+ * the respective return value.
+ *
+ * To activate this server, call [register]. This will register the DdmHandleMotionTool with the
+ * [DdmServer]. The DdmHandleMotionTool can be registered once per process. To unregister from the
+ * DdmServer, call [unregister].
+ */
+class DdmHandleMotionTool private constructor(
+ private val motionToolManager: MotionToolManager
+) : DdmHandle() {
+
+ companion object {
+ val CHUNK_MOTO = ChunkHandler.type("MOTO")
+ private const val SERVER_VERSION = 1
+
+ @Volatile
+ private var INSTANCE: DdmHandleMotionTool? = null
+
+ @Synchronized
+ fun getInstance(motionToolManager: MotionToolManager): DdmHandleMotionTool {
+ return INSTANCE ?: DdmHandleMotionTool(motionToolManager).also {
+ INSTANCE = it
+ }
+ }
+ }
+
+ fun register() {
+ DdmServer.registerHandler(CHUNK_MOTO, this)
+ }
+
+ fun unregister() {
+ DdmServer.unregisterHandler(CHUNK_MOTO)
+ }
+
+ override fun handleChunk(request: Chunk): Chunk {
+ val requestDataBuffer = wrapChunk(request)
+ val protoRequest =
+ try {
+ MotionToolsRequest.parseFrom(requestDataBuffer.array())
+ } catch (e: InvalidProtocolBufferNanoException) {
+ val parseErrorResponse =
+ ErrorResponse().apply {
+ code = ErrorResponse.INVALID_REQUEST
+ message = "Invalid request format (Protobuf parse exception)"
+ }
+ val wrappedResponse = MotionToolsResponse().apply { error = parseErrorResponse }
+ val responseData = MessageNano.toByteArray(wrappedResponse)
+ return Chunk(CHUNK_MOTO, responseData, 0, responseData.size)
+ }
+
+ val response =
+ when (protoRequest.typeCase) {
+ MotionToolsRequest.HANDSHAKE_FIELD_NUMBER ->
+ handleHandshakeRequest(protoRequest.handshake)
+ MotionToolsRequest.BEGIN_TRACE_FIELD_NUMBER ->
+ handleBeginTraceRequest(protoRequest.beginTrace)
+ MotionToolsRequest.POLL_TRACE_FIELD_NUMBER ->
+ handlePollTraceRequest(protoRequest.pollTrace)
+ MotionToolsRequest.END_TRACE_FIELD_NUMBER ->
+ handleEndTraceRequest(protoRequest.endTrace)
+ else ->
+ MotionToolsResponse().apply {
+ error =
+ ErrorResponse().apply {
+ code = ErrorResponse.INVALID_REQUEST
+ message = "Unknown request type"
+ }
+ }
+ }
+
+ val responseData = MessageNano.toByteArray(response)
+ return Chunk(CHUNK_MOTO, responseData, 0, responseData.size)
+ }
+
+ private fun handleBeginTraceRequest(beginTraceRequest: BeginTraceRequest) =
+ MotionToolsResponse().apply {
+ tryCatchingMotionToolManagerExceptions {
+ beginTrace =
+ BeginTraceResponse().apply {
+ traceId = motionToolManager.beginTrace(beginTraceRequest.window.rootWindow)
+ }
+ }
+ }
+
+ private fun handlePollTraceRequest(pollTraceRequest: PollTraceRequest) =
+ MotionToolsResponse().apply {
+ tryCatchingMotionToolManagerExceptions {
+ pollTrace =
+ PollTraceResponse().apply {
+ exportedData = motionToolManager.pollTrace(pollTraceRequest.traceId)
+ }
+ }
+ }
+
+ private fun handleEndTraceRequest(endTraceRequest: EndTraceRequest) =
+ MotionToolsResponse().apply {
+ tryCatchingMotionToolManagerExceptions {
+ endTrace =
+ EndTraceResponse().apply {
+ exportedData = motionToolManager.endTrace(endTraceRequest.traceId)
+ }
+ }
+ }
+
+ private fun handleHandshakeRequest(handshakeRequest: HandshakeRequest) =
+ MotionToolsResponse().apply {
+ handshake =
+ HandshakeResponse().apply {
+ serverVersion = SERVER_VERSION
+ status =
+ if (motionToolManager.hasWindow(handshakeRequest.window)) {
+ HandshakeResponse.OK
+ } else {
+ HandshakeResponse.WINDOW_NOT_FOUND
+ }
+ }
+ }
+
+ /**
+ * Executes the [block] and catches all Exceptions thrown by [MotionToolManager]. In case of an
+ * exception being caught, the error response field of the [MotionToolsResponse] is being set
+ * with the according [ErrorResponse].
+ */
+ private fun MotionToolsResponse.tryCatchingMotionToolManagerExceptions(block: () -> Unit) {
+ try {
+ block()
+ } catch (e: UnknownTraceIdException) {
+ error = createUnknownTraceIdResponse(e.traceId)
+ } catch (e: WindowNotFoundException) {
+ error = createWindowNotFoundResponse(e.windowId)
+ }
+ }
+
+ private fun createUnknownTraceIdResponse(traceId: Int) =
+ ErrorResponse().apply {
+ this.code = ErrorResponse.UNKNOWN_TRACE_ID
+ this.message = "No running Trace found with traceId $traceId"
+ }
+
+ private fun createWindowNotFoundResponse(windowId: String) =
+ ErrorResponse().apply {
+ this.code = ErrorResponse.WINDOW_NOT_FOUND
+ this.message = "No window found with windowId $windowId"
+ }
+
+ override fun onConnected() {}
+
+ override fun onDisconnected() {}
+}
diff --git a/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt b/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt
new file mode 100644
index 0000000..392134d
--- /dev/null
+++ b/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2022 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.app.motiontool
+
+import android.util.Log
+import android.view.View
+import android.view.WindowManagerGlobal
+import com.android.app.motiontool.nano.WindowIdentifier
+import com.android.app.viewcapture.ViewCapture
+import com.android.app.viewcapture.data.nano.ExportedData
+
+/**
+ * Singleton to manage motion tracing sessions.
+ *
+ * A motion tracing session captures motion-relevant data on a frame-by-frame basis for a given
+ * window, as long as the trace is running.
+ *
+ * To start a trace, use [beginTrace]. The returned handle must be used to terminate tracing and
+ * receive the data by calling [endTrace]. While the trace is active, data is buffered, however
+ * the buffer size is limited (@see [ViewCapture.MEMORY_SIZE]. Use [pollTrace] periodically to
+ * ensure no data is dropped. Both, [pollTrace] and [endTrace] only return data captured since the
+ * last call to either [beginTrace] or [endTrace].
+ *
+ * NOTE: a running trace will incur some performance penalty. Only keep traces running while a user
+ * requested it.
+ *
+ * @see [DdmHandleMotionTool]
+ */
+class MotionToolManager private constructor(
+ private val viewCapture: ViewCapture,
+ private val windowManagerGlobal: WindowManagerGlobal
+) {
+
+ companion object {
+ private const val TAG = "MotionToolManager"
+
+ @Volatile
+ private var INSTANCE: MotionToolManager? = null
+
+ @Synchronized
+ fun getInstance(
+ viewCapture: ViewCapture,
+ windowManagerGlobal: WindowManagerGlobal
+ ): MotionToolManager {
+ return INSTANCE ?: MotionToolManager(viewCapture, windowManagerGlobal).also {
+ INSTANCE = it
+ }
+ }
+ }
+
+ private var traceIdCounter = 0
+ private val traces = mutableMapOf<Int, TraceMetadata>()
+
+ fun hasWindow(windowId: WindowIdentifier): Boolean {
+ val rootView = getRootView(windowId.rootWindow)
+ return rootView != null
+ }
+
+ /** Starts [ViewCapture] and returns a traceId. */
+ fun beginTrace(windowId: String): Int {
+ val traceId = ++traceIdCounter
+ Log.d(TAG, "Begin Trace for id: $traceId")
+ val rootView = getRootView(windowId) ?: throw WindowNotFoundException(windowId)
+ val autoCloseable = viewCapture.startCapture(rootView, windowId)
+ traces[traceId] = TraceMetadata(windowId, 0, autoCloseable::close)
+ return traceId
+ }
+
+ /**
+ * Ends [ViewCapture] and returns the captured [ExportedData] since the [beginTrace] call or the
+ * last [pollTrace] call.
+ */
+ fun endTrace(traceId: Int): ExportedData {
+ Log.d(TAG, "End Trace for id: $traceId")
+ val traceMetadata = traces.getOrElse(traceId) { throw UnknownTraceIdException(traceId) }
+ val exportedData = pollTrace(traceId)
+ traceMetadata.stopTrace()
+ traces.remove(traceId)
+ return exportedData
+ }
+
+ /**
+ * Returns the [ExportedData] captured since the [beginTrace] call or the last [pollTrace] call.
+ * This function can only be used after [beginTrace] is called and before [endTrace] is called.
+ */
+ fun pollTrace(traceId: Int): ExportedData {
+ val traceMetadata = traces.getOrElse(traceId) { throw UnknownTraceIdException(traceId) }
+ val exportedData = getExportedDataFromViewCapture(traceMetadata)
+ traceMetadata.updateLastPolledTime(exportedData)
+ return exportedData
+ }
+
+ fun reset() {
+ for(traceMetadata in traces.values){
+ traceMetadata.stopTrace()
+ }
+ traces.clear()
+ traceIdCounter = 0
+ }
+
+ private fun getExportedDataFromViewCapture(traceMetadata: TraceMetadata): ExportedData {
+ val rootView =
+ getRootView(traceMetadata.windowId)
+ ?: throw WindowNotFoundException(traceMetadata.windowId)
+ return viewCapture
+ .getDumpTask(rootView)
+ ?.orElse(null)
+ ?.get()
+ ?.apply {
+ frameData = frameData?.filter { it.timestamp > traceMetadata.lastPolledTime }
+ ?.toTypedArray()
+ }
+ ?: ExportedData()
+ }
+
+ private fun getRootView(windowId: String): View? {
+ return windowManagerGlobal.getRootView(windowId)
+ }
+}
+
+private data class TraceMetadata(
+ val windowId: String,
+ var lastPolledTime: Long,
+ var stopTrace: () -> Unit
+) {
+ fun updateLastPolledTime(exportedData: ExportedData?) {
+ exportedData?.frameData?.maxOfOrNull { it.timestamp }?.let { maxFrameTimestamp ->
+ lastPolledTime = maxFrameTimestamp
+ }
+ }
+}
+
+class UnknownTraceIdException(val traceId: Int) : Exception()
+
+class WindowNotFoundException(val windowId: String) : Exception()
diff --git a/motiontoollib/src/com/android/app/motiontool/proto/motion_tool.proto b/motiontoollib/src/com/android/app/motiontool/proto/motion_tool.proto
new file mode 100644
index 0000000..cd5ad2f
--- /dev/null
+++ b/motiontoollib/src/com/android/app/motiontool/proto/motion_tool.proto
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+syntax = "proto2";
+
+package com.android.app.motiontool;
+
+import "view_capture.proto";
+
+option java_multiple_files = true;
+
+message MotionToolsRequest {
+ oneof type {
+ HandshakeRequest handshake = 1;
+ BeginTraceRequest begin_trace = 2;
+ EndTraceRequest end_trace = 3;
+ PollTraceRequest poll_trace = 4;
+ }
+}
+
+// RPC response messages.
+//
+// Returns the result from the corresponding request.
+message MotionToolsResponse {
+ oneof type {
+ // Contains error information whenever the request failed.
+ ErrorResponse error = 1;
+
+ HandshakeResponse handshake = 2;
+ BeginTraceResponse begin_trace = 3;
+ EndTraceResponse end_trace = 4;
+ PollTraceResponse poll_trace = 5;
+ }
+}
+
+message ErrorResponse {
+ enum Code {
+ UNKNOWN = 0;
+ INVALID_REQUEST = 1;
+ UNKNOWN_TRACE_ID = 2;
+ WINDOW_NOT_FOUND = 3;
+ }
+
+ optional Code code = 1;
+ // Human readable error message.
+ optional string message = 2;
+}
+
+// Identifies the window, in which context the motion tools are executed
+message WindowIdentifier {
+ // An identifier for the root view, as accepted by
+ // WindowManagerGlobal#getRootView. This is formatted as
+ // `windowName/rootViewClassName@rootViewIdentityHashCode`,
+ // for example `NotificationShade/android.view.ViewRootImpl@bab6a53`.
+ optional string root_window = 1;
+}
+
+// Verifies the motion tools are available for the specified window.
+message HandshakeRequest {
+ optional WindowIdentifier window = 1;
+ optional int32 client_version = 2;
+}
+
+message HandshakeResponse {
+ enum Status {
+ OK = 1;
+ WINDOW_NOT_FOUND = 2;
+ }
+ optional Status status = 1;
+ optional int32 server_version = 2;
+}
+
+// Enables motion tracing for the specified window
+message BeginTraceRequest {
+ optional WindowIdentifier window = 1;
+}
+
+message BeginTraceResponse {
+ optional int32 trace_id = 1;
+}
+
+// Disabled motion tracing for the specified window
+message EndTraceRequest {
+ optional int32 trace_id = 1;
+}
+
+message EndTraceResponse {
+ optional com.android.app.viewcapture.data.ExportedData exported_data = 1;
+}
+
+// Polls collected motion trace data collected since the last PollTraceRequest (or the
+// BeginTraceRequest)
+message PollTraceRequest {
+ optional int32 trace_id = 1;
+}
+
+message PollTraceResponse {
+ optional com.android.app.viewcapture.data.ExportedData exported_data = 1;
+}
+
diff --git a/motiontoollib/tests/AndroidManifest.xml b/motiontoollib/tests/AndroidManifest.xml
new file mode 100644
index 0000000..3db8d2f
--- /dev/null
+++ b/motiontoollib/tests/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2022 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.app.motiontool">
+
+ <application
+ android:debuggable="true"
+ android:theme="@android:style/Theme.NoTitleBar">
+
+ <activity
+ android:name=".util.TestActivity"
+ android:exported="false" />
+
+ <uses-library android:name="android.test.runner" />
+
+ </application>
+
+ <instrumentation
+ android:name="android.testing.TestableInstrumentation"
+ android:label="Tests for MotionTool Lib"
+ android:targetPackage="com.android.app.motiontool"/>
+
+</manifest>
diff --git a/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt b/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt
new file mode 100644
index 0000000..c3d796d
--- /dev/null
+++ b/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2022 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.app.motiontool
+
+import android.content.Intent
+import android.testing.AndroidTestingRunner
+import android.view.View
+import android.view.WindowManagerGlobal
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.app.motiontool.DdmHandleMotionTool.Companion.CHUNK_MOTO
+import com.android.app.motiontool.nano.BeginTraceRequest
+import com.android.app.motiontool.nano.EndTraceRequest
+import com.android.app.motiontool.nano.ErrorResponse
+import com.android.app.motiontool.nano.HandshakeRequest
+import com.android.app.motiontool.nano.HandshakeResponse
+import com.android.app.motiontool.nano.MotionToolsRequest
+import com.android.app.motiontool.nano.MotionToolsResponse
+import com.android.app.motiontool.nano.PollTraceRequest
+import com.android.app.motiontool.nano.WindowIdentifier
+import com.android.app.motiontool.util.TestActivity
+import com.android.app.viewcapture.ViewCapture
+import com.google.protobuf.nano.MessageNano
+import junit.framework.Assert
+import junit.framework.Assert.assertEquals
+import org.apache.harmony.dalvik.ddmc.Chunk
+import org.apache.harmony.dalvik.ddmc.ChunkHandler.wrapChunk
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DdmHandleMotionToolTest {
+
+ private val viewCaptureMemorySize = 100
+ private val viewCaptureInitPoolSize = 15
+ private val viewCapture =
+ ViewCapture.getInstance(false, viewCaptureMemorySize, viewCaptureInitPoolSize)
+ private val windowManagerGlobal = WindowManagerGlobal.getInstance()
+ private val motionToolManager = MotionToolManager.getInstance(viewCapture, windowManagerGlobal)
+ private val ddmHandleMotionTool = DdmHandleMotionTool.getInstance(motionToolManager)
+ private val CLIENT_VERSION = 1
+
+ private val activityIntent =
+ Intent(InstrumentationRegistry.getInstrumentation().context, TestActivity::class.java)
+
+ @get:Rule
+ val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent)
+
+ @Before
+ fun setup() {
+ ddmHandleMotionTool.register()
+ }
+
+ @After
+ fun cleanup() {
+ ddmHandleMotionTool.unregister()
+ motionToolManager.reset()
+ }
+
+ @Test
+ fun testHandshakeErrorWithInvalidWindowId() {
+ val handshakeResponse = performHandshakeRequest("InvalidWindowId")
+ assertEquals(HandshakeResponse.WINDOW_NOT_FOUND, handshakeResponse.handshake.status)
+ }
+
+ @Test
+ fun testHandshakeOkWithValidWindowId() {
+ val handshakeResponse = performHandshakeRequest(getActivityViewRootId())
+ assertEquals(HandshakeResponse.OK, handshakeResponse.handshake.status)
+ }
+
+ @Test
+ fun testBeginFailsWithInvalidWindowId() {
+ val errorResponse = performBeginTraceRequest("InvalidWindowId")
+ assertEquals(ErrorResponse.WINDOW_NOT_FOUND, errorResponse.error.code)
+ }
+
+ @Test
+ fun testEndTraceFailsWithoutPrecedingBeginTrace() {
+ val errorResponse = performEndTraceRequest(0)
+ assertEquals(ErrorResponse.UNKNOWN_TRACE_ID, errorResponse.error.code)
+ }
+
+ @Test
+ fun testPollTraceFailsWithoutPrecedingBeginTrace() {
+ val errorResponse = performPollTraceRequest(0)
+ assertEquals(ErrorResponse.UNKNOWN_TRACE_ID, errorResponse.error.code)
+ }
+
+ @Test
+ fun testEndTraceFailsWithInvalidTraceId() {
+ val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId())
+ val endTraceResponse = performEndTraceRequest(beginTraceResponse.beginTrace.traceId + 1)
+ assertEquals(ErrorResponse.UNKNOWN_TRACE_ID, endTraceResponse.error.code)
+ }
+
+ @Test
+ fun testPollTraceFailsWithInvalidTraceId() {
+ val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId())
+ val endTraceResponse = performPollTraceRequest(beginTraceResponse.beginTrace.traceId + 1)
+ assertEquals(ErrorResponse.UNKNOWN_TRACE_ID, endTraceResponse.error.code)
+ }
+
+ @Test
+ fun testMalformedRequestFails() {
+ val requestBytes = ByteArray(9)
+ val requestChunk = Chunk(CHUNK_MOTO, requestBytes, 0, requestBytes.size)
+ val responseChunk = ddmHandleMotionTool.handleChunk(requestChunk)
+ val response = MotionToolsResponse.parseFrom(wrapChunk(responseChunk).array()).error
+ assertEquals(ErrorResponse.INVALID_REQUEST, response.code)
+ }
+
+ @Test
+ fun testNoOnDrawCallReturnsEmptyTrace() {
+ activityScenarioRule.scenario.onActivity {
+ val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId())
+ val endTraceResponse = performEndTraceRequest(beginTraceResponse.beginTrace.traceId)
+ Assert.assertTrue(endTraceResponse.endTrace.exportedData.frameData.isEmpty())
+ }
+ }
+
+ @Test
+ fun testOneOnDrawCallReturnsOneFrameResponse() {
+ var traceId = 0
+ activityScenarioRule.scenario.onActivity {
+ val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId())
+ traceId = beginTraceResponse.beginTrace.traceId
+ val rootView = it.findViewById<View>(android.R.id.content)
+ rootView.invalidate()
+ }
+
+ // waits until main looper has no remaining tasks and is idle
+ activityScenarioRule.scenario.onActivity {
+ val pollTraceResponse = performPollTraceRequest(traceId)
+ assertEquals(1, pollTraceResponse.pollTrace.exportedData.frameData.size)
+
+ // Verify that frameData is only included once and is not returned again
+ val endTraceResponse = performEndTraceRequest(traceId)
+ assertEquals(0, endTraceResponse.endTrace.exportedData.frameData.size)
+ }
+
+ }
+
+ private fun performPollTraceRequest(requestTraceId: Int): MotionToolsResponse {
+ val pollTraceRequest = MotionToolsRequest().apply {
+ pollTrace = PollTraceRequest().apply {
+ traceId = requestTraceId
+ }
+ }
+ return performRequest(pollTraceRequest)
+ }
+
+ private fun performEndTraceRequest(requestTraceId: Int): MotionToolsResponse {
+ val endTraceRequest = MotionToolsRequest().apply {
+ endTrace = EndTraceRequest().apply {
+ traceId = requestTraceId
+ }
+ }
+ return performRequest(endTraceRequest)
+ }
+
+ private fun performBeginTraceRequest(windowId: String): MotionToolsResponse {
+ val beginTraceRequest = MotionToolsRequest().apply {
+ beginTrace = BeginTraceRequest().apply {
+ window = WindowIdentifier().apply {
+ rootWindow = windowId
+ }
+ }
+ }
+ return performRequest(beginTraceRequest)
+ }
+
+ private fun performHandshakeRequest(windowId: String): MotionToolsResponse {
+ val handshakeRequest = MotionToolsRequest().apply {
+ handshake = HandshakeRequest().apply {
+ window = WindowIdentifier().apply {
+ rootWindow = windowId
+ }
+ clientVersion = CLIENT_VERSION
+ }
+ }
+ return performRequest(handshakeRequest)
+ }
+
+ private fun performRequest(motionToolsRequest: MotionToolsRequest): MotionToolsResponse {
+ val requestBytes = MessageNano.toByteArray(motionToolsRequest)
+ val requestChunk = Chunk(CHUNK_MOTO, requestBytes, 0, requestBytes.size)
+ val responseChunk = ddmHandleMotionTool.handleChunk(requestChunk)
+ return MotionToolsResponse.parseFrom(wrapChunk(responseChunk).array())
+ }
+
+ private fun getActivityViewRootId() = WindowManagerGlobal.getInstance().viewRootNames.first()
+
+}
diff --git a/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt b/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt
new file mode 100644
index 0000000..0e1450a
--- /dev/null
+++ b/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2022 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.app.motiontool
+
+import android.content.Intent
+import android.testing.AndroidTestingRunner
+import android.view.View
+import android.view.WindowManagerGlobal
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.app.motiontool.util.TestActivity
+import com.android.app.viewcapture.ViewCapture
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+import org.junit.After
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class MotionToolManagerTest {
+
+ private val windowManagerGlobal = WindowManagerGlobal.getInstance()
+ private val viewCaptureMemorySize = 100
+ private val viewCaptureInitPoolSize = 15
+ private val viewCapture =
+ ViewCapture.getInstance(false, viewCaptureMemorySize, viewCaptureInitPoolSize)
+ private val motionToolManager = MotionToolManager.getInstance(viewCapture, windowManagerGlobal)
+
+ private val activityIntent =
+ Intent(InstrumentationRegistry.getInstrumentation().context, TestActivity::class.java)
+
+ @get:Rule
+ val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent)
+
+ @After
+ fun cleanup() {
+ motionToolManager.reset()
+ }
+
+ @Test(expected = UnknownTraceIdException::class)
+ fun testEndTraceThrowsWithoutPrecedingBeginTrace() {
+ motionToolManager.endTrace(0)
+ }
+
+ @Test(expected = UnknownTraceIdException::class)
+ fun testPollTraceThrowsWithoutPrecedingBeginTrace() {
+ motionToolManager.pollTrace(0)
+ }
+
+ @Test(expected = UnknownTraceIdException::class)
+ fun testEndTraceThrowsWithInvalidTraceId() {
+ val traceId = motionToolManager.beginTrace(getActivityViewRootId())
+ motionToolManager.endTrace(traceId + 1)
+ }
+
+ @Test(expected = UnknownTraceIdException::class)
+ fun testPollTraceThrowsWithInvalidTraceId() {
+ val traceId = motionToolManager.beginTrace(getActivityViewRootId())
+ motionToolManager.pollTrace(traceId + 1)
+ }
+
+ @Test(expected = WindowNotFoundException::class)
+ fun testBeginTraceThrowsWithInvalidWindowId() {
+ motionToolManager.beginTrace("InvalidWindowId")
+ }
+
+ @Test
+ fun testNoOnDrawCallReturnsEmptyResponse() {
+ activityScenarioRule.scenario.onActivity {
+ val traceId = motionToolManager.beginTrace(getActivityViewRootId())
+ val result = motionToolManager.endTrace(traceId)
+ assertTrue(result.frameData.isEmpty())
+ }
+ }
+
+ @Test
+ fun testOneOnDrawCallReturnsOneFrameResponse() {
+ var traceId = 0
+ activityScenarioRule.scenario.onActivity {
+ traceId = motionToolManager.beginTrace(getActivityViewRootId())
+ val rootView = it.findViewById<View>(android.R.id.content)
+ rootView.invalidate()
+ }
+
+ // waits until main looper has no remaining tasks and is idle
+ activityScenarioRule.scenario.onActivity {
+ val polledExportedData = motionToolManager.pollTrace(traceId)
+ assertEquals(1, polledExportedData.frameData.size)
+
+ // Verify that frameData is only included once and is not returned again
+ val endExportedData = motionToolManager.endTrace(traceId)
+ assertEquals(0, endExportedData.frameData.size)
+ }
+
+ }
+
+ private fun getActivityViewRootId() = WindowManagerGlobal.getInstance().viewRootNames.first()
+
+}
diff --git a/motiontoollib/tests/com/android/app/motiontool/util/TestActivity.kt b/motiontoollib/tests/com/android/app/motiontool/util/TestActivity.kt
new file mode 100644
index 0000000..a9d68ab
--- /dev/null
+++ b/motiontoollib/tests/com/android/app/motiontool/util/TestActivity.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 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.app.motiontool.util
+
+import android.app.Activity
+
+class TestActivity : Activity()