Loading viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java +6 −1 Original line number Diff line number Diff line Loading @@ -98,6 +98,9 @@ public abstract class ViewCapture { private boolean mIsEnabled = true; @VisibleForTesting public boolean mIsStarted = false; protected ViewCapture(int memorySize, int initPoolSize, Executor bgExecutor) { mMemorySize = memorySize; mBgExecutor = bgExecutor; Loading Loading @@ -128,6 +131,7 @@ public abstract class ViewCapture { @AnyThread @NonNull public SafeCloseable startCapture(@NonNull View view, @NonNull String name) { mIsStarted = true; WindowListener listener = new WindowListener(view, name); if (mIsEnabled) { Loading @@ -136,7 +140,7 @@ public abstract class ViewCapture { mListeners.add(listener); runOnUiThread(() -> view.getContext().registerComponentCallbacks(listener), view); view.getContext().registerComponentCallbacks(listener); return () -> { if (listener.mRoot != null && listener.mRoot.getContext() != null) { Loading @@ -160,6 +164,7 @@ public abstract class ViewCapture { @VisibleForTesting @AnyThread public void stopCapture(@NonNull View rootView) { mIsStarted = false; mListeners.forEach(it -> { if (rootView == it.mRoot) { runOnUiThread(() -> { Loading viewcapturelib/src/com/android/app/viewcapture/ViewCaptureAwareWindowManager.kt +22 −28 Original line number Diff line number Diff line Loading @@ -16,57 +16,51 @@ package com.android.app.viewcapture import android.content.Context import android.media.permission.SafeCloseable import android.os.IBinder import android.view.View import android.view.ViewGroup import android.view.Window import android.view.WindowManager /** Tag for debug logging. */ private const val TAG = "ViewCaptureWindowManager" import android.view.WindowManagerImpl /** * Wrapper class for [WindowManager]. Adds [ViewCapture] to associated window when it is added to * view hierarchy. * [WindowManager] implementation to enable view tracing. Adds [ViewCapture] to associated window * when it is added to view hierarchy. Use [ViewCaptureAwareWindowManagerFactory] to create an * instance of this class. */ class ViewCaptureAwareWindowManager( private val windowManager: WindowManager, private val lazyViewCapture: Lazy<ViewCapture>, private val isViewCaptureEnabled: Boolean, ) : WindowManager by windowManager { internal class ViewCaptureAwareWindowManager( private val context: Context, private val parent: Window? = null, private val windowContextToken: IBinder? = null, ) : WindowManagerImpl(context, parent, windowContextToken) { private var viewCaptureCloseableMap: MutableMap<View, SafeCloseable> = mutableMapOf() override fun addView(view: View, params: ViewGroup.LayoutParams?) { windowManager.addView(view, params) if (isViewCaptureEnabled) { override fun addView(view: View, params: ViewGroup.LayoutParams) { super.addView(view, params) val viewCaptureCloseable: SafeCloseable = lazyViewCapture.value.startCapture(view, getViewName(view)) ViewCaptureFactory.getInstance(context).startCapture(view, getViewName(view)) viewCaptureCloseableMap[view] = viewCaptureCloseable } } override fun removeView(view: View?) { removeViewFromCloseableMap(view) windowManager.removeView(view) super.removeView(view) } override fun removeViewImmediate(view: View?) { removeViewFromCloseableMap(view) windowManager.removeViewImmediate(view) super.removeViewImmediate(view) } private fun getViewName(view: View) = "." + view.javaClass.name private fun removeViewFromCloseableMap(view: View?) { if (isViewCaptureEnabled) { if (viewCaptureCloseableMap.containsKey(view)) { viewCaptureCloseableMap[view]?.close() viewCaptureCloseableMap.remove(view) } } } interface Factory { fun create(windowManager: WindowManager): ViewCaptureAwareWindowManager } } viewcapturelib/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerFactory.kt 0 → 100644 +63 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.viewcapture import android.content.Context import android.os.IBinder import android.os.Trace import android.os.Trace.TRACE_TAG_APP import android.view.Window import android.view.WindowManager import java.lang.ref.WeakReference import java.util.Collections import java.util.WeakHashMap /** Factory to create [Context] specific instances of [ViewCaptureAwareWindowManager]. */ object ViewCaptureAwareWindowManagerFactory { /** * Keeps track of [ViewCaptureAwareWindowManager] instance for a [Context]. It is a * [WeakHashMap] to ensure that if a [Context] mapped in the [instanceMap] is destroyed, the map * entry is garbage collected as well. */ private val instanceMap = Collections.synchronizedMap(WeakHashMap<Context, WeakReference<WindowManager>>()) /** * Returns the weakly cached [ViewCaptureAwareWindowManager] instance for a given [Context]. If * no instance is cached; it creates, caches and returns a new instance. */ @JvmStatic fun getInstance( context: Context, parent: Window? = null, windowContextToken: IBinder? = null, ): WindowManager { Trace.traceCounter(TRACE_TAG_APP, "ViewCaptureAwareWindowManagerFactory#instanceMap.size", instanceMap.size) val cachedWindowManager = instanceMap[context]?.get() if (cachedWindowManager != null) { return cachedWindowManager } else { val windowManager = ViewCaptureAwareWindowManager(context, parent, windowContextToken) instanceMap[context] = WeakReference(windowManager) return windowManager } } } viewcapturelib/src/com/android/app/viewcapture/ViewCaptureFactory.kt +0 −16 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ import android.content.Context import android.os.Process import android.tracing.Flags import android.util.Log import android.view.WindowManager /** * Factory to create polymorphic instances of ViewCapture according to build configurations and Loading Loading @@ -68,19 +67,4 @@ object ViewCaptureFactory { } return instance } /** Returns an instance of [ViewCaptureAwareWindowManager]. */ @JvmStatic fun getViewCaptureAwareWindowManagerInstance( context: Context, isViewCaptureTracingEnabled: Boolean, ): ViewCaptureAwareWindowManager { val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager val lazyViewCapture = lazy { getInstance(context) } return ViewCaptureAwareWindowManager( windowManager, lazyViewCapture, isViewCaptureTracingEnabled, ) } } viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureAwareWindowManagerTest.kt +23 −50 Original line number Diff line number Diff line Loading @@ -17,69 +17,42 @@ package com.android.app.viewcapture import android.content.Context import android.content.Intent import android.testing.AndroidTestingRunner import android.view.View import android.view.WindowManager import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.filters.SmallTest import org.junit.Before import androidx.test.platform.app.InstrumentationRegistry import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito.doAnswer import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.invocation.InvocationOnMock import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock @RunWith(AndroidTestingRunner::class) @SmallTest class ViewCaptureAwareWindowManagerTest { private val context: Context = ApplicationProvider.getApplicationContext() private val mockRootView = mock<View>() private val windowManager = mock<WindowManager>() private val viewCaptureSpy = spy(ViewCaptureFactory.getInstance(context)) private val lazyViewCapture = mock<Lazy<ViewCapture>> { on { value } doReturn viewCaptureSpy } private var mViewCaptureAwareWindowManager: ViewCaptureAwareWindowManager? = null private val mContext: Context = InstrumentationRegistry.getInstrumentation().context private lateinit var mViewCaptureAwareWindowManager: ViewCaptureAwareWindowManager @Before fun setUp() { doAnswer { invocation: InvocationOnMock -> val view = invocation.getArgument<View>(0) val lp = invocation.getArgument<WindowManager.LayoutParams>(1) view.layoutParams = lp null } .`when`(windowManager) .addView(any(View::class.java), any(WindowManager.LayoutParams::class.java)) `when`(mockRootView.context).thenReturn(context) } private val activityIntent = Intent(mContext, TestActivity::class.java) @Test fun testAddView_viewCaptureEnabled_verifyStartCaptureCall() { mViewCaptureAwareWindowManager = ViewCaptureAwareWindowManager( windowManager, lazyViewCapture, isViewCaptureEnabled = true ) mViewCaptureAwareWindowManager?.addView(mockRootView, mockRootView.layoutParams) verify(viewCaptureSpy).startCapture(any(), anyString()) } @get:Rule val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent) @Test fun testAddView_viewCaptureNotEnabled_verifyStartCaptureCall() { mViewCaptureAwareWindowManager = ViewCaptureAwareWindowManager( windowManager, lazyViewCapture, isViewCaptureEnabled = false fun testAddView_verifyStartCaptureCall() { activityScenarioRule.scenario.onActivity { activity -> mViewCaptureAwareWindowManager = ViewCaptureAwareWindowManager(mContext) val activityDecorView = activity.window.decorView // removing view since it is already added to view hierarchy on declaration mViewCaptureAwareWindowManager.removeView(activityDecorView) val viewCapture = ViewCaptureFactory.getInstance(mContext) mViewCaptureAwareWindowManager.addView( activityDecorView, activityDecorView.layoutParams as WindowManager.LayoutParams, ) mViewCaptureAwareWindowManager?.addView(mockRootView, mockRootView.layoutParams) verify(viewCaptureSpy, times(0)).startCapture(any(), anyString()) assertTrue(viewCapture.mIsStarted) } } } Loading
viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java +6 −1 Original line number Diff line number Diff line Loading @@ -98,6 +98,9 @@ public abstract class ViewCapture { private boolean mIsEnabled = true; @VisibleForTesting public boolean mIsStarted = false; protected ViewCapture(int memorySize, int initPoolSize, Executor bgExecutor) { mMemorySize = memorySize; mBgExecutor = bgExecutor; Loading Loading @@ -128,6 +131,7 @@ public abstract class ViewCapture { @AnyThread @NonNull public SafeCloseable startCapture(@NonNull View view, @NonNull String name) { mIsStarted = true; WindowListener listener = new WindowListener(view, name); if (mIsEnabled) { Loading @@ -136,7 +140,7 @@ public abstract class ViewCapture { mListeners.add(listener); runOnUiThread(() -> view.getContext().registerComponentCallbacks(listener), view); view.getContext().registerComponentCallbacks(listener); return () -> { if (listener.mRoot != null && listener.mRoot.getContext() != null) { Loading @@ -160,6 +164,7 @@ public abstract class ViewCapture { @VisibleForTesting @AnyThread public void stopCapture(@NonNull View rootView) { mIsStarted = false; mListeners.forEach(it -> { if (rootView == it.mRoot) { runOnUiThread(() -> { Loading
viewcapturelib/src/com/android/app/viewcapture/ViewCaptureAwareWindowManager.kt +22 −28 Original line number Diff line number Diff line Loading @@ -16,57 +16,51 @@ package com.android.app.viewcapture import android.content.Context import android.media.permission.SafeCloseable import android.os.IBinder import android.view.View import android.view.ViewGroup import android.view.Window import android.view.WindowManager /** Tag for debug logging. */ private const val TAG = "ViewCaptureWindowManager" import android.view.WindowManagerImpl /** * Wrapper class for [WindowManager]. Adds [ViewCapture] to associated window when it is added to * view hierarchy. * [WindowManager] implementation to enable view tracing. Adds [ViewCapture] to associated window * when it is added to view hierarchy. Use [ViewCaptureAwareWindowManagerFactory] to create an * instance of this class. */ class ViewCaptureAwareWindowManager( private val windowManager: WindowManager, private val lazyViewCapture: Lazy<ViewCapture>, private val isViewCaptureEnabled: Boolean, ) : WindowManager by windowManager { internal class ViewCaptureAwareWindowManager( private val context: Context, private val parent: Window? = null, private val windowContextToken: IBinder? = null, ) : WindowManagerImpl(context, parent, windowContextToken) { private var viewCaptureCloseableMap: MutableMap<View, SafeCloseable> = mutableMapOf() override fun addView(view: View, params: ViewGroup.LayoutParams?) { windowManager.addView(view, params) if (isViewCaptureEnabled) { override fun addView(view: View, params: ViewGroup.LayoutParams) { super.addView(view, params) val viewCaptureCloseable: SafeCloseable = lazyViewCapture.value.startCapture(view, getViewName(view)) ViewCaptureFactory.getInstance(context).startCapture(view, getViewName(view)) viewCaptureCloseableMap[view] = viewCaptureCloseable } } override fun removeView(view: View?) { removeViewFromCloseableMap(view) windowManager.removeView(view) super.removeView(view) } override fun removeViewImmediate(view: View?) { removeViewFromCloseableMap(view) windowManager.removeViewImmediate(view) super.removeViewImmediate(view) } private fun getViewName(view: View) = "." + view.javaClass.name private fun removeViewFromCloseableMap(view: View?) { if (isViewCaptureEnabled) { if (viewCaptureCloseableMap.containsKey(view)) { viewCaptureCloseableMap[view]?.close() viewCaptureCloseableMap.remove(view) } } } interface Factory { fun create(windowManager: WindowManager): ViewCaptureAwareWindowManager } }
viewcapturelib/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerFactory.kt 0 → 100644 +63 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.viewcapture import android.content.Context import android.os.IBinder import android.os.Trace import android.os.Trace.TRACE_TAG_APP import android.view.Window import android.view.WindowManager import java.lang.ref.WeakReference import java.util.Collections import java.util.WeakHashMap /** Factory to create [Context] specific instances of [ViewCaptureAwareWindowManager]. */ object ViewCaptureAwareWindowManagerFactory { /** * Keeps track of [ViewCaptureAwareWindowManager] instance for a [Context]. It is a * [WeakHashMap] to ensure that if a [Context] mapped in the [instanceMap] is destroyed, the map * entry is garbage collected as well. */ private val instanceMap = Collections.synchronizedMap(WeakHashMap<Context, WeakReference<WindowManager>>()) /** * Returns the weakly cached [ViewCaptureAwareWindowManager] instance for a given [Context]. If * no instance is cached; it creates, caches and returns a new instance. */ @JvmStatic fun getInstance( context: Context, parent: Window? = null, windowContextToken: IBinder? = null, ): WindowManager { Trace.traceCounter(TRACE_TAG_APP, "ViewCaptureAwareWindowManagerFactory#instanceMap.size", instanceMap.size) val cachedWindowManager = instanceMap[context]?.get() if (cachedWindowManager != null) { return cachedWindowManager } else { val windowManager = ViewCaptureAwareWindowManager(context, parent, windowContextToken) instanceMap[context] = WeakReference(windowManager) return windowManager } } }
viewcapturelib/src/com/android/app/viewcapture/ViewCaptureFactory.kt +0 −16 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ import android.content.Context import android.os.Process import android.tracing.Flags import android.util.Log import android.view.WindowManager /** * Factory to create polymorphic instances of ViewCapture according to build configurations and Loading Loading @@ -68,19 +67,4 @@ object ViewCaptureFactory { } return instance } /** Returns an instance of [ViewCaptureAwareWindowManager]. */ @JvmStatic fun getViewCaptureAwareWindowManagerInstance( context: Context, isViewCaptureTracingEnabled: Boolean, ): ViewCaptureAwareWindowManager { val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager val lazyViewCapture = lazy { getInstance(context) } return ViewCaptureAwareWindowManager( windowManager, lazyViewCapture, isViewCaptureTracingEnabled, ) } }
viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureAwareWindowManagerTest.kt +23 −50 Original line number Diff line number Diff line Loading @@ -17,69 +17,42 @@ package com.android.app.viewcapture import android.content.Context import android.content.Intent import android.testing.AndroidTestingRunner import android.view.View import android.view.WindowManager import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.filters.SmallTest import org.junit.Before import androidx.test.platform.app.InstrumentationRegistry import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito.doAnswer import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.invocation.InvocationOnMock import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock @RunWith(AndroidTestingRunner::class) @SmallTest class ViewCaptureAwareWindowManagerTest { private val context: Context = ApplicationProvider.getApplicationContext() private val mockRootView = mock<View>() private val windowManager = mock<WindowManager>() private val viewCaptureSpy = spy(ViewCaptureFactory.getInstance(context)) private val lazyViewCapture = mock<Lazy<ViewCapture>> { on { value } doReturn viewCaptureSpy } private var mViewCaptureAwareWindowManager: ViewCaptureAwareWindowManager? = null private val mContext: Context = InstrumentationRegistry.getInstrumentation().context private lateinit var mViewCaptureAwareWindowManager: ViewCaptureAwareWindowManager @Before fun setUp() { doAnswer { invocation: InvocationOnMock -> val view = invocation.getArgument<View>(0) val lp = invocation.getArgument<WindowManager.LayoutParams>(1) view.layoutParams = lp null } .`when`(windowManager) .addView(any(View::class.java), any(WindowManager.LayoutParams::class.java)) `when`(mockRootView.context).thenReturn(context) } private val activityIntent = Intent(mContext, TestActivity::class.java) @Test fun testAddView_viewCaptureEnabled_verifyStartCaptureCall() { mViewCaptureAwareWindowManager = ViewCaptureAwareWindowManager( windowManager, lazyViewCapture, isViewCaptureEnabled = true ) mViewCaptureAwareWindowManager?.addView(mockRootView, mockRootView.layoutParams) verify(viewCaptureSpy).startCapture(any(), anyString()) } @get:Rule val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent) @Test fun testAddView_viewCaptureNotEnabled_verifyStartCaptureCall() { mViewCaptureAwareWindowManager = ViewCaptureAwareWindowManager( windowManager, lazyViewCapture, isViewCaptureEnabled = false fun testAddView_verifyStartCaptureCall() { activityScenarioRule.scenario.onActivity { activity -> mViewCaptureAwareWindowManager = ViewCaptureAwareWindowManager(mContext) val activityDecorView = activity.window.decorView // removing view since it is already added to view hierarchy on declaration mViewCaptureAwareWindowManager.removeView(activityDecorView) val viewCapture = ViewCaptureFactory.getInstance(mContext) mViewCaptureAwareWindowManager.addView( activityDecorView, activityDecorView.layoutParams as WindowManager.LayoutParams, ) mViewCaptureAwareWindowManager?.addView(mockRootView, mockRootView.layoutParams) verify(viewCaptureSpy, times(0)).startCapture(any(), anyString()) assertTrue(viewCapture.mIsStarted) } } }