/* * 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.os.Process import android.util.Log import android.view.Choreographer import android.view.View import android.view.WindowManagerGlobal import androidx.annotation.VisibleForTesting import com.android.app.viewcapture.SimpleViewCapture import com.android.app.viewcapture.ViewCapture import com.android.app.viewcapture.data.MotionWindowData /** * 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.mMemorySize]. 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 windowManagerGlobal: WindowManagerGlobal) { private val viewCapture: ViewCapture = SimpleViewCapture("MTViewCapture") companion object { private const val TAG = "MotionToolManager" private var INSTANCE: MotionToolManager? = null @Synchronized fun getInstance(windowManagerGlobal: WindowManagerGlobal): MotionToolManager { return INSTANCE ?: MotionToolManager(windowManagerGlobal).also { INSTANCE = it } } } private var traceIdCounter = 0 private val traces = mutableMapOf() @Synchronized fun hasWindow(windowId: WindowIdentifier): Boolean { val rootView = getRootView(windowId.rootWindow) return rootView != null } /** Starts [ViewCapture] and returns a traceId. */ @Synchronized 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 [MotionWindowData] since the [beginTrace] call or * the last [pollTrace] call. */ @Synchronized fun endTrace(traceId: Int): MotionWindowData { Log.d(TAG, "End Trace for id: $traceId") val traceMetadata = traces.getOrElse(traceId) { throw UnknownTraceIdException(traceId) } val data = pollTrace(traceId) traceMetadata.stopTrace() traces.remove(traceId) return data } /** * Returns the [MotionWindowData] captured since the [beginTrace] call or last [pollTrace] call. * This function can only be used after [beginTrace] is called and before [endTrace] is called. */ @Synchronized fun pollTrace(traceId: Int): MotionWindowData { val traceMetadata = traces.getOrElse(traceId) { throw UnknownTraceIdException(traceId) } val data = getDataFromViewCapture(traceMetadata) traceMetadata.updateLastPolledTime(data) return data } /** * Stops and deletes all active [traces] and resets the [traceIdCounter]. */ @VisibleForTesting @Synchronized fun reset() { for (traceMetadata in traces.values) { traceMetadata.stopTrace() } traces.clear() traceIdCounter = 0 } private fun getDataFromViewCapture(traceMetadata: TraceMetadata): MotionWindowData { val rootView = getRootView(traceMetadata.windowId) ?: throw WindowNotFoundException(traceMetadata.windowId) val data: MotionWindowData = viewCapture .getDumpTask(rootView).get() ?.orElse(null) ?: return MotionWindowData.newBuilder().build() val filteredFrameData = data.frameDataList.filter { it.timestamp > traceMetadata.lastPolledTime } return data.toBuilder() .clearFrameData() .addAllFrameData(filteredFrameData) .build() } 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(data: MotionWindowData?) { data?.frameDataList?.maxOfOrNull { it.timestamp }?.let { lastPolledTime = it } } } class UnknownTraceIdException(val traceId: Int) : Exception() class WindowNotFoundException(val windowId: String) : Exception()