src.android.view.ViewDebug Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of android-all Show documentation
Show all versions of android-all Show documentation
A library jar that provides APIs for Applications written for the Google Android Platform.
/*
* Copyright (C) 2007 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 android.view;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.HardwareRenderer;
import android.graphics.Picture;
import android.graphics.RecordingCanvas;
import android.graphics.Rect;
import android.graphics.RenderNode;
import android.os.Build;
import android.os.Debug;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import libcore.util.HexEncoding;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.HashMap;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.stream.Stream;
/**
* Various debugging/tracing tools related to {@link View} and the view hierarchy.
*/
public class ViewDebug {
/**
* @deprecated This flag is now unused
*/
@Deprecated
public static final boolean TRACE_HIERARCHY = false;
/**
* @deprecated This flag is now unused
*/
@Deprecated
public static final boolean TRACE_RECYCLER = false;
/**
* Enables detailed logging of drag/drop operations.
* @hide
*/
public static final boolean DEBUG_DRAG = false;
/**
* Enables detailed logging of task positioning operations.
* @hide
*/
public static final boolean DEBUG_POSITIONING = false;
/**
* This annotation can be used to mark fields and methods to be dumped by
* the view server. Only non-void methods with no arguments can be annotated
* by this annotation.
*/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ExportedProperty {
/**
* When resolveId is true, and if the annotated field/method return value
* is an int, the value is converted to an Android's resource name.
*
* @return true if the property's value must be transformed into an Android
* resource name, false otherwise
*/
boolean resolveId() default false;
/**
* A mapping can be defined to map int values to specific strings. For
* instance, View.getVisibility() returns 0, 4 or 8. However, these values
* actually mean VISIBLE, INVISIBLE and GONE. A mapping can be used to see
* these human readable values:
*
*
* {@literal @}ViewDebug.ExportedProperty(mapping = {
* {@literal @}ViewDebug.IntToString(from = 0, to = "VISIBLE"),
* {@literal @}ViewDebug.IntToString(from = 4, to = "INVISIBLE"),
* {@literal @}ViewDebug.IntToString(from = 8, to = "GONE")
* })
* public int getVisibility() { ...
*
*
* @return An array of int to String mappings
*
* @see android.view.ViewDebug.IntToString
*/
IntToString[] mapping() default { };
/**
* A mapping can be defined to map array indices to specific strings.
* A mapping can be used to see human readable values for the indices
* of an array:
*
*
* {@literal @}ViewDebug.ExportedProperty(indexMapping = {
* {@literal @}ViewDebug.IntToString(from = 0, to = "INVALID"),
* {@literal @}ViewDebug.IntToString(from = 1, to = "FIRST"),
* {@literal @}ViewDebug.IntToString(from = 2, to = "SECOND")
* })
* private int[] mElements;
*
*
* @return An array of int to String mappings
*
* @see android.view.ViewDebug.IntToString
* @see #mapping()
*/
IntToString[] indexMapping() default { };
/**
* A flags mapping can be defined to map flags encoded in an integer to
* specific strings. A mapping can be used to see human readable values
* for the flags of an integer:
*
*
* {@literal @}ViewDebug.ExportedProperty(flagMapping = {
* {@literal @}ViewDebug.FlagToString(mask = ENABLED_MASK, equals = ENABLED,
* name = "ENABLED"),
* {@literal @}ViewDebug.FlagToString(mask = ENABLED_MASK, equals = DISABLED,
* name = "DISABLED"),
* })
* private int mFlags;
*
*
* A specified String is output when the following is true:
*
* @return An array of int to String mappings
*/
FlagToString[] flagMapping() default { };
/**
* When deep export is turned on, this property is not dumped. Instead, the
* properties contained in this property are dumped. Each child property
* is prefixed with the name of this property.
*
* @return true if the properties of this property should be dumped
*
* @see #prefix()
*/
boolean deepExport() default false;
/**
* The prefix to use on child properties when deep export is enabled
*
* @return a prefix as a String
*
* @see #deepExport()
*/
String prefix() default "";
/**
* Specifies the category the property falls into, such as measurement,
* layout, drawing, etc.
*
* @return the category as String
*/
String category() default "";
/**
* Indicates whether or not to format an {@code int} or {@code byte} value as a hex string.
*
* @return true if the supported values should be formatted as a hex string.
*/
boolean formatToHexString() default false;
/**
* Indicates whether or not the key to value mappings are held in adjacent indices.
*
* Note: Applies only to fields and methods that return String[].
*
* @return true if the key to value mappings are held in adjacent indices.
*/
boolean hasAdjacentMapping() default false;
}
/**
* Defines a mapping from an int value to a String. Such a mapping can be used
* in an @ExportedProperty to provide more meaningful values to the end user.
*
* @see android.view.ViewDebug.ExportedProperty
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface IntToString {
/**
* The original int value to map to a String.
*
* @return An arbitrary int value.
*/
int from();
/**
* The String to use in place of the original int value.
*
* @return An arbitrary non-null String.
*/
String to();
}
/**
* Defines a mapping from a flag to a String. Such a mapping can be used
* in an @ExportedProperty to provide more meaningful values to the end user.
*
* @see android.view.ViewDebug.ExportedProperty
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface FlagToString {
/**
* The mask to apply to the original value.
*
* @return An arbitrary int value.
*/
int mask();
/**
* The value to compare to the result of:
* original value & {@link #mask()}
.
*
* @return An arbitrary value.
*/
int equals();
/**
* The String to use in place of the original int value.
*
* @return An arbitrary non-null String.
*/
String name();
/**
* Indicates whether to output the flag when the test is true,
* or false. Defaults to true.
*/
boolean outputIf() default true;
}
/**
* This annotation can be used to mark fields and methods to be dumped when
* the view is captured. Methods with this annotation must have no arguments
* and must return a valid type of data.
*/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface CapturedViewProperty {
/**
* When retrieveReturn is true, we need to retrieve second level methods
* e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod()
* we will set retrieveReturn = true on the annotation of
* myView.getFirstLevelMethod()
* @return true if we need the second level methods
*/
boolean retrieveReturn() default false;
}
/**
* Allows a View to inject custom children into HierarchyViewer. For example,
* WebView uses this to add its internal layer tree as a child to itself
* @hide
*/
public interface HierarchyHandler {
/**
* Dumps custom children to hierarchy viewer.
* See ViewDebug.dumpViewWithProperties(Context, View, BufferedWriter, int)
* for the format
*
* An empty implementation should simply do nothing
*
* @param out The output writer
* @param level The indentation level
*/
public void dumpViewHierarchyWithProperties(BufferedWriter out, int level);
/**
* Returns a View to enable grabbing screenshots from custom children
* returned in dumpViewHierarchyWithProperties.
*
* @param className The className of the view to find
* @param hashCode The hashCode of the view to find
* @return the View to capture from, or null if not found
*/
public View findHierarchyView(String className, int hashCode);
}
private abstract static class PropertyInfo {
public final R member;
public final T property;
public final String name;
public final Class> returnType;
public String entrySuffix = "";
public String valueSuffix = "";
PropertyInfo(Class property, R member, Class> returnType) {
this.member = member;
this.name = member.getName();
this.property = member.getAnnotation(property);
this.returnType = returnType;
}
public abstract Object invoke(Object target) throws Exception;
static PropertyInfo forMethod(Method method,
Class property) {
// Ensure the method return and parameter types can be resolved.
try {
if ((method.getReturnType() == Void.class)
|| (method.getParameterTypes().length != 0)) {
return null;
}
} catch (NoClassDefFoundError e) {
return null;
}
if (!method.isAnnotationPresent(property)) {
return null;
}
method.setAccessible(true);
PropertyInfo info = new MethodPI(method, property);
info.entrySuffix = "()";
info.valueSuffix = ";";
return info;
}
static PropertyInfo forField(Field field, Class property) {
if (!field.isAnnotationPresent(property)) {
return null;
}
field.setAccessible(true);
return new FieldPI<>(field, property);
}
}
private static class MethodPI extends PropertyInfo {
MethodPI(Method method, Class property) {
super(property, method, method.getReturnType());
}
@Override
public Object invoke(Object target) throws Exception {
return member.invoke(target);
}
}
private static class FieldPI extends PropertyInfo {
FieldPI(Field field, Class property) {
super(property, field, field.getType());
}
@Override
public Object invoke(Object target) throws Exception {
return member.get(target);
}
}
// Maximum delay in ms after which we stop trying to capture a View's drawing
private static final int CAPTURE_TIMEOUT = 6000;
private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE";
private static final String REMOTE_COMMAND_DUMP = "DUMP";
private static final String REMOTE_COMMAND_DUMP_THEME = "DUMP_THEME";
/**
* Similar to REMOTE_COMMAND_DUMP but uses ViewHierarchyEncoder instead of flat text
* @hide
*/
public static final String REMOTE_COMMAND_DUMP_ENCODED = "DUMP_ENCODED";
private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE";
private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT";
private static final String REMOTE_PROFILE = "PROFILE";
private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS";
private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST";
private static HashMap, PropertyInfo[]> sExportProperties;
private static HashMap, PropertyInfo[]>
sCapturedViewProperties;
/**
* @deprecated This enum is now unused
*/
@Deprecated
public enum HierarchyTraceType {
INVALIDATE,
INVALIDATE_CHILD,
INVALIDATE_CHILD_IN_PARENT,
REQUEST_LAYOUT,
ON_LAYOUT,
ON_MEASURE,
DRAW,
BUILD_CACHE
}
/**
* @deprecated This enum is now unused
*/
@Deprecated
public enum RecyclerTraceType {
NEW_VIEW,
BIND_VIEW,
RECYCLE_FROM_ACTIVE_HEAP,
RECYCLE_FROM_SCRAP_HEAP,
MOVE_TO_SCRAP_HEAP,
MOVE_FROM_ACTIVE_TO_SCRAP_HEAP
}
/**
* Returns the number of instanciated Views.
*
* @return The number of Views instanciated in the current process.
*
* @hide
*/
@UnsupportedAppUsage
public static long getViewInstanceCount() {
return Debug.countInstancesOfClass(View.class);
}
/**
* Returns the number of instanciated ViewAncestors.
*
* @return The number of ViewAncestors instanciated in the current process.
*
* @hide
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static long getViewRootImplCount() {
return Debug.countInstancesOfClass(ViewRootImpl.class);
}
/**
* @deprecated This method is now unused and invoking it is a no-op
*/
@Deprecated
@SuppressWarnings({ "UnusedParameters", "deprecation" })
public static void trace(View view, RecyclerTraceType type, int... parameters) {
}
/**
* @deprecated This method is now unused and invoking it is a no-op
*/
@Deprecated
@SuppressWarnings("UnusedParameters")
public static void startRecyclerTracing(String prefix, View view) {
}
/**
* @deprecated This method is now unused and invoking it is a no-op
*/
@Deprecated
@SuppressWarnings("UnusedParameters")
public static void stopRecyclerTracing() {
}
/**
* @deprecated This method is now unused and invoking it is a no-op
*/
@Deprecated
@SuppressWarnings({ "UnusedParameters", "deprecation" })
public static void trace(View view, HierarchyTraceType type) {
}
/**
* @deprecated This method is now unused and invoking it is a no-op
*/
@Deprecated
@SuppressWarnings("UnusedParameters")
public static void startHierarchyTracing(String prefix, View view) {
}
/**
* @deprecated This method is now unused and invoking it is a no-op
*/
@Deprecated
public static void stopHierarchyTracing() {
}
@UnsupportedAppUsage
static void dispatchCommand(View view, String command, String parameters,
OutputStream clientStream) throws IOException {
// Just being cautious...
view = view.getRootView();
if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) {
dump(view, false, true, clientStream);
} else if (REMOTE_COMMAND_DUMP_THEME.equalsIgnoreCase(command)) {
dumpTheme(view, clientStream);
} else if (REMOTE_COMMAND_DUMP_ENCODED.equalsIgnoreCase(command)) {
dumpEncoded(view, clientStream);
} else if (REMOTE_COMMAND_CAPTURE_LAYERS.equalsIgnoreCase(command)) {
captureLayers(view, new DataOutputStream(clientStream));
} else {
final String[] params = parameters.split(" ");
if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) {
capture(view, clientStream, params[0]);
} else if (REMOTE_COMMAND_OUTPUT_DISPLAYLIST.equalsIgnoreCase(command)) {
outputDisplayList(view, params[0]);
} else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) {
invalidate(view, params[0]);
} else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) {
requestLayout(view, params[0]);
} else if (REMOTE_PROFILE.equalsIgnoreCase(command)) {
profile(view, clientStream, params[0]);
}
}
}
/** @hide */
public static View findView(View root, String parameter) {
// Look by type/hashcode
if (parameter.indexOf('@') != -1) {
final String[] ids = parameter.split("@");
final String className = ids[0];
final int hashCode = (int) Long.parseLong(ids[1], 16);
View view = root.getRootView();
if (view instanceof ViewGroup) {
return findView((ViewGroup) view, className, hashCode);
}
} else {
// Look by id
final int id = root.getResources().getIdentifier(parameter, null, null);
return root.getRootView().findViewById(id);
}
return null;
}
private static void invalidate(View root, String parameter) {
final View view = findView(root, parameter);
if (view != null) {
view.postInvalidate();
}
}
private static void requestLayout(View root, String parameter) {
final View view = findView(root, parameter);
if (view != null) {
root.post(new Runnable() {
public void run() {
view.requestLayout();
}
});
}
}
private static void profile(View root, OutputStream clientStream, String parameter)
throws IOException {
final View view = findView(root, parameter);
BufferedWriter out = null;
try {
out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
if (view != null) {
profileViewAndChildren(view, out);
} else {
out.write("-1 -1 -1");
out.newLine();
}
out.write("DONE.");
out.newLine();
} catch (Exception e) {
android.util.Log.w("View", "Problem profiling the view:", e);
} finally {
if (out != null) {
out.close();
}
}
}
/** @hide */
public static void profileViewAndChildren(final View view, BufferedWriter out)
throws IOException {
RenderNode node = RenderNode.create("ViewDebug", null);
profileViewAndChildren(view, node, out, true);
}
private static void profileViewAndChildren(View view, RenderNode node, BufferedWriter out,
boolean root) throws IOException {
long durationMeasure =
(root || (view.mPrivateFlags & View.PFLAG_MEASURED_DIMENSION_SET) != 0)
? profileViewMeasure(view) : 0;
long durationLayout =
(root || (view.mPrivateFlags & View.PFLAG_LAYOUT_REQUIRED) != 0)
? profileViewLayout(view) : 0;
long durationDraw =
(root || !view.willNotDraw() || (view.mPrivateFlags & View.PFLAG_DRAWN) != 0)
? profileViewDraw(view, node) : 0;
out.write(String.valueOf(durationMeasure));
out.write(' ');
out.write(String.valueOf(durationLayout));
out.write(' ');
out.write(String.valueOf(durationDraw));
out.newLine();
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
final int count = group.getChildCount();
for (int i = 0; i < count; i++) {
profileViewAndChildren(group.getChildAt(i), node, out, false);
}
}
}
private static long profileViewMeasure(final View view) {
return profileViewOperation(view, new ViewOperation() {
@Override
public void pre() {
forceLayout(view);
}
private void forceLayout(View view) {
view.forceLayout();
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
final int count = group.getChildCount();
for (int i = 0; i < count; i++) {
forceLayout(group.getChildAt(i));
}
}
}
@Override
public void run() {
view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec);
}
});
}
private static long profileViewLayout(View view) {
return profileViewOperation(view,
() -> view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom));
}
private static long profileViewDraw(View view, RenderNode node) {
DisplayMetrics dm = view.getResources().getDisplayMetrics();
if (dm == null) {
return 0;
}
if (view.isHardwareAccelerated()) {
RecordingCanvas canvas = node.beginRecording(dm.widthPixels, dm.heightPixels);
try {
return profileViewOperation(view, () -> view.draw(canvas));
} finally {
node.endRecording();
}
} else {
Bitmap bitmap = Bitmap.createBitmap(
dm, dm.widthPixels, dm.heightPixels, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
try {
return profileViewOperation(view, () -> view.draw(canvas));
} finally {
canvas.setBitmap(null);
bitmap.recycle();
}
}
}
interface ViewOperation {
default void pre() {}
void run();
}
private static long profileViewOperation(View view, final ViewOperation operation) {
final CountDownLatch latch = new CountDownLatch(1);
final long[] duration = new long[1];
view.post(() -> {
try {
operation.pre();
long start = Debug.threadCpuTimeNanos();
//noinspection unchecked
operation.run();
duration[0] = Debug.threadCpuTimeNanos() - start;
} finally {
latch.countDown();
}
});
try {
if (!latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS)) {
Log.w("View", "Could not complete the profiling of the view " + view);
return -1;
}
} catch (InterruptedException e) {
Log.w("View", "Could not complete the profiling of the view " + view);
Thread.currentThread().interrupt();
return -1;
}
return duration[0];
}
/** @hide */
public static void captureLayers(View root, final DataOutputStream clientStream)
throws IOException {
try {
Rect outRect = new Rect();
root.mAttachInfo.mViewRootImpl.getDisplayFrame(outRect);
clientStream.writeInt(outRect.width());
clientStream.writeInt(outRect.height());
captureViewLayer(root, clientStream, true);
clientStream.write(2);
} finally {
clientStream.close();
}
}
private static void captureViewLayer(View view, DataOutputStream clientStream, boolean visible)
throws IOException {
final boolean localVisible = view.getVisibility() == View.VISIBLE && visible;
if ((view.mPrivateFlags & View.PFLAG_SKIP_DRAW) != View.PFLAG_SKIP_DRAW) {
final int id = view.getId();
String name = view.getClass().getSimpleName();
if (id != View.NO_ID) {
name = resolveId(view.getContext(), id).toString();
}
clientStream.write(1);
clientStream.writeUTF(name);
clientStream.writeByte(localVisible ? 1 : 0);
int[] position = new int[2];
// XXX: Should happen on the UI thread
view.getLocationInWindow(position);
clientStream.writeInt(position[0]);
clientStream.writeInt(position[1]);
clientStream.flush();
Bitmap b = performViewCapture(view, true);
if (b != null) {
ByteArrayOutputStream arrayOut = new ByteArrayOutputStream(b.getWidth() *
b.getHeight() * 2);
b.compress(Bitmap.CompressFormat.PNG, 100, arrayOut);
clientStream.writeInt(arrayOut.size());
arrayOut.writeTo(clientStream);
}
clientStream.flush();
}
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
int count = group.getChildCount();
for (int i = 0; i < count; i++) {
captureViewLayer(group.getChildAt(i), clientStream, localVisible);
}
}
if (view.mOverlay != null) {
ViewGroup overlayContainer = view.getOverlay().mOverlayViewGroup;
captureViewLayer(overlayContainer, clientStream, localVisible);
}
}
private static void outputDisplayList(View root, String parameter) throws IOException {
final View view = findView(root, parameter);
view.getViewRootImpl().outputDisplayList(view);
}
/** @hide */
public static void outputDisplayList(View root, View target) {
root.getViewRootImpl().outputDisplayList(target);
}
private static class PictureCallbackHandler implements AutoCloseable,
HardwareRenderer.PictureCapturedCallback, Runnable {
private final HardwareRenderer mRenderer;
private final Function