Loading quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +6 −0 Original line number Diff line number Diff line Loading @@ -120,10 +120,12 @@ import com.android.launcher3.util.ObjectWrapper; import com.android.launcher3.util.PendingRequestArgs; import com.android.launcher3.util.PendingSplitSelectInfo; import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; import com.android.launcher3.util.TouchController; import com.android.launcher3.util.UiThreadHelper; import com.android.launcher3.util.UiThreadHelper.AsyncCommand; import com.android.launcher3.util.ViewCapture; import com.android.launcher3.widget.LauncherAppWidgetHost; import com.android.quickstep.OverviewCommandHelper; import com.android.quickstep.RecentsModel; Loading Loading @@ -195,6 +197,8 @@ public class QuickstepLauncher extends Launcher { */ private PendingSplitSelectInfo mPendingSplitSelectInfo = null; private SafeCloseable mViewCapture; @Override protected void setupViews() { super.setupViews(); Loading Loading @@ -410,6 +414,7 @@ public class QuickstepLauncher extends Launcher { super.onDestroy(); mHotseatPredictionController.destroy(); mViewCapture.close(); } @Override Loading Loading @@ -509,6 +514,7 @@ public class QuickstepLauncher extends Launcher { } addMultiWindowModeChangedListener(mDepthController); initUnfoldTransitionProgressProvider(); mViewCapture = ViewCapture.INSTANCE.get(this).startCapture(getWindow()); } @Override Loading quickstep/src/com/android/quickstep/TouchInteractionService.java +3 −0 Original line number Diff line number Diff line Loading @@ -82,6 +82,7 @@ import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.OnboardingPrefs; import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.ViewCapture; import com.android.launcher3.util.WindowBounds; import com.android.quickstep.inputconsumers.AccessibilityInputConsumer; import com.android.quickstep.inputconsumers.AssistantInputConsumer; Loading Loading @@ -1213,6 +1214,8 @@ public class TouchInteractionService extends Service createdOverviewActivity.getDeviceProfile().dump(this, "", pw); } mTaskbarManager.dumpLogs("", pw); ViewCapture.INSTANCE.get(this).dump(pw, fd); } } Loading src/com/android/launcher3/Launcher.java +0 −19 Original line number Diff line number Diff line Loading @@ -195,7 +195,6 @@ import com.android.launcher3.util.Thunk; import com.android.launcher3.util.TouchController; import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.UiThreadHelper; import com.android.launcher3.util.ViewCapture; import com.android.launcher3.util.ViewOnDrawExecutor; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.FloatingIconView; Loading Loading @@ -393,7 +392,6 @@ public class Launcher extends StatefulActivity<LauncherState> private LauncherState mPrevLauncherState; private StringCache mStringCache; private ViewCapture mViewCapture; @Override @TargetApi(Build.VERSION_CODES.S) Loading Loading @@ -1488,23 +1486,11 @@ public class Launcher extends StatefulActivity<LauncherState> public void onAttachedToWindow() { super.onAttachedToWindow(); mOverlayManager.onAttachedToWindow(); if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) { View root = getDragLayer().getRootView(); if (mViewCapture != null) { mViewCapture.detach(); } mViewCapture = new ViewCapture(getWindow()); mViewCapture.attach(); } } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mViewCapture != null) { mViewCapture.detach(); mViewCapture = null; } mOverlayManager.onDetachedFromWindow(); closeContextMenu(); } Loading Loading @@ -2985,7 +2971,6 @@ public class Launcher extends StatefulActivity<LauncherState> */ @Override public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { SafeCloseable viewDump = mViewCapture == null ? null : mViewCapture.beginDump(writer, fd); super.dump(prefix, fd, writer, args); if (args.length > 0 && TextUtils.equals(args[0], "--all")) { Loading Loading @@ -3026,10 +3011,6 @@ public class Launcher extends StatefulActivity<LauncherState> mPopupDataProvider.dump(prefix, writer); mDeviceProfile.dump(this, prefix, writer); if (viewDump != null) { viewDump.close(); } try { FileLog.flushAll(writer); } catch (Exception e) { Loading src/com/android/launcher3/util/ViewCapture.java +238 −161 Original line number Diff line number Diff line Loading @@ -17,18 +17,25 @@ package com.android.launcher3.util; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.Executors.createAndStartNewLooper; import static java.util.stream.Collectors.toList; import android.content.Context; import android.content.res.Resources; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.Trace; import android.text.TextUtils; import android.util.Base64; import android.util.Base64OutputStream; import android.util.Log; import android.util.Pair; import android.util.SparseArray; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnDrawListener; import android.view.Window; Loading @@ -36,6 +43,7 @@ import android.view.Window; import androidx.annotation.UiThread; import androidx.annotation.WorkerThread; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.view.ViewCaptureData.ExportedData; import com.android.launcher3.view.ViewCaptureData.FrameData; import com.android.launcher3.view.ViewCaptureData.ViewNode; Loading @@ -45,13 +53,14 @@ import java.io.FileOutputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Future; import java.util.zip.GZIPOutputStream; /** * Utility class for capturing view data every frame */ public class ViewCapture implements OnDrawListener { public class ViewCapture { private static final String TAG = "ViewCapture"; Loading @@ -61,43 +70,129 @@ public class ViewCapture implements OnDrawListener { // Launcher. This allows the first free frames avoid object allocation during view capture. private static final int INIT_POOL_SIZE = 300; private final Window mWindow; private final View mRoot; private final Resources mResources; public static final MainThreadInitializedObject<ViewCapture> INSTANCE = new MainThreadInitializedObject<>(ViewCapture::new); private final Handler mHandler; private final ViewRef mViewRef = new ViewRef(); private final List<WindowListener> mListeners = new ArrayList<>(); private int mFrameIndexBg = -1; private final long[] mFrameTimesBg = new long[MEMORY_SIZE]; private final ViewPropertyRef[] mNodesBg = new ViewPropertyRef[MEMORY_SIZE]; private final Context mContext; private final LooperExecutor mExecutor; // Pool used for capturing view tree on the UI thread. private ViewRef mPool = new ViewRef(); private ViewCapture(Context context) { mContext = context; if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) { Looper looper = createAndStartNewLooper("ViewCapture", Process.THREAD_PRIORITY_FOREGROUND); mExecutor = new LooperExecutor(looper); mExecutor.execute(this::initPool); } else { mExecutor = UI_HELPER_EXECUTOR; } } @UiThread private void addToPool(ViewRef start, ViewRef end) { end.next = mPool; mPool = start; } @WorkerThread private void initPool() { ViewRef start = new ViewRef(); ViewRef current = start; for (int i = 0; i < INIT_POOL_SIZE; i++) { current.next = new ViewRef(); current = current.next; } ViewRef finalCurrent = current; MAIN_EXECUTOR.execute(() -> addToPool(start, finalCurrent)); } /** * @param window the window for the capture data * Attaches the ViewCapture to the provided window and returns a handle to detach the listener */ public ViewCapture(Window window) { mWindow = window; mRoot = mWindow.getDecorView(); mResources = mRoot.getResources(); mHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::captureViewPropertiesBg); public SafeCloseable startCapture(Window window) { String title = window.getAttributes().getTitle().toString(); String name = TextUtils.isEmpty(title) ? window.toString() : title; return startCapture(window.getDecorView(), name); } /** * Attaches the ViewCapture to the root * Attaches the ViewCapture to the provided window and returns a handle to detach the listener */ public void attach() { mHandler.post(this::initPool); public SafeCloseable startCapture(View view, String name) { if (!FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) { return () -> { }; } WindowListener listener = new WindowListener(view, name); mExecutor.execute(() -> MAIN_EXECUTOR.execute(listener::attachToRoot)); mListeners.add(listener); return () -> { mListeners.remove(listener); listener.destroy(); }; } /** * Removes a previously attached ViewCapture from the root * Dumps all the active view captures */ public void detach() { mHandler.post(() -> MAIN_EXECUTOR.execute( () -> mRoot.getViewTreeObserver().removeOnDrawListener(this))); public void dump(PrintWriter writer, FileDescriptor out) { if (!FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) { return; } ViewIdProvider idProvider = new ViewIdProvider(mContext.getResources()); // Collect all the tasks first so that all the tasks are posted on the executor List<Pair<String, Future<ExportedData>>> tasks = mListeners.stream() .map(l -> Pair.create(l.name, mExecutor.submit(() -> l.dumpToProto(idProvider)))) .collect(toList()); tasks.forEach(pair -> { writer.println(); writer.println(" ContinuousViewCapture:"); writer.println(" window " + pair.first + ":"); writer.println(" pkg:" + mContext.getPackageName()); writer.print(" data:"); writer.flush(); try (OutputStream os = new FileOutputStream(out)) { ExportedData data = pair.second.get(); OutputStream encodedOS = new GZIPOutputStream(new Base64OutputStream(os, Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP)); data.writeTo(encodedOS); encodedOS.close(); os.flush(); } catch (Exception e) { Log.e(TAG, "Error capturing proto", e); } writer.println(); writer.println("--end--"); }); } private class WindowListener implements OnDrawListener { private final View mRoot; public final String name; private final Handler mHandler; private final ViewRef mViewRef = new ViewRef(); private int mFrameIndexBg = -1; private final long[] mFrameTimesBg = new long[MEMORY_SIZE]; private final ViewPropertyRef[] mNodesBg = new ViewPropertyRef[MEMORY_SIZE]; private boolean mDestroyed = false; WindowListener(View view, String name) { mRoot = view; this.name = name; mHandler = new Handler(mExecutor.getLooper(), this::captureViewPropertiesBg); } @Override Loading Loading @@ -161,76 +256,40 @@ public class ViewCapture implements OnDrawListener { return true; } @UiThread private void addToPool(ViewRef start, ViewRef end) { end.next = mPool; mPool = start; } @WorkerThread private void initPool() { ViewRef start = new ViewRef(); ViewRef current = start; for (int i = 0; i < INIT_POOL_SIZE; i++) { current.next = new ViewRef(); current = current.next; } ViewRef end = current; MAIN_EXECUTOR.execute(() -> { addToPool(start, end); void attachToRoot() { if (mRoot.isAttachedToWindow()) { mRoot.getViewTreeObserver().addOnDrawListener(this); } else { mRoot.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { if (!mDestroyed) { mRoot.getViewTreeObserver().addOnDrawListener(WindowListener.this); } }); mRoot.removeOnAttachStateChangeListener(this); } private String getName() { String title = mWindow.getAttributes().getTitle().toString(); return TextUtils.isEmpty(title) ? mWindow.toString() : title; @Override public void onViewDetachedFromWindow(View v) { } }); } /** * Starts the dump process which is completed on closing the returned object. */ public SafeCloseable beginDump(PrintWriter writer, FileDescriptor out) { Future<ExportedData> task = UI_HELPER_EXECUTOR.submit(this::dumpToProto); return () -> { writer.println(); writer.println(" ContinuousViewCapture:"); writer.println(" window " + getName() + ":"); writer.println(" pkg:" + mRoot.getContext().getPackageName()); writer.print(" data:"); writer.flush(); try (OutputStream os = new FileOutputStream(out)) { ExportedData data = task.get(); OutputStream encodedOS = new GZIPOutputStream(new Base64OutputStream(os, Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP)); data.writeTo(encodedOS); encodedOS.close(); os.flush(); } catch (Exception e) { Log.e(TAG, "Error capturing proto", e); } writer.println(); writer.println("--end--"); }; void destroy() { mRoot.getViewTreeObserver().removeOnDrawListener(this); mDestroyed = true; } @WorkerThread private ExportedData dumpToProto() { private ExportedData dumpToProto(ViewIdProvider idProvider) { ExportedData.Builder dataBuilder = ExportedData.newBuilder(); Resources res = mResources; ArrayList<Class> classList = new ArrayList<>(); int size = (mNodesBg[MEMORY_SIZE - 1] == null) ? mFrameIndexBg + 1 : MEMORY_SIZE; for (int i = size - 1; i >= 0; i--) { int index = (MEMORY_SIZE + mFrameIndexBg - i) % MEMORY_SIZE; ViewNode.Builder nodeBuilder = ViewNode.newBuilder(); mNodesBg[index].toProto(res, classList, nodeBuilder); mNodesBg[index].toProto(idProvider, classList, nodeBuilder); dataBuilder.addFrameData(FrameData.newBuilder() .setNode(nodeBuilder) .setTimestamp(mFrameTimesBg[index])); Loading Loading @@ -264,6 +323,7 @@ public class ViewCapture implements OnDrawListener { return ref; } } } private static class ViewPropertyRef { // We store reference in memory to avoid generating and storing too many strings Loading Loading @@ -318,18 +378,8 @@ public class ViewCapture implements OnDrawListener { * at the end of the iteration. * @return */ public ViewPropertyRef toProto(Resources res, ArrayList<Class> classList, public ViewPropertyRef toProto(ViewIdProvider idProvider, ArrayList<Class> classList, ViewNode.Builder outBuilder) { String resolvedId; if (id >= 0) { try { resolvedId = res.getResourceTypeName(id) + '/' + res.getResourceEntryName(id); } catch (Resources.NotFoundException e) { resolvedId = "id/" + "0x" + Integer.toHexString(id).toUpperCase(); } } else { resolvedId = "NO_ID"; } int classnameIndex = classList.indexOf(clazz); if (classnameIndex < 0) { classnameIndex = classList.size(); Loading @@ -338,7 +388,7 @@ public class ViewCapture implements OnDrawListener { outBuilder .setClassnameIndex(classnameIndex) .setHashcode(hashCode) .setId(resolvedId) .setId(idProvider.getName(id)) .setLeft(left) .setTop(top) .setWidth(right - left) Loading @@ -356,7 +406,7 @@ public class ViewCapture implements OnDrawListener { ViewPropertyRef result = next; for (int i = 0; (i < childCount) && (result != null); i++) { ViewNode.Builder childBuilder = ViewNode.newBuilder(); result = result.toProto(res, classList, childBuilder); result = result.toProto(idProvider, classList, childBuilder); outBuilder.addChildren(childBuilder); } return result; Loading @@ -368,4 +418,31 @@ public class ViewCapture implements OnDrawListener { public int childCount = 0; public ViewRef next; } private static final class ViewIdProvider { private final SparseArray<String> mNames = new SparseArray<>(); private final Resources mRes; ViewIdProvider(Resources res) { mRes = res; } String getName(int id) { String name = mNames.get(id); if (name == null) { if (id >= 0) { try { name = mRes.getResourceTypeName(id) + '/' + mRes.getResourceEntryName(id); } catch (Resources.NotFoundException e) { name = "id/" + "0x" + Integer.toHexString(id).toUpperCase(); } } else { name = "NO_ID"; } mNames.put(id, name); } return name; } } } Loading
quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +6 −0 Original line number Diff line number Diff line Loading @@ -120,10 +120,12 @@ import com.android.launcher3.util.ObjectWrapper; import com.android.launcher3.util.PendingRequestArgs; import com.android.launcher3.util.PendingSplitSelectInfo; import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; import com.android.launcher3.util.TouchController; import com.android.launcher3.util.UiThreadHelper; import com.android.launcher3.util.UiThreadHelper.AsyncCommand; import com.android.launcher3.util.ViewCapture; import com.android.launcher3.widget.LauncherAppWidgetHost; import com.android.quickstep.OverviewCommandHelper; import com.android.quickstep.RecentsModel; Loading Loading @@ -195,6 +197,8 @@ public class QuickstepLauncher extends Launcher { */ private PendingSplitSelectInfo mPendingSplitSelectInfo = null; private SafeCloseable mViewCapture; @Override protected void setupViews() { super.setupViews(); Loading Loading @@ -410,6 +414,7 @@ public class QuickstepLauncher extends Launcher { super.onDestroy(); mHotseatPredictionController.destroy(); mViewCapture.close(); } @Override Loading Loading @@ -509,6 +514,7 @@ public class QuickstepLauncher extends Launcher { } addMultiWindowModeChangedListener(mDepthController); initUnfoldTransitionProgressProvider(); mViewCapture = ViewCapture.INSTANCE.get(this).startCapture(getWindow()); } @Override Loading
quickstep/src/com/android/quickstep/TouchInteractionService.java +3 −0 Original line number Diff line number Diff line Loading @@ -82,6 +82,7 @@ import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.OnboardingPrefs; import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.ViewCapture; import com.android.launcher3.util.WindowBounds; import com.android.quickstep.inputconsumers.AccessibilityInputConsumer; import com.android.quickstep.inputconsumers.AssistantInputConsumer; Loading Loading @@ -1213,6 +1214,8 @@ public class TouchInteractionService extends Service createdOverviewActivity.getDeviceProfile().dump(this, "", pw); } mTaskbarManager.dumpLogs("", pw); ViewCapture.INSTANCE.get(this).dump(pw, fd); } } Loading
src/com/android/launcher3/Launcher.java +0 −19 Original line number Diff line number Diff line Loading @@ -195,7 +195,6 @@ import com.android.launcher3.util.Thunk; import com.android.launcher3.util.TouchController; import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.UiThreadHelper; import com.android.launcher3.util.ViewCapture; import com.android.launcher3.util.ViewOnDrawExecutor; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.FloatingIconView; Loading Loading @@ -393,7 +392,6 @@ public class Launcher extends StatefulActivity<LauncherState> private LauncherState mPrevLauncherState; private StringCache mStringCache; private ViewCapture mViewCapture; @Override @TargetApi(Build.VERSION_CODES.S) Loading Loading @@ -1488,23 +1486,11 @@ public class Launcher extends StatefulActivity<LauncherState> public void onAttachedToWindow() { super.onAttachedToWindow(); mOverlayManager.onAttachedToWindow(); if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) { View root = getDragLayer().getRootView(); if (mViewCapture != null) { mViewCapture.detach(); } mViewCapture = new ViewCapture(getWindow()); mViewCapture.attach(); } } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mViewCapture != null) { mViewCapture.detach(); mViewCapture = null; } mOverlayManager.onDetachedFromWindow(); closeContextMenu(); } Loading Loading @@ -2985,7 +2971,6 @@ public class Launcher extends StatefulActivity<LauncherState> */ @Override public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { SafeCloseable viewDump = mViewCapture == null ? null : mViewCapture.beginDump(writer, fd); super.dump(prefix, fd, writer, args); if (args.length > 0 && TextUtils.equals(args[0], "--all")) { Loading Loading @@ -3026,10 +3011,6 @@ public class Launcher extends StatefulActivity<LauncherState> mPopupDataProvider.dump(prefix, writer); mDeviceProfile.dump(this, prefix, writer); if (viewDump != null) { viewDump.close(); } try { FileLog.flushAll(writer); } catch (Exception e) { Loading
src/com/android/launcher3/util/ViewCapture.java +238 −161 Original line number Diff line number Diff line Loading @@ -17,18 +17,25 @@ package com.android.launcher3.util; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.Executors.createAndStartNewLooper; import static java.util.stream.Collectors.toList; import android.content.Context; import android.content.res.Resources; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.Trace; import android.text.TextUtils; import android.util.Base64; import android.util.Base64OutputStream; import android.util.Log; import android.util.Pair; import android.util.SparseArray; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnDrawListener; import android.view.Window; Loading @@ -36,6 +43,7 @@ import android.view.Window; import androidx.annotation.UiThread; import androidx.annotation.WorkerThread; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.view.ViewCaptureData.ExportedData; import com.android.launcher3.view.ViewCaptureData.FrameData; import com.android.launcher3.view.ViewCaptureData.ViewNode; Loading @@ -45,13 +53,14 @@ import java.io.FileOutputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Future; import java.util.zip.GZIPOutputStream; /** * Utility class for capturing view data every frame */ public class ViewCapture implements OnDrawListener { public class ViewCapture { private static final String TAG = "ViewCapture"; Loading @@ -61,43 +70,129 @@ public class ViewCapture implements OnDrawListener { // Launcher. This allows the first free frames avoid object allocation during view capture. private static final int INIT_POOL_SIZE = 300; private final Window mWindow; private final View mRoot; private final Resources mResources; public static final MainThreadInitializedObject<ViewCapture> INSTANCE = new MainThreadInitializedObject<>(ViewCapture::new); private final Handler mHandler; private final ViewRef mViewRef = new ViewRef(); private final List<WindowListener> mListeners = new ArrayList<>(); private int mFrameIndexBg = -1; private final long[] mFrameTimesBg = new long[MEMORY_SIZE]; private final ViewPropertyRef[] mNodesBg = new ViewPropertyRef[MEMORY_SIZE]; private final Context mContext; private final LooperExecutor mExecutor; // Pool used for capturing view tree on the UI thread. private ViewRef mPool = new ViewRef(); private ViewCapture(Context context) { mContext = context; if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) { Looper looper = createAndStartNewLooper("ViewCapture", Process.THREAD_PRIORITY_FOREGROUND); mExecutor = new LooperExecutor(looper); mExecutor.execute(this::initPool); } else { mExecutor = UI_HELPER_EXECUTOR; } } @UiThread private void addToPool(ViewRef start, ViewRef end) { end.next = mPool; mPool = start; } @WorkerThread private void initPool() { ViewRef start = new ViewRef(); ViewRef current = start; for (int i = 0; i < INIT_POOL_SIZE; i++) { current.next = new ViewRef(); current = current.next; } ViewRef finalCurrent = current; MAIN_EXECUTOR.execute(() -> addToPool(start, finalCurrent)); } /** * @param window the window for the capture data * Attaches the ViewCapture to the provided window and returns a handle to detach the listener */ public ViewCapture(Window window) { mWindow = window; mRoot = mWindow.getDecorView(); mResources = mRoot.getResources(); mHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::captureViewPropertiesBg); public SafeCloseable startCapture(Window window) { String title = window.getAttributes().getTitle().toString(); String name = TextUtils.isEmpty(title) ? window.toString() : title; return startCapture(window.getDecorView(), name); } /** * Attaches the ViewCapture to the root * Attaches the ViewCapture to the provided window and returns a handle to detach the listener */ public void attach() { mHandler.post(this::initPool); public SafeCloseable startCapture(View view, String name) { if (!FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) { return () -> { }; } WindowListener listener = new WindowListener(view, name); mExecutor.execute(() -> MAIN_EXECUTOR.execute(listener::attachToRoot)); mListeners.add(listener); return () -> { mListeners.remove(listener); listener.destroy(); }; } /** * Removes a previously attached ViewCapture from the root * Dumps all the active view captures */ public void detach() { mHandler.post(() -> MAIN_EXECUTOR.execute( () -> mRoot.getViewTreeObserver().removeOnDrawListener(this))); public void dump(PrintWriter writer, FileDescriptor out) { if (!FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) { return; } ViewIdProvider idProvider = new ViewIdProvider(mContext.getResources()); // Collect all the tasks first so that all the tasks are posted on the executor List<Pair<String, Future<ExportedData>>> tasks = mListeners.stream() .map(l -> Pair.create(l.name, mExecutor.submit(() -> l.dumpToProto(idProvider)))) .collect(toList()); tasks.forEach(pair -> { writer.println(); writer.println(" ContinuousViewCapture:"); writer.println(" window " + pair.first + ":"); writer.println(" pkg:" + mContext.getPackageName()); writer.print(" data:"); writer.flush(); try (OutputStream os = new FileOutputStream(out)) { ExportedData data = pair.second.get(); OutputStream encodedOS = new GZIPOutputStream(new Base64OutputStream(os, Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP)); data.writeTo(encodedOS); encodedOS.close(); os.flush(); } catch (Exception e) { Log.e(TAG, "Error capturing proto", e); } writer.println(); writer.println("--end--"); }); } private class WindowListener implements OnDrawListener { private final View mRoot; public final String name; private final Handler mHandler; private final ViewRef mViewRef = new ViewRef(); private int mFrameIndexBg = -1; private final long[] mFrameTimesBg = new long[MEMORY_SIZE]; private final ViewPropertyRef[] mNodesBg = new ViewPropertyRef[MEMORY_SIZE]; private boolean mDestroyed = false; WindowListener(View view, String name) { mRoot = view; this.name = name; mHandler = new Handler(mExecutor.getLooper(), this::captureViewPropertiesBg); } @Override Loading Loading @@ -161,76 +256,40 @@ public class ViewCapture implements OnDrawListener { return true; } @UiThread private void addToPool(ViewRef start, ViewRef end) { end.next = mPool; mPool = start; } @WorkerThread private void initPool() { ViewRef start = new ViewRef(); ViewRef current = start; for (int i = 0; i < INIT_POOL_SIZE; i++) { current.next = new ViewRef(); current = current.next; } ViewRef end = current; MAIN_EXECUTOR.execute(() -> { addToPool(start, end); void attachToRoot() { if (mRoot.isAttachedToWindow()) { mRoot.getViewTreeObserver().addOnDrawListener(this); } else { mRoot.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { if (!mDestroyed) { mRoot.getViewTreeObserver().addOnDrawListener(WindowListener.this); } }); mRoot.removeOnAttachStateChangeListener(this); } private String getName() { String title = mWindow.getAttributes().getTitle().toString(); return TextUtils.isEmpty(title) ? mWindow.toString() : title; @Override public void onViewDetachedFromWindow(View v) { } }); } /** * Starts the dump process which is completed on closing the returned object. */ public SafeCloseable beginDump(PrintWriter writer, FileDescriptor out) { Future<ExportedData> task = UI_HELPER_EXECUTOR.submit(this::dumpToProto); return () -> { writer.println(); writer.println(" ContinuousViewCapture:"); writer.println(" window " + getName() + ":"); writer.println(" pkg:" + mRoot.getContext().getPackageName()); writer.print(" data:"); writer.flush(); try (OutputStream os = new FileOutputStream(out)) { ExportedData data = task.get(); OutputStream encodedOS = new GZIPOutputStream(new Base64OutputStream(os, Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP)); data.writeTo(encodedOS); encodedOS.close(); os.flush(); } catch (Exception e) { Log.e(TAG, "Error capturing proto", e); } writer.println(); writer.println("--end--"); }; void destroy() { mRoot.getViewTreeObserver().removeOnDrawListener(this); mDestroyed = true; } @WorkerThread private ExportedData dumpToProto() { private ExportedData dumpToProto(ViewIdProvider idProvider) { ExportedData.Builder dataBuilder = ExportedData.newBuilder(); Resources res = mResources; ArrayList<Class> classList = new ArrayList<>(); int size = (mNodesBg[MEMORY_SIZE - 1] == null) ? mFrameIndexBg + 1 : MEMORY_SIZE; for (int i = size - 1; i >= 0; i--) { int index = (MEMORY_SIZE + mFrameIndexBg - i) % MEMORY_SIZE; ViewNode.Builder nodeBuilder = ViewNode.newBuilder(); mNodesBg[index].toProto(res, classList, nodeBuilder); mNodesBg[index].toProto(idProvider, classList, nodeBuilder); dataBuilder.addFrameData(FrameData.newBuilder() .setNode(nodeBuilder) .setTimestamp(mFrameTimesBg[index])); Loading Loading @@ -264,6 +323,7 @@ public class ViewCapture implements OnDrawListener { return ref; } } } private static class ViewPropertyRef { // We store reference in memory to avoid generating and storing too many strings Loading Loading @@ -318,18 +378,8 @@ public class ViewCapture implements OnDrawListener { * at the end of the iteration. * @return */ public ViewPropertyRef toProto(Resources res, ArrayList<Class> classList, public ViewPropertyRef toProto(ViewIdProvider idProvider, ArrayList<Class> classList, ViewNode.Builder outBuilder) { String resolvedId; if (id >= 0) { try { resolvedId = res.getResourceTypeName(id) + '/' + res.getResourceEntryName(id); } catch (Resources.NotFoundException e) { resolvedId = "id/" + "0x" + Integer.toHexString(id).toUpperCase(); } } else { resolvedId = "NO_ID"; } int classnameIndex = classList.indexOf(clazz); if (classnameIndex < 0) { classnameIndex = classList.size(); Loading @@ -338,7 +388,7 @@ public class ViewCapture implements OnDrawListener { outBuilder .setClassnameIndex(classnameIndex) .setHashcode(hashCode) .setId(resolvedId) .setId(idProvider.getName(id)) .setLeft(left) .setTop(top) .setWidth(right - left) Loading @@ -356,7 +406,7 @@ public class ViewCapture implements OnDrawListener { ViewPropertyRef result = next; for (int i = 0; (i < childCount) && (result != null); i++) { ViewNode.Builder childBuilder = ViewNode.newBuilder(); result = result.toProto(res, classList, childBuilder); result = result.toProto(idProvider, classList, childBuilder); outBuilder.addChildren(childBuilder); } return result; Loading @@ -368,4 +418,31 @@ public class ViewCapture implements OnDrawListener { public int childCount = 0; public ViewRef next; } private static final class ViewIdProvider { private final SparseArray<String> mNames = new SparseArray<>(); private final Resources mRes; ViewIdProvider(Resources res) { mRes = res; } String getName(int id) { String name = mNames.get(id); if (name == null) { if (id >= 0) { try { name = mRes.getResourceTypeName(id) + '/' + mRes.getResourceEntryName(id); } catch (Resources.NotFoundException e) { name = "id/" + "0x" + Integer.toHexString(id).toUpperCase(); } } else { name = "NO_ID"; } mNames.put(id, name); } return name; } } }