summaryrefslogtreecommitdiff
path: root/viewcapturelib
diff options
context:
space:
mode:
authorGeorge Zacharia <george.zcharia@gmail.com>2023-07-02 15:00:14 +0530
committerGeorge Zacharia <george.zcharia@gmail.com>2023-07-02 15:00:14 +0530
commite1fe4bad8afa333499590808f3994c8bea0c833b (patch)
tree84db2665b4d6f4e0ae52cecf10eef27facf72cb5 /viewcapturelib
parent2d42f0cd9b97e656643049457e668864063a7051 (diff)
parent313ca2b3cbaef97cb4665b24412eccf3d4cdcae3 (diff)
Merge tag 'android-13.0.0_r52' of https://android.googlesource.com/platform/frameworks/libs/systemui into HEADt13.0
Android 13.0.0 Release 52 (TQ3A.230605.012) Change-Id: I1a59918e5a17f17968121c15476f31567c3d257a
Diffstat (limited to 'viewcapturelib')
-rw-r--r--viewcapturelib/Android.bp4
-rw-r--r--viewcapturelib/AndroidManifest.xml3
-rw-r--r--viewcapturelib/build.gradle26
-rw-r--r--viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java2
-rw-r--r--viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt83
-rw-r--r--viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java171
-rw-r--r--viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt92
-rw-r--r--viewcapturelib/tests/com/android/app/viewcapture/TestActivity.kt45
-rw-r--r--viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt123
9 files changed, 367 insertions, 182 deletions
diff --git a/viewcapturelib/Android.bp b/viewcapturelib/Android.bp
index d5700e5..33da2dd 100644
--- a/viewcapturelib/Android.bp
+++ b/viewcapturelib/Android.bp
@@ -20,12 +20,12 @@ java_library {
name: "view_capture_proto",
srcs: ["src/com/android/app/viewcapture/proto/*.proto"],
proto: {
- type: "nano",
+ type: "lite",
local_include_dirs:[
"src/com/android/app/viewcapture/proto"
],
},
- static_libs: ["libprotobuf-java-nano"],
+ static_libs: ["libprotobuf-java-lite"],
java_version: "1.8",
}
diff --git a/viewcapturelib/AndroidManifest.xml b/viewcapturelib/AndroidManifest.xml
index e5127c6..1da8129 100644
--- a/viewcapturelib/AndroidManifest.xml
+++ b/viewcapturelib/AndroidManifest.xml
@@ -16,5 +16,8 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
package="com.android.app.viewcapture">
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
+ tools:ignore="ProtectedPermissions" />
</manifest>
diff --git a/viewcapturelib/build.gradle b/viewcapturelib/build.gradle
index 7f56819..3f40ad6 100644
--- a/viewcapturelib/build.gradle
+++ b/viewcapturelib/build.gradle
@@ -35,7 +35,7 @@ android {
dependencies {
implementation "androidx.core:core:1.9.0"
- implementation PROTOBUF_DEPENDENCY
+ implementation "com.google.protobuf:protobuf-lite:${protobuf_version}"
androidTestImplementation project(':SharedTestLib')
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation "androidx.test:rules:1.4.0"
@@ -45,15 +45,21 @@ protobuf {
// Configure the protoc executable
protoc {
artifact = "com.google.protobuf:protoc:${protobuf_version}${PROTO_ARCH_SUFFIX}"
- generateProtoTasks {
- all().each { task ->
- task.builtins {
- remove java
- javanano {
- option "enum_style=c"
- }
- }
+ }
+ plugins {
+ javalite {
+ // The codegen for lite comes as a separate artifact
+ artifact = "com.google.protobuf:protoc-gen-javalite:${protobuf_version}${PROTO_ARCH_SUFFIX}"
+ }
+ }
+ generateProtoTasks {
+ all().each { task ->
+ task.builtins {
+ remove java
+ }
+ task.plugins {
+ javalite { }
}
}
}
-}
+} \ No newline at end of file
diff --git a/viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java b/viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java
index 1a18193..e3450f6 100644
--- a/viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java
+++ b/viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java
@@ -28,7 +28,7 @@ import java.util.concurrent.RunnableFuture;
/**
* Implementation of {@link Executor} which executes on a provided looper.
*/
-class LooperExecutor implements Executor {
+public class LooperExecutor implements Executor {
private final Handler mHandler;
diff --git a/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt b/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt
new file mode 100644
index 0000000..c84d4d5
--- /dev/null
+++ b/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 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.database.ContentObserver
+import android.os.Handler
+import android.os.Looper
+import android.os.Process
+import android.provider.Settings
+import android.view.Choreographer
+import androidx.annotation.AnyThread
+import androidx.annotation.VisibleForTesting
+import java.util.concurrent.Executor
+
+/**
+ * ViewCapture that listens to system updates and enables / disables attached ViewCapture
+ * WindowListeners accordingly. The Settings toggle is currently controlled by the Winscope
+ * developer tile in the System developer options.
+ */
+class SettingsAwareViewCapture
+@VisibleForTesting
+internal constructor(private val context: Context, choreographer: Choreographer, executor: Executor)
+ : ViewCapture(DEFAULT_MEMORY_SIZE, DEFAULT_INIT_POOL_SIZE, choreographer, executor) {
+
+ init {
+ enableOrDisableWindowListeners()
+ context.contentResolver.registerContentObserver(
+ Settings.Global.getUriFor(VIEW_CAPTURE_ENABLED),
+ false,
+ object : ContentObserver(Handler()) {
+ override fun onChange(selfChange: Boolean) {
+ enableOrDisableWindowListeners()
+ }
+ })
+ }
+
+ @AnyThread
+ private fun enableOrDisableWindowListeners() {
+ mBgExecutor.execute {
+ val isEnabled = Settings.Global.getInt(context.contentResolver, VIEW_CAPTURE_ENABLED,
+ 0) != 0
+ MAIN_EXECUTOR.execute {
+ enableOrDisableWindowListeners(isEnabled)
+ }
+ }
+ }
+
+ companion object {
+ @VisibleForTesting internal const val VIEW_CAPTURE_ENABLED = "view_capture_enabled"
+
+ private var INSTANCE: ViewCapture? = null
+
+ @JvmStatic
+ fun getInstance(context: Context): ViewCapture = when {
+ INSTANCE != null -> INSTANCE!!
+ Looper.myLooper() == Looper.getMainLooper() -> SettingsAwareViewCapture(
+ context.applicationContext, Choreographer.getInstance(),
+ createAndStartNewLooperExecutor("SAViewCapture",
+ Process.THREAD_PRIORITY_FOREGROUND)).also { INSTANCE = it }
+ else -> try {
+ MAIN_EXECUTOR.submit { getInstance(context) }.get()
+ } catch (e: Exception) {
+ throw e
+ }
+ }
+
+ }
+} \ No newline at end of file
diff --git a/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java b/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java
index 70c58cb..fcd7ad8 100644
--- a/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java
+++ b/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java
@@ -35,18 +35,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
-import android.os.Process;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
-import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
-import com.android.app.viewcapture.data.nano.ExportedData;
-import com.android.app.viewcapture.data.nano.FrameData;
-import com.android.app.viewcapture.data.nano.ViewNode;
-
-import com.google.protobuf.nano.MessageNano;
+import com.android.app.viewcapture.data.ExportedData;
+import com.android.app.viewcapture.data.FrameData;
+import com.android.app.viewcapture.data.ViewNode;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
@@ -54,9 +50,8 @@ import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
import java.util.Optional;
+import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;
import java.util.function.Consumer;
import java.util.zip.GZIPOutputStream;
@@ -64,7 +59,7 @@ import java.util.zip.GZIPOutputStream;
/**
* Utility class for capturing view data every frame
*/
-public class ViewCapture {
+public abstract class ViewCapture {
private static final String TAG = "ViewCapture";
@@ -74,55 +69,31 @@ public class ViewCapture {
// Number of frames to keep in memory
private final int mMemorySize;
- private static final int DEFAULT_MEMORY_SIZE = 2000;
+ protected static final int DEFAULT_MEMORY_SIZE = 2000;
// Initial size of the reference pool. This is at least be 5 * total number of views in
// Launcher. This allows the first free frames avoid object allocation during view capture.
- private static final int DEFAULT_INIT_POOL_SIZE = 300;
+ protected static final int DEFAULT_INIT_POOL_SIZE = 300;
- private static ViewCapture INSTANCE;
public static final LooperExecutor MAIN_EXECUTOR = new LooperExecutor(Looper.getMainLooper());
- public static ViewCapture getInstance() {
- return getInstance(true, DEFAULT_MEMORY_SIZE, DEFAULT_INIT_POOL_SIZE);
- }
-
- @VisibleForTesting
- public static ViewCapture getInstance(boolean offloadToBackgroundThread, int memorySize,
- int initPoolSize) {
- if (INSTANCE == null) {
- if (Looper.myLooper() == Looper.getMainLooper()) {
- INSTANCE = new ViewCapture(offloadToBackgroundThread, memorySize, initPoolSize);
- } else {
- try {
- return MAIN_EXECUTOR.submit(() ->
- getInstance(offloadToBackgroundThread, memorySize, initPoolSize)).get();
- } catch (InterruptedException | ExecutionException e) {
- throw new RuntimeException(e);
- }
- }
- }
- return INSTANCE;
- }
-
private final List<WindowListener> mListeners = new ArrayList<>();
- private final Executor mExecutor;
+ protected final Executor mBgExecutor;
+ private final Choreographer mChoreographer;
// Pool used for capturing view tree on the UI thread.
private ViewRef mPool = new ViewRef();
+ private boolean mIsEnabled = true;
- private ViewCapture(boolean offloadToBackgroundThread, int memorySize, int initPoolSize) {
+ protected ViewCapture(int memorySize, int initPoolSize, Choreographer choreographer,
+ Executor bgExecutor) {
mMemorySize = memorySize;
- if (offloadToBackgroundThread) {
- mExecutor = createAndStartNewLooperExecutor("ViewCapture",
- Process.THREAD_PRIORITY_FOREGROUND);
- } else {
- mExecutor = MAIN_EXECUTOR;
- }
- mExecutor.execute(() -> initPool(initPoolSize));
+ mChoreographer = choreographer;
+ mBgExecutor = bgExecutor;
+ mBgExecutor.execute(() -> initPool(initPoolSize));
}
- private static LooperExecutor createAndStartNewLooperExecutor(String name, int priority) {
+ public static LooperExecutor createAndStartNewLooperExecutor(String name, int priority) {
HandlerThread thread = new HandlerThread(name, priority);
thread.start();
return new LooperExecutor(thread.getLooper());
@@ -158,22 +129,34 @@ public class ViewCapture {
}
/**
- * Attaches the ViewCapture to the provided window and returns a handle to detach the listener
+ * Attaches the ViewCapture to the provided window and returns a handle to detach the listener.
+ * Verifies that ViewCapture is enabled before actually attaching an onDrawListener.
*/
public SafeCloseable startCapture(View view, String name) {
WindowListener listener = new WindowListener(view, name);
- mExecutor.execute(() -> MAIN_EXECUTOR.execute(listener::attachToRoot));
+ if (mIsEnabled) MAIN_EXECUTOR.execute(listener::attachToRoot);
mListeners.add(listener);
return () -> {
mListeners.remove(listener);
- listener.destroy();
+ listener.detachFromRoot();
};
}
+ @UiThread
+ protected void enableOrDisableWindowListeners(boolean isEnabled) {
+ mIsEnabled = isEnabled;
+ mListeners.forEach(WindowListener::detachFromRoot);
+ if (mIsEnabled) mListeners.forEach(WindowListener::attachToRoot);
+ }
+
+
/**
* Dumps all the active view captures
*/
public void dump(PrintWriter writer, FileDescriptor out, Context context) {
+ if (!mIsEnabled) {
+ return;
+ }
ViewIdProvider idProvider = new ViewIdProvider(context.getResources());
// Collect all the tasks first so that all the tasks are posted on the executor
@@ -181,7 +164,7 @@ public class ViewCapture {
.map(l -> {
FutureTask<ExportedData> task =
new FutureTask<ExportedData>(() -> l.dumpToProto(idProvider));
- mExecutor.execute(task);
+ mBgExecutor.execute(task);
return Pair.create(l.name, task);
})
.collect(toList());
@@ -196,7 +179,7 @@ public class ViewCapture {
ExportedData data = pair.second.get();
OutputStream encodedOS = new GZIPOutputStream(new Base64OutputStream(os,
Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP));
- encodedOS.write(MessageNano.toByteArray(data));
+ data.writeTo(encodedOS);
encodedOS.close();
os.flush();
} catch (Exception e) {
@@ -216,7 +199,7 @@ public class ViewCapture {
.map(l -> {
FutureTask<ExportedData> task =
new FutureTask<ExportedData>(() -> l.dumpToProto(idProvider));
- mExecutor.execute(task);
+ mBgExecutor.execute(task);
return task;
})
.findFirst();
@@ -272,16 +255,10 @@ public class ViewCapture {
private final long[] mFrameTimesNanosBg = new long[mMemorySize];
private final ViewPropertyRef[] mNodesBg = new ViewPropertyRef[mMemorySize];
- private boolean mDestroyed = false;
+ private boolean mIsActive = true;
private final Consumer<ViewRef> mCaptureCallback = this::captureViewPropertiesBg;
- private Choreographer mChoreographer;
WindowListener(View view, String name) {
- try {
- mChoreographer = MAIN_EXECUTOR.submit(Choreographer::getInstance).get();
- } catch (InterruptedException | ExecutionException e) {
- throw new RuntimeException(e);
- }
mRoot = view;
this.name = name;
}
@@ -300,7 +277,7 @@ public class ViewCapture {
if (captured != null) {
captured.callback = mCaptureCallback;
captured.choreographerTimeNanos = mChoreographer.getFrameTimeNanos();
- mExecutor.execute(captured);
+ mBgExecutor.execute(captured);
}
mIsFirstFrame = false;
Trace.endSection();
@@ -392,14 +369,15 @@ public class ViewCapture {
}
void attachToRoot() {
+ mIsActive = true;
if (mRoot.isAttachedToWindow()) {
- mRoot.getViewTreeObserver().addOnDrawListener(this);
+ safelyEnableOnDrawListener();
} else {
mRoot.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
- if (!mDestroyed) {
- mRoot.getViewTreeObserver().addOnDrawListener(WindowListener.this);
+ if (mIsActive) {
+ safelyEnableOnDrawListener();
}
mRoot.removeOnAttachStateChangeListener(this);
}
@@ -411,29 +389,34 @@ public class ViewCapture {
}
}
- void destroy() {
+ void detachFromRoot() {
+ mIsActive = false;
mRoot.getViewTreeObserver().removeOnDrawListener(this);
- mDestroyed = true;
+ }
+
+ private void safelyEnableOnDrawListener() {
+ mRoot.getViewTreeObserver().removeOnDrawListener(this);
+ mRoot.getViewTreeObserver().addOnDrawListener(this);
}
@WorkerThread
private ExportedData dumpToProto(ViewIdProvider idProvider) {
int size = (mNodesBg[mMemorySize - 1] == null) ? mFrameIndexBg + 1 : mMemorySize;
- ExportedData exportedData = new ExportedData();
- exportedData.frameData = new FrameData[size];
+ ExportedData.Builder exportedDataBuilder = ExportedData.newBuilder();
ArrayList<Class> classList = new ArrayList<>();
for (int i = size - 1; i >= 0; i--) {
int index = (mMemorySize + mFrameIndexBg - i) % mMemorySize;
- ViewNode node = new ViewNode();
- mNodesBg[index].toProto(idProvider, classList, node);
- FrameData frameData = new FrameData();
- frameData.node = node;
- frameData.timestamp = mFrameTimesNanosBg[index];
- exportedData.frameData[size - i - 1] = frameData;
+ ViewNode.Builder nodeBuilder = ViewNode.newBuilder();
+ mNodesBg[index].toProto(idProvider, classList, nodeBuilder);
+ FrameData.Builder frameDataBuilder = FrameData.newBuilder()
+ .setNode(nodeBuilder)
+ .setTimestamp(mFrameTimesNanosBg[index]);
+ exportedDataBuilder.addFrameData(frameDataBuilder);
}
- exportedData.classname = classList.stream().map(Class::getName).toArray(String[]::new);
- return exportedData;
+ return exportedDataBuilder
+ .addAllClassname(classList.stream().map(Class::getName).collect(toList()))
+ .build();
}
private ViewRef captureViewTree(View view, ViewRef start) {
@@ -518,35 +501,35 @@ public class ViewCapture {
* at the end of the iteration.
*/
public ViewPropertyRef toProto(ViewIdProvider idProvider, ArrayList<Class> classList,
- ViewNode viewNode) {
+ ViewNode.Builder viewNode) {
int classnameIndex = classList.indexOf(clazz);
if (classnameIndex < 0) {
classnameIndex = classList.size();
classList.add(clazz);
}
- viewNode.classnameIndex = classnameIndex;
- viewNode.hashcode = hashCode;
- viewNode.id = idProvider.getName(id);
- viewNode.left = left;
- viewNode.top = top;
- viewNode.width = right - left;
- viewNode.height = bottom - top;
- viewNode.translationX = translateX;
- viewNode.translationY = translateY;
- viewNode.scaleX = scaleX;
- viewNode.scaleY = scaleY;
- viewNode.alpha = alpha;
- viewNode.visibility = visibility;
- viewNode.willNotDraw = willNotDraw;
- viewNode.elevation = elevation;
- viewNode.clipChildren = clipChildren;
+
+ viewNode.setClassnameIndex(classnameIndex)
+ .setHashcode(hashCode)
+ .setId(idProvider.getName(id))
+ .setLeft(left)
+ .setTop(top)
+ .setWidth(right - left)
+ .setHeight(bottom - top)
+ .setTranslationX(translateX)
+ .setTranslationY(translateY)
+ .setScaleX(scaleX)
+ .setScaleY(scaleY)
+ .setAlpha(alpha)
+ .setVisibility(visibility)
+ .setWillNotDraw(willNotDraw)
+ .setElevation(elevation)
+ .setClipChildren(clipChildren);
ViewPropertyRef result = next;
- viewNode.children = new ViewNode[childCount];
for (int i = 0; (i < childCount) && (result != null); i++) {
- ViewNode childViewNode = new ViewNode();
+ ViewNode.Builder childViewNode = ViewNode.newBuilder();
result = result.toProto(idProvider, classList, childViewNode);
- viewNode.children[i] = childViewNode;
+ viewNode.addChildren(childViewNode);
}
return result;
}
diff --git a/viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt b/viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt
new file mode 100644
index 0000000..e08b549
--- /dev/null
+++ b/viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2023 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.content.Intent
+import android.media.permission.SafeCloseable
+import android.provider.Settings
+import android.testing.AndroidTestingRunner
+import android.view.Choreographer
+import android.view.View
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.app.viewcapture.SettingsAwareViewCapture.Companion.VIEW_CAPTURE_ENABLED
+import com.android.app.viewcapture.ViewCapture.MAIN_EXECUTOR
+import junit.framework.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class SettingsAwareViewCaptureTest {
+ private val context: Context = InstrumentationRegistry.getInstrumentation().context
+ private val activityIntent = Intent(context, TestActivity::class.java)
+
+ @get:Rule val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent)
+
+ @Test
+ fun do_not_capture_view_hierarchies_if_setting_is_disabled() {
+ Settings.Global.putInt(context.contentResolver, VIEW_CAPTURE_ENABLED, 0)
+
+ activityScenarioRule.scenario.onActivity { activity ->
+ val viewCapture: ViewCapture =
+ SettingsAwareViewCapture(context, Choreographer.getInstance(), MAIN_EXECUTOR)
+ val rootView: View = activity.findViewById(android.R.id.content)
+
+ val closeable: SafeCloseable = viewCapture.startCapture(rootView, "rootViewId")
+ Choreographer.getInstance().postFrameCallback {
+ rootView.viewTreeObserver.dispatchOnDraw()
+
+ assertEquals(0, viewCapture.getDumpTask(
+ activity.findViewById(android.R.id.content)).get().get().frameDataList.size)
+ closeable.close()
+ }
+ }
+ }
+
+ @Test
+ fun capture_view_hierarchies_if_setting_is_enabled() {
+ Settings.Global.putInt(context.contentResolver, VIEW_CAPTURE_ENABLED, 1)
+
+ activityScenarioRule.scenario.onActivity { activity ->
+ val viewCapture: ViewCapture =
+ SettingsAwareViewCapture(context, Choreographer.getInstance(), MAIN_EXECUTOR)
+ val rootView: View = activity.findViewById(android.R.id.content)
+
+ val closeable: SafeCloseable = viewCapture.startCapture(rootView, "rootViewId")
+ Choreographer.getInstance().postFrameCallback {
+ rootView.viewTreeObserver.dispatchOnDraw()
+
+ assertEquals(1, viewCapture.getDumpTask(activity.findViewById(
+ android.R.id.content)).get().get().frameDataList.size)
+
+ closeable.close()
+ }
+ }
+ }
+
+ @Test
+ fun getInstance_calledTwiceInARow_returnsSameObject() {
+ assertEquals(
+ SettingsAwareViewCapture.getInstance(context).hashCode(),
+ SettingsAwareViewCapture.getInstance(context).hashCode()
+ )
+ }
+}
diff --git a/viewcapturelib/tests/com/android/app/viewcapture/TestActivity.kt b/viewcapturelib/tests/com/android/app/viewcapture/TestActivity.kt
new file mode 100644
index 0000000..749327e
--- /dev/null
+++ b/viewcapturelib/tests/com/android/app/viewcapture/TestActivity.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 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.app.Activity
+import android.os.Bundle
+import android.widget.LinearLayout
+import android.widget.TextView
+
+/**
+ * Activity with the content set to a [LinearLayout] with [TextView] children.
+ */
+class TestActivity : Activity() {
+
+ companion object {
+ const val TEXT_VIEW_COUNT = 1000
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(createContentView())
+ }
+
+ private fun createContentView(): LinearLayout {
+ val root = LinearLayout(this)
+ for (i in 0 until TEXT_VIEW_COUNT) {
+ root.addView(TextView(this))
+ }
+ return root
+ }
+} \ No newline at end of file
diff --git a/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt b/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt
index d390c72..b0fcca1 100644
--- a/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt
+++ b/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt
@@ -16,11 +16,10 @@
package com.android.app.viewcapture
-import android.app.Activity
import android.content.Intent
import android.media.permission.SafeCloseable
-import android.os.Bundle
import android.testing.AndroidTestingRunner
+import android.view.Choreographer
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
@@ -28,7 +27,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.app.viewcapture.TestActivity.Companion.TEXT_VIEW_COUNT
-import com.android.app.viewcapture.data.nano.ExportedData
+import com.android.app.viewcapture.data.ExportedData
import junit.framework.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@@ -38,106 +37,80 @@ import org.junit.runner.RunWith
@RunWith(AndroidTestingRunner::class)
class ViewCaptureTest {
- private val viewCaptureMemorySize = 100
- private val viewCaptureInitPoolSize = 15
- private val viewCapture =
- ViewCapture.getInstance(false, viewCaptureMemorySize, viewCaptureInitPoolSize)
+ private val memorySize = 100
+ private val initPoolSize = 15
+ private val viewCapture by lazy {
+ object :
+ ViewCapture(memorySize, initPoolSize, Choreographer.getInstance(), MAIN_EXECUTOR) {}
+ }
private val activityIntent =
Intent(InstrumentationRegistry.getInstrumentation().context, TestActivity::class.java)
- @get:Rule
- val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent)
-
+ @get:Rule val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent)
@Test
fun testViewCaptureDumpsOneFrameAfterInvalidate() {
- val closeable = startViewCaptureAndInvalidateNTimes(1)
-
- // waits until main looper has no remaining tasks and is idle
- activityScenarioRule.scenario.onActivity {
- val rootView = it.findViewById<View>(android.R.id.content)
- val exportedData = viewCapture.getDumpTask(rootView).get().get()
-
- assertEquals(1, exportedData.frameData.size)
- verifyTestActivityViewHierarchy(exportedData)
+ activityScenarioRule.scenario.onActivity { activity ->
+ Choreographer.getInstance().postFrameCallback {
+ val closeable = startViewCaptureAndInvalidateNTimes(1, activity)
+ val rootView = activity.findViewById<View>(android.R.id.content)
+ val exportedData = viewCapture.getDumpTask(rootView).get().get()
+
+ assertEquals(1, exportedData.frameDataList.size)
+ verifyTestActivityViewHierarchy(exportedData)
+ closeable.close()
+ }
}
- closeable?.close()
}
@Test
fun testViewCaptureDumpsCorrectlyAfterRecyclingStarted() {
- val closeable = startViewCaptureAndInvalidateNTimes(viewCaptureMemorySize + 5)
-
- // waits until main looper has no remaining tasks and is idle
- activityScenarioRule.scenario.onActivity {
- val rootView = it.findViewById<View>(android.R.id.content)
- val exportedData = viewCapture.getDumpTask(rootView).get().get()
-
- // since ViewCapture MEMORY_SIZE is [viewCaptureMemorySize], only
- // [viewCaptureMemorySize] frames are exported, although the view is invalidated
- // [viewCaptureMemorySize + 5] times
- assertEquals(viewCaptureMemorySize, exportedData.frameData.size)
- verifyTestActivityViewHierarchy(exportedData)
+ activityScenarioRule.scenario.onActivity { activity ->
+ Choreographer.getInstance().postFrameCallback {
+ val closeable = startViewCaptureAndInvalidateNTimes(memorySize + 5, activity)
+ val rootView = activity.findViewById<View>(android.R.id.content)
+ val exportedData = viewCapture.getDumpTask(rootView).get().get()
+
+ // since ViewCapture MEMORY_SIZE is [viewCaptureMemorySize], only
+ // [viewCaptureMemorySize] frames are exported, although the view is invalidated
+ // [viewCaptureMemorySize + 5] times
+ assertEquals(memorySize, exportedData.frameDataList.size)
+ verifyTestActivityViewHierarchy(exportedData)
+ closeable.close()
+ }
}
- closeable?.close()
}
- private fun startViewCaptureAndInvalidateNTimes(n: Int): SafeCloseable? {
- var closeable: SafeCloseable? = null
- activityScenarioRule.scenario.onActivity {
- val rootView = it.findViewById<View>(android.R.id.content)
- closeable = viewCapture.startCapture(rootView, "rootViewId")
- invalidateView(rootView, times = n)
- }
+ private fun startViewCaptureAndInvalidateNTimes(n: Int, activity: TestActivity): SafeCloseable {
+ val rootView: View = activity.findViewById(android.R.id.content)
+ val closeable: SafeCloseable = viewCapture.startCapture(rootView, "rootViewId")
+ dispatchOnDraw(rootView, times = n)
return closeable
}
- private fun invalidateView(view: View, times: Int) {
- if (times <= 0) return
- view.post {
- view.invalidate()
- invalidateView(view, times - 1)
+ private fun dispatchOnDraw(view: View, times: Int) {
+ if (times > 0) {
+ view.viewTreeObserver.dispatchOnDraw()
+ dispatchOnDraw(view, times - 1)
}
}
private fun verifyTestActivityViewHierarchy(exportedData: ExportedData) {
- val classnames = exportedData.classname
- for (frame in exportedData.frameData) {
- val root = frame.node // FrameLayout (android.R.id.content)
- val testActivityRoot = root.children.first() // LinearLayout (set by setContentView())
- assertEquals(TEXT_VIEW_COUNT, testActivityRoot.children.size)
+ for (frame in exportedData.frameDataList) {
+ val testActivityRoot =
+ frame.node // FrameLayout (android.R.id.content)
+ .childrenList
+ .first() // LinearLayout (set by setContentView())
+ assertEquals(TEXT_VIEW_COUNT, testActivityRoot.childrenList.size)
assertEquals(
LinearLayout::class.qualifiedName,
- classnames[testActivityRoot.classnameIndex]
+ exportedData.getClassname(testActivityRoot.classnameIndex)
)
assertEquals(
TextView::class.qualifiedName,
- classnames[testActivityRoot.children.first().classnameIndex]
+ exportedData.getClassname(testActivityRoot.childrenList.first().classnameIndex)
)
}
}
}
-
-/**
- * Activity with the content set to a [LinearLayout] with [TextView] children.
- */
-class TestActivity : Activity() {
-
- companion object {
- const val TEXT_VIEW_COUNT = 1000
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(createContentView())
- }
-
- private fun createContentView(): LinearLayout {
- val root = LinearLayout(this)
- for (i in 0 until TEXT_VIEW_COUNT) {
- root.addView(TextView(this))
- }
- return root
- }
-}