2186 lines
80 KiB
Java
2186 lines
80 KiB
Java
![]() |
/*
|
||
|
* 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, softwareViewDebug
|
||
|
* 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 static com.android.internal.util.Preconditions.checkArgument;
|
||
|
|
||
|
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.Base64;
|
||
|
import android.util.DisplayMetrics;
|
||
|
import android.util.Log;
|
||
|
import android.util.TypedValue;
|
||
|
|
||
|
import com.android.internal.annotations.VisibleForTesting;
|
||
|
|
||
|
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.Member;
|
||
|
import java.lang.reflect.Method;
|
||
|
import java.nio.BufferUnderflowException;
|
||
|
import java.nio.ByteBuffer;
|
||
|
import java.nio.charset.StandardCharsets;
|
||
|
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.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 {
|
||
|
|
||
|
private static final String TAG = "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:
|
||
|
*
|
||
|
* <pre>
|
||
|
* {@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() { ...
|
||
|
* <pre>
|
||
|
*
|
||
|
* @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:
|
||
|
*
|
||
|
* <pre>
|
||
|
* {@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;
|
||
|
* <pre>
|
||
|
*
|
||
|
* @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:
|
||
|
*
|
||
|
* <pre>
|
||
|
* {@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;
|
||
|
* <pre>
|
||
|
*
|
||
|
* 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:
|
||
|
* <code>original value & {@link #mask()}</code>.
|
||
|
*
|
||
|
* @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<T extends Annotation,
|
||
|
R extends AccessibleObject & Member> {
|
||
|
|
||
|
public final R member;
|
||
|
public final T property;
|
||
|
public final String name;
|
||
|
public final Class<?> returnType;
|
||
|
|
||
|
public String entrySuffix = "";
|
||
|
public String valueSuffix = "";
|
||
|
|
||
|
PropertyInfo(Class<T> 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 <T extends Annotation> PropertyInfo<T, ?> forMethod(Method method,
|
||
|
Class<T> 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 <T extends Annotation> PropertyInfo<T, ?> forField(Field field, Class<T> property) {
|
||
|
if (!field.isAnnotationPresent(property)) {
|
||
|
return null;
|
||
|
}
|
||
|
field.setAccessible(true);
|
||
|
return new FieldPI<>(field, property);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class MethodPI<T extends Annotation> extends PropertyInfo<T, Method> {
|
||
|
|
||
|
MethodPI(Method method, Class<T> property) {
|
||
|
super(property, method, method.getReturnType());
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public Object invoke(Object target) throws Exception {
|
||
|
return member.invoke(target);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class FieldPI<T extends Annotation> extends PropertyInfo<T, Field> {
|
||
|
|
||
|
FieldPI(Field field, Class<T> 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 final String REMOTE_COMMAND_INVOKE_METHOD = "INVOKE_METHOD";
|
||
|
|
||
|
private static HashMap<Class<?>, PropertyInfo<ExportedProperty, ?>[]> sExportProperties;
|
||
|
private static HashMap<Class<?>, PropertyInfo<CapturedViewProperty, ?>[]>
|
||
|
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]);
|
||
|
} else if (REMOTE_COMMAND_INVOKE_METHOD.equals(command)) {
|
||
|
invokeViewMethod(view, clientStream, params);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** @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<Picture, Boolean> mCallback;
|
||
|
private final Executor mExecutor;
|
||
|
private final ReentrantLock mLock = new ReentrantLock(false);
|
||
|
private final ArrayDeque<Picture> mQueue = new ArrayDeque<>(3);
|
||
|
private boolean mStopListening;
|
||
|
private Thread mRenderThread;
|
||
|
|
||
|
private PictureCallbackHandler(HardwareRenderer renderer,
|
||
|
Function<Picture, Boolean> callback, Executor executor) {
|
||
|
mRenderer = renderer;
|
||
|
mCallback = callback;
|
||
|
mExecutor = executor;
|
||
|
mRenderer.setPictureCaptureCallback(this);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void close() {
|
||
|
mLock.lock();
|
||
|
mStopListening = true;
|
||
|
mLock.unlock();
|
||
|
mRenderer.setPictureCaptureCallback(null);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onPictureCaptured(Picture picture) {
|
||
|
mLock.lock();
|
||
|
if (mStopListening) {
|
||
|
mLock.unlock();
|
||
|
mRenderer.setPictureCaptureCallback(null);
|
||
|
return;
|
||
|
}
|
||
|
if (mRenderThread == null) {
|
||
|
mRenderThread = Thread.currentThread();
|
||
|
}
|
||
|
Picture toDestroy = null;
|
||
|
if (mQueue.size() == 3) {
|
||
|
toDestroy = mQueue.removeLast();
|
||
|
}
|
||
|
mQueue.add(picture);
|
||
|
mLock.unlock();
|
||
|
if (toDestroy == null) {
|
||
|
mExecutor.execute(this);
|
||
|
} else {
|
||
|
toDestroy.close();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void run() {
|
||
|
mLock.lock();
|
||
|
final Picture picture = mQueue.poll();
|
||
|
final boolean isStopped = mStopListening;
|
||
|
mLock.unlock();
|
||
|
if (Thread.currentThread() == mRenderThread) {
|
||
|
close();
|
||
|
throw new IllegalStateException(
|
||
|
"ViewDebug#startRenderingCommandsCapture must be given an executor that "
|
||
|
+ "invokes asynchronously");
|
||
|
}
|
||
|
if (isStopped) {
|
||
|
picture.close();
|
||
|
return;
|
||
|
}
|
||
|
final boolean keepReceiving = mCallback.apply(picture);
|
||
|
if (!keepReceiving) {
|
||
|
close();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Begins capturing the entire rendering commands for the view tree referenced by the given
|
||
|
* view. The view passed may be any View in the tree as long as it is attached. That is,
|
||
|
* {@link View#isAttachedToWindow()} must be true.
|
||
|
*
|
||
|
* Every time a frame is rendered a Picture will be passed to the given callback via the given
|
||
|
* executor. As long as the callback returns 'true' it will continue to receive new frames.
|
||
|
* The system will only invoke the callback at a rate that the callback is able to keep up with.
|
||
|
* That is, if it takes 48ms for the callback to complete and there is a 60fps animation running
|
||
|
* then the callback will only receive 33% of the frames produced.
|
||
|
*
|
||
|
* This method must be called on the same thread as the View tree.
|
||
|
*
|
||
|
* @param tree The View tree to capture the rendering commands.
|
||
|
* @param callback The callback to invoke on every frame produced. Should return true to
|
||
|
* continue receiving new frames, false to stop capturing.
|
||
|
* @param executor The executor to invoke the callback on. Recommend using a background thread
|
||
|
* to avoid stalling the UI thread. Must be an asynchronous invoke or an
|
||
|
* exception will be thrown.
|
||
|
* @return a closeable that can be used to stop capturing. May be invoked on any thread. Note
|
||
|
* that the callback may continue to receive another frame or two depending on thread timings.
|
||
|
* Returns null if the capture stream cannot be started, such as if there's no
|
||
|
* HardwareRenderer for the given view tree.
|
||
|
* @hide
|
||
|
* @deprecated use {@link #startRenderingCommandsCapture(View, Executor, Callable)} instead.
|
||
|
*/
|
||
|
@TestApi
|
||
|
@Nullable
|
||
|
@Deprecated
|
||
|
public static AutoCloseable startRenderingCommandsCapture(View tree, Executor executor,
|
||
|
Function<Picture, Boolean> callback) {
|
||
|
final View.AttachInfo attachInfo = tree.mAttachInfo;
|
||
|
if (attachInfo == null) {
|
||
|
throw new IllegalArgumentException("Given view isn't attached");
|
||
|
}
|
||
|
if (attachInfo.mHandler.getLooper() != Looper.myLooper()) {
|
||
|
throw new IllegalStateException("Called on the wrong thread."
|
||
|
+ " Must be called on the thread that owns the given View");
|
||
|
}
|
||
|
final HardwareRenderer renderer = attachInfo.mThreadedRenderer;
|
||
|
if (renderer != null) {
|
||
|
return new PictureCallbackHandler(renderer, callback, executor);
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
private static class StreamingPictureCallbackHandler implements AutoCloseable,
|
||
|
HardwareRenderer.PictureCapturedCallback, Runnable {
|
||
|
private final HardwareRenderer mRenderer;
|
||
|
private final Callable<OutputStream> mCallback;
|
||
|
private final Executor mExecutor;
|
||
|
private final ReentrantLock mLock = new ReentrantLock(false);
|
||
|
private final ArrayDeque<Picture> mQueue = new ArrayDeque<>(3);
|
||
|
private boolean mStopListening;
|
||
|
private Thread mRenderThread;
|
||
|
|
||
|
private StreamingPictureCallbackHandler(HardwareRenderer renderer,
|
||
|
Callable<OutputStream> callback, Executor executor) {
|
||
|
mRenderer = renderer;
|
||
|
mCallback = callback;
|
||
|
mExecutor = executor;
|
||
|
mRenderer.setPictureCaptureCallback(this);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void close() {
|
||
|
mLock.lock();
|
||
|
mStopListening = true;
|
||
|
mLock.unlock();
|
||
|
mRenderer.setPictureCaptureCallback(null);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onPictureCaptured(Picture picture) {
|
||
|
mLock.lock();
|
||
|
if (mStopListening) {
|
||
|
mLock.unlock();
|
||
|
mRenderer.setPictureCaptureCallback(null);
|
||
|
return;
|
||
|
}
|
||
|
if (mRenderThread == null) {
|
||
|
mRenderThread = Thread.currentThread();
|
||
|
}
|
||
|
boolean needsInvoke = true;
|
||
|
if (mQueue.size() == 3) {
|
||
|
mQueue.removeLast();
|
||
|
needsInvoke = false;
|
||
|
}
|
||
|
mQueue.add(picture);
|
||
|
mLock.unlock();
|
||
|
|
||
|
if (needsInvoke) {
|
||
|
mExecutor.execute(this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void run() {
|
||
|
mLock.lock();
|
||
|
final Picture picture = mQueue.poll();
|
||
|
final boolean isStopped = mStopListening;
|
||
|
mLock.unlock();
|
||
|
if (Thread.currentThread() == mRenderThread) {
|
||
|
close();
|
||
|
throw new IllegalStateException(
|
||
|
"ViewDebug#startRenderingCommandsCapture must be given an executor that "
|
||
|
+ "invokes asynchronously");
|
||
|
}
|
||
|
if (isStopped) {
|
||
|
return;
|
||
|
}
|
||
|
OutputStream stream = null;
|
||
|
try {
|
||
|
stream = mCallback.call();
|
||
|
} catch (Exception ex) {
|
||
|
Log.w("ViewDebug", "Aborting rendering commands capture "
|
||
|
+ "because callback threw exception", ex);
|
||
|
}
|
||
|
if (stream != null) {
|
||
|
try {
|
||
|
picture.writeToStream(stream);
|
||
|
stream.flush();
|
||
|
} catch (IOException ex) {
|
||
|
Log.w("ViewDebug", "Aborting rendering commands capture "
|
||
|
+ "due to IOException writing to output stream", ex);
|
||
|
}
|
||
|
} else {
|
||
|
close();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Begins capturing the entire rendering commands for the view tree referenced by the given
|
||
|
* view. The view passed may be any View in the tree as long as it is attached. That is,
|
||
|
* {@link View#isAttachedToWindow()} must be true.
|
||
|
*
|
||
|
* Every time a frame is rendered the callback will be invoked on the given executor to
|
||
|
* provide an OutputStream to serialize to. As long as the callback returns a valid
|
||
|
* OutputStream the capturing will continue. The system will only invoke the callback at a rate
|
||
|
* that the callback & OutputStream is able to keep up with. That is, if it takes 48ms for the
|
||
|
* callback & serialization to complete and there is a 60fps animation running
|
||
|
* then the callback will only receive 33% of the frames produced.
|
||
|
*
|
||
|
* This method must be called on the same thread as the View tree.
|
||
|
*
|
||
|
* @param tree The View tree to capture the rendering commands.
|
||
|
* @param callback The callback to invoke on every frame produced. Should return an
|
||
|
* OutputStream to write the data to. Return null to cancel capture. The
|
||
|
* same stream may be returned each time as the serialized data contains
|
||
|
* start & end markers. The callback will not be invoked while a previous
|
||
|
* serialization is being performed, so if a single continuous stream is being
|
||
|
* used it is valid for the callback to write its own metadata to that stream
|
||
|
* in response to callback invocation.
|
||
|
* @param executor The executor to invoke the callback on. Recommend using a background thread
|
||
|
* to avoid stalling the UI thread. Must be an asynchronous invoke or an
|
||
|
* exception will be thrown.
|
||
|
* @return a closeable that can be used to stop capturing. May be invoked on any thread. Note
|
||
|
* that the callback may continue to receive another frame or two depending on thread timings.
|
||
|
* Returns null if the capture stream cannot be started, such as if there's no
|
||
|
* HardwareRenderer for the given view tree.
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
@Nullable
|
||
|
@UnsupportedAppUsage // Visible for Studio; least-worst option available
|
||
|
public static AutoCloseable startRenderingCommandsCapture(View tree, Executor executor,
|
||
|
Callable<OutputStream> callback) {
|
||
|
final View.AttachInfo attachInfo = tree.mAttachInfo;
|
||
|
if (attachInfo == null) {
|
||
|
throw new IllegalArgumentException("Given view isn't attached");
|
||
|
}
|
||
|
if (attachInfo.mHandler.getLooper() != Looper.myLooper()) {
|
||
|
throw new IllegalStateException("Called on the wrong thread."
|
||
|
+ " Must be called on the thread that owns the given View");
|
||
|
}
|
||
|
final HardwareRenderer renderer = attachInfo.mThreadedRenderer;
|
||
|
if (renderer != null) {
|
||
|
return new StreamingPictureCallbackHandler(renderer, callback, executor);
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
private static void capture(View root, final OutputStream clientStream, String parameter)
|
||
|
throws IOException {
|
||
|
|
||
|
final View captureView = findView(root, parameter);
|
||
|
capture(root, clientStream, captureView);
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public static void capture(View root, final OutputStream clientStream, View captureView)
|
||
|
throws IOException {
|
||
|
Bitmap b = performViewCapture(captureView, false);
|
||
|
|
||
|
if (b == null) {
|
||
|
Log.w("View", "Failed to create capture bitmap!");
|
||
|
// Send an empty one so that it doesn't get stuck waiting for
|
||
|
// something.
|
||
|
b = Bitmap.createBitmap(root.getResources().getDisplayMetrics(),
|
||
|
1, 1, Bitmap.Config.ARGB_8888);
|
||
|
}
|
||
|
|
||
|
BufferedOutputStream out = null;
|
||
|
try {
|
||
|
out = new BufferedOutputStream(clientStream, 32 * 1024);
|
||
|
b.compress(Bitmap.CompressFormat.PNG, 100, out);
|
||
|
out.flush();
|
||
|
} finally {
|
||
|
if (out != null) {
|
||
|
out.close();
|
||
|
}
|
||
|
b.recycle();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static Bitmap performViewCapture(final View captureView, final boolean skipChildren) {
|
||
|
if (captureView != null) {
|
||
|
final CountDownLatch latch = new CountDownLatch(1);
|
||
|
final Bitmap[] cache = new Bitmap[1];
|
||
|
|
||
|
captureView.post(() -> {
|
||
|
try {
|
||
|
CanvasProvider provider = captureView.isHardwareAccelerated()
|
||
|
? new HardwareCanvasProvider() : new SoftwareCanvasProvider();
|
||
|
cache[0] = captureView.createSnapshot(provider, skipChildren);
|
||
|
} catch (OutOfMemoryError e) {
|
||
|
Log.w("View", "Out of memory for bitmap");
|
||
|
} finally {
|
||
|
latch.countDown();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
try {
|
||
|
latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS);
|
||
|
return cache[0];
|
||
|
} catch (InterruptedException e) {
|
||
|
Log.w("View", "Could not complete the capture of the view " + captureView);
|
||
|
Thread.currentThread().interrupt();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Dumps the view hierarchy starting from the given view.
|
||
|
* @deprecated See {@link #dumpv2(View, ByteArrayOutputStream)} below.
|
||
|
* @hide
|
||
|
*/
|
||
|
@Deprecated
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
public static void dump(View root, boolean skipChildren, boolean includeProperties,
|
||
|
OutputStream clientStream) throws IOException {
|
||
|
BufferedWriter out = null;
|
||
|
try {
|
||
|
out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024);
|
||
|
View view = root.getRootView();
|
||
|
if (view instanceof ViewGroup) {
|
||
|
ViewGroup group = (ViewGroup) view;
|
||
|
dumpViewHierarchy(group.getContext(), group, out, 0,
|
||
|
skipChildren, includeProperties);
|
||
|
}
|
||
|
out.write("DONE.");
|
||
|
out.newLine();
|
||
|
} catch (Exception e) {
|
||
|
android.util.Log.w("View", "Problem dumping the view:", e);
|
||
|
} finally {
|
||
|
if (out != null) {
|
||
|
out.close();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Dumps the view hierarchy starting from the given view.
|
||
|
* Rather than using reflection, it uses View's encode method to obtain all the properties.
|
||
|
* @hide
|
||
|
*/
|
||
|
public static void dumpv2(@NonNull final View view, @NonNull ByteArrayOutputStream out)
|
||
|
throws InterruptedException {
|
||
|
final ViewHierarchyEncoder encoder = new ViewHierarchyEncoder(out);
|
||
|
final CountDownLatch latch = new CountDownLatch(1);
|
||
|
|
||
|
view.post(new Runnable() {
|
||
|
@Override
|
||
|
public void run() {
|
||
|
encoder.addProperty("window:left", view.mAttachInfo.mWindowLeft);
|
||
|
encoder.addProperty("window:top", view.mAttachInfo.mWindowTop);
|
||
|
view.encode(encoder);
|
||
|
latch.countDown();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
latch.await(2, TimeUnit.SECONDS);
|
||
|
encoder.endStream();
|
||
|
}
|
||
|
|
||
|
private static void dumpEncoded(@NonNull final View view, @NonNull OutputStream out)
|
||
|
throws IOException {
|
||
|
ByteArrayOutputStream baOut = new ByteArrayOutputStream();
|
||
|
|
||
|
final ViewHierarchyEncoder encoder = new ViewHierarchyEncoder(baOut);
|
||
|
encoder.setUserPropertiesEnabled(false);
|
||
|
encoder.addProperty("window:left", view.mAttachInfo.mWindowLeft);
|
||
|
encoder.addProperty("window:top", view.mAttachInfo.mWindowTop);
|
||
|
view.encode(encoder);
|
||
|
encoder.endStream();
|
||
|
out.write(baOut.toByteArray());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Dumps the theme attributes from the given View.
|
||
|
* @hide
|
||
|
*/
|
||
|
public static void dumpTheme(View view, OutputStream clientStream) throws IOException {
|
||
|
BufferedWriter out = null;
|
||
|
try {
|
||
|
out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024);
|
||
|
String[] attributes = getStyleAttributesDump(view.getContext().getResources(),
|
||
|
view.getContext().getTheme());
|
||
|
if (attributes != null) {
|
||
|
for (int i = 0; i < attributes.length; i += 2) {
|
||
|
if (attributes[i] != null) {
|
||
|
out.write(attributes[i] + "\n");
|
||
|
out.write(attributes[i + 1] + "\n");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
out.write("DONE.");
|
||
|
out.newLine();
|
||
|
} catch (Exception e) {
|
||
|
android.util.Log.w("View", "Problem dumping View Theme:", e);
|
||
|
} finally {
|
||
|
if (out != null) {
|
||
|
out.close();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the style attributes from the {@link Resources.Theme}. For debugging only.
|
||
|
*
|
||
|
* @param resources Resources to resolve attributes from.
|
||
|
* @param theme Theme to dump.
|
||
|
* @return a String array containing pairs of adjacent Theme attribute data: name followed by
|
||
|
* its value.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
private static String[] getStyleAttributesDump(Resources resources, Resources.Theme theme) {
|
||
|
TypedValue outValue = new TypedValue();
|
||
|
String nullString = "null";
|
||
|
int i = 0;
|
||
|
int[] attributes = theme.getAllAttributes();
|
||
|
String[] data = new String[attributes.length * 2];
|
||
|
for (int attributeId : attributes) {
|
||
|
try {
|
||
|
data[i] = resources.getResourceName(attributeId);
|
||
|
data[i + 1] = theme.resolveAttribute(attributeId, outValue, true) ?
|
||
|
outValue.coerceToString().toString() : nullString;
|
||
|
i += 2;
|
||
|
|
||
|
// attempt to replace reference data with its name
|
||
|
if (outValue.type == TypedValue.TYPE_REFERENCE) {
|
||
|
data[i - 1] = resources.getResourceName(outValue.resourceId);
|
||
|
}
|
||
|
} catch (Resources.NotFoundException e) {
|
||
|
// ignore resources we can't resolve
|
||
|
}
|
||
|
}
|
||
|
return data;
|
||
|
}
|
||
|
|
||
|
private static View findView(ViewGroup group, String className, int hashCode) {
|
||
|
if (isRequestedView(group, className, hashCode)) {
|
||
|
return group;
|
||
|
}
|
||
|
|
||
|
final int count = group.getChildCount();
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
final View view = group.getChildAt(i);
|
||
|
if (view instanceof ViewGroup) {
|
||
|
final View found = findView((ViewGroup) view, className, hashCode);
|
||
|
if (found != null) {
|
||
|
return found;
|
||
|
}
|
||
|
} else if (isRequestedView(view, className, hashCode)) {
|
||
|
return view;
|
||
|
}
|
||
|
if (view.mOverlay != null) {
|
||
|
final View found = findView((ViewGroup) view.mOverlay.mOverlayViewGroup,
|
||
|
className, hashCode);
|
||
|
if (found != null) {
|
||
|
return found;
|
||
|
}
|
||
|
}
|
||
|
if (view instanceof HierarchyHandler) {
|
||
|
final View found = ((HierarchyHandler)view)
|
||
|
.findHierarchyView(className, hashCode);
|
||
|
if (found != null) {
|
||
|
return found;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
private static boolean isRequestedView(View view, String className, int hashCode) {
|
||
|
if (view.hashCode() == hashCode) {
|
||
|
String viewClassName = view.getClass().getName();
|
||
|
if (className.equals("ViewOverlay")) {
|
||
|
return viewClassName.equals("android.view.ViewOverlay$OverlayViewGroup");
|
||
|
} else {
|
||
|
return className.equals(viewClassName);
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
private static void dumpViewHierarchy(Context context, ViewGroup group,
|
||
|
BufferedWriter out, int level, boolean skipChildren, boolean includeProperties) {
|
||
|
cacheExportedProperties(group.getClass());
|
||
|
if (!skipChildren) {
|
||
|
cacheExportedPropertiesForChildren(group);
|
||
|
}
|
||
|
// Try to use the handler provided by the view
|
||
|
Handler handler = group.getHandler();
|
||
|
// Fall back on using the main thread
|
||
|
if (handler == null) {
|
||
|
handler = new Handler(Looper.getMainLooper());
|
||
|
}
|
||
|
|
||
|
if (handler.getLooper() == Looper.myLooper()) {
|
||
|
dumpViewHierarchyOnUIThread(context, group, out, level, skipChildren,
|
||
|
includeProperties);
|
||
|
} else {
|
||
|
FutureTask task = new FutureTask(() ->
|
||
|
dumpViewHierarchyOnUIThread(context, group, out, level, skipChildren,
|
||
|
includeProperties), null);
|
||
|
Message msg = Message.obtain(handler, task);
|
||
|
msg.setAsynchronous(true);
|
||
|
handler.sendMessage(msg);
|
||
|
while (true) {
|
||
|
try {
|
||
|
task.get(CAPTURE_TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
|
||
|
return;
|
||
|
} catch (InterruptedException e) {
|
||
|
// try again
|
||
|
} catch (ExecutionException | TimeoutException e) {
|
||
|
// Something unexpected happened.
|
||
|
throw new RuntimeException(e);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static void cacheExportedPropertiesForChildren(ViewGroup group) {
|
||
|
final int count = group.getChildCount();
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
final View view = group.getChildAt(i);
|
||
|
cacheExportedProperties(view.getClass());
|
||
|
if (view instanceof ViewGroup) {
|
||
|
cacheExportedPropertiesForChildren((ViewGroup) view);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static void cacheExportedProperties(Class<?> klass) {
|
||
|
if (sExportProperties != null && sExportProperties.containsKey(klass)) {
|
||
|
return;
|
||
|
}
|
||
|
do {
|
||
|
for (PropertyInfo<ExportedProperty, ?> info : getExportedProperties(klass)) {
|
||
|
if (!info.returnType.isPrimitive() && info.property.deepExport()) {
|
||
|
cacheExportedProperties(info.returnType);
|
||
|
}
|
||
|
}
|
||
|
klass = klass.getSuperclass();
|
||
|
} while (klass != Object.class);
|
||
|
}
|
||
|
|
||
|
|
||
|
private static void dumpViewHierarchyOnUIThread(Context context, ViewGroup group,
|
||
|
BufferedWriter out, int level, boolean skipChildren, boolean includeProperties) {
|
||
|
if (!dumpView(context, group, out, level, includeProperties)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (skipChildren) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
final int count = group.getChildCount();
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
final View view = group.getChildAt(i);
|
||
|
if (view instanceof ViewGroup) {
|
||
|
dumpViewHierarchyOnUIThread(context, (ViewGroup) view, out, level + 1,
|
||
|
skipChildren, includeProperties);
|
||
|
} else {
|
||
|
dumpView(context, view, out, level + 1, includeProperties);
|
||
|
}
|
||
|
if (view.mOverlay != null) {
|
||
|
ViewOverlay overlay = view.getOverlay();
|
||
|
ViewGroup overlayContainer = overlay.mOverlayViewGroup;
|
||
|
dumpViewHierarchyOnUIThread(context, overlayContainer, out, level + 2,
|
||
|
skipChildren, includeProperties);
|
||
|
}
|
||
|
}
|
||
|
if (group instanceof HierarchyHandler) {
|
||
|
((HierarchyHandler)group).dumpViewHierarchyWithProperties(out, level + 1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static boolean dumpView(Context context, View view,
|
||
|
BufferedWriter out, int level, boolean includeProperties) {
|
||
|
|
||
|
try {
|
||
|
for (int i = 0; i < level; i++) {
|
||
|
out.write(' ');
|
||
|
}
|
||
|
String className = view.getClass().getName();
|
||
|
if (className.equals("android.view.ViewOverlay$OverlayViewGroup")) {
|
||
|
className = "ViewOverlay";
|
||
|
}
|
||
|
out.write(className);
|
||
|
out.write('@');
|
||
|
out.write(Integer.toHexString(view.hashCode()));
|
||
|
out.write(' ');
|
||
|
if (includeProperties) {
|
||
|
dumpViewProperties(context, view, out);
|
||
|
}
|
||
|
out.newLine();
|
||
|
} catch (IOException e) {
|
||
|
Log.w("View", "Error while dumping hierarchy tree");
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private static <T extends Annotation> PropertyInfo<T, ?>[] convertToPropertyInfos(
|
||
|
Method[] methods, Field[] fields, Class<T> property) {
|
||
|
return Stream.of(Arrays.stream(methods).map(m -> PropertyInfo.forMethod(m, property)),
|
||
|
Arrays.stream(fields).map(f -> PropertyInfo.forField(f, property)))
|
||
|
.flatMap(Function.identity())
|
||
|
.filter(i -> i != null)
|
||
|
.toArray(PropertyInfo[]::new);
|
||
|
}
|
||
|
|
||
|
private static PropertyInfo<ExportedProperty, ?>[] getExportedProperties(Class<?> klass) {
|
||
|
if (sExportProperties == null) {
|
||
|
sExportProperties = new HashMap<>();
|
||
|
}
|
||
|
final HashMap<Class<?>, PropertyInfo<ExportedProperty, ?>[]> map = sExportProperties;
|
||
|
PropertyInfo<ExportedProperty, ?>[] properties = sExportProperties.get(klass);
|
||
|
|
||
|
if (properties == null) {
|
||
|
properties = convertToPropertyInfos(klass.getDeclaredMethods(),
|
||
|
klass.getDeclaredFields(), ExportedProperty.class);
|
||
|
map.put(klass, properties);
|
||
|
}
|
||
|
return properties;
|
||
|
}
|
||
|
|
||
|
private static void dumpViewProperties(Context context, Object view,
|
||
|
BufferedWriter out) throws IOException {
|
||
|
|
||
|
dumpViewProperties(context, view, out, "");
|
||
|
}
|
||
|
|
||
|
private static void dumpViewProperties(Context context, Object view,
|
||
|
BufferedWriter out, String prefix) throws IOException {
|
||
|
|
||
|
if (view == null) {
|
||
|
out.write(prefix + "=4,null ");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Class<?> klass = view.getClass();
|
||
|
do {
|
||
|
writeExportedProperties(context, view, out, klass, prefix);
|
||
|
klass = klass.getSuperclass();
|
||
|
} while (klass != Object.class);
|
||
|
}
|
||
|
|
||
|
private static String formatIntToHexString(int value) {
|
||
|
return "0x" + Integer.toHexString(value).toUpperCase();
|
||
|
}
|
||
|
|
||
|
private static void writeExportedProperties(Context context, Object view, BufferedWriter out,
|
||
|
Class<?> klass, String prefix) throws IOException {
|
||
|
for (PropertyInfo<ExportedProperty, ?> info : getExportedProperties(klass)) {
|
||
|
//noinspection EmptyCatchBlock
|
||
|
Object value;
|
||
|
try {
|
||
|
value = info.invoke(view);
|
||
|
} catch (Exception e) {
|
||
|
// ignore
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
String categoryPrefix =
|
||
|
info.property.category().length() != 0 ? info.property.category() + ":" : "";
|
||
|
|
||
|
if (info.returnType == int.class || info.returnType == byte.class) {
|
||
|
if (info.property.resolveId() && context != null) {
|
||
|
final int id = (Integer) value;
|
||
|
value = resolveId(context, id);
|
||
|
|
||
|
} else if (info.property.formatToHexString()) {
|
||
|
if (info.returnType == int.class) {
|
||
|
value = formatIntToHexString((Integer) value);
|
||
|
} else if (info.returnType == byte.class) {
|
||
|
value = "0x"
|
||
|
+ HexEncoding.encodeToString((Byte) value, true);
|
||
|
}
|
||
|
} else {
|
||
|
final ViewDebug.FlagToString[] flagsMapping = info.property.flagMapping();
|
||
|
if (flagsMapping.length > 0) {
|
||
|
final int intValue = (Integer) value;
|
||
|
final String valuePrefix =
|
||
|
categoryPrefix + prefix + info.name + '_';
|
||
|
exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
|
||
|
}
|
||
|
|
||
|
final ViewDebug.IntToString[] mapping = info.property.mapping();
|
||
|
if (mapping.length > 0) {
|
||
|
final int intValue = (Integer) value;
|
||
|
boolean mapped = false;
|
||
|
int mappingCount = mapping.length;
|
||
|
for (int j = 0; j < mappingCount; j++) {
|
||
|
final ViewDebug.IntToString mapper = mapping[j];
|
||
|
if (mapper.from() == intValue) {
|
||
|
value = mapper.to();
|
||
|
mapped = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!mapped) {
|
||
|
value = intValue;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} else if (info.returnType == int[].class) {
|
||
|
final int[] array = (int[]) value;
|
||
|
final String valuePrefix = categoryPrefix + prefix + info.name + '_';
|
||
|
exportUnrolledArray(context, out, info.property, array, valuePrefix,
|
||
|
info.entrySuffix);
|
||
|
|
||
|
continue;
|
||
|
} else if (info.returnType == String[].class) {
|
||
|
final String[] array = (String[]) value;
|
||
|
if (info.property.hasAdjacentMapping() && array != null) {
|
||
|
for (int j = 0; j < array.length; j += 2) {
|
||
|
if (array[j] != null) {
|
||
|
writeEntry(out, categoryPrefix + prefix, array[j],
|
||
|
info.entrySuffix, array[j + 1] == null ? "null" : array[j + 1]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
continue;
|
||
|
} else if (!info.returnType.isPrimitive()) {
|
||
|
if (info.property.deepExport()) {
|
||
|
dumpViewProperties(context, value, out, prefix + info.property.prefix());
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
writeEntry(out, categoryPrefix + prefix, info.name, info.entrySuffix, value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static void writeEntry(BufferedWriter out, String prefix, String name,
|
||
|
String suffix, Object value) throws IOException {
|
||
|
|
||
|
out.write(prefix);
|
||
|
out.write(name);
|
||
|
out.write(suffix);
|
||
|
out.write("=");
|
||
|
writeValue(out, value);
|
||
|
out.write(' ');
|
||
|
}
|
||
|
|
||
|
private static void exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping,
|
||
|
int intValue, String prefix) throws IOException {
|
||
|
|
||
|
final int count = mapping.length;
|
||
|
for (int j = 0; j < count; j++) {
|
||
|
final FlagToString flagMapping = mapping[j];
|
||
|
final boolean ifTrue = flagMapping.outputIf();
|
||
|
final int maskResult = intValue & flagMapping.mask();
|
||
|
final boolean test = maskResult == flagMapping.equals();
|
||
|
if ((test && ifTrue) || (!test && !ifTrue)) {
|
||
|
final String name = flagMapping.name();
|
||
|
final String value = formatIntToHexString(maskResult);
|
||
|
writeEntry(out, prefix, name, "", value);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Converts an integer from a field that is mapped with {@link IntToString} to its string
|
||
|
* representation.
|
||
|
*
|
||
|
* @param clazz The class the field is defined on.
|
||
|
* @param field The field on which the {@link ExportedProperty} is defined on.
|
||
|
* @param integer The value to convert.
|
||
|
* @return The value converted into its string representation.
|
||
|
* @hide
|
||
|
*/
|
||
|
public static String intToString(Class<?> clazz, String field, int integer) {
|
||
|
final IntToString[] mapping = getMapping(clazz, field);
|
||
|
if (mapping == null) {
|
||
|
return Integer.toString(integer);
|
||
|
}
|
||
|
final int count = mapping.length;
|
||
|
for (int j = 0; j < count; j++) {
|
||
|
final IntToString map = mapping[j];
|
||
|
if (map.from() == integer) {
|
||
|
return map.to();
|
||
|
}
|
||
|
}
|
||
|
return Integer.toString(integer);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Converts a set of flags from a field that is mapped with {@link FlagToString} to its string
|
||
|
* representation.
|
||
|
*
|
||
|
* @param clazz The class the field is defined on.
|
||
|
* @param field The field on which the {@link ExportedProperty} is defined on.
|
||
|
* @param flags The flags to convert.
|
||
|
* @return The flags converted into their string representations.
|
||
|
* @hide
|
||
|
*/
|
||
|
public static String flagsToString(Class<?> clazz, String field, int flags) {
|
||
|
final FlagToString[] mapping = getFlagMapping(clazz, field);
|
||
|
if (mapping == null) {
|
||
|
return Integer.toHexString(flags);
|
||
|
}
|
||
|
final StringBuilder result = new StringBuilder();
|
||
|
final int count = mapping.length;
|
||
|
for (int j = 0; j < count; j++) {
|
||
|
final FlagToString flagMapping = mapping[j];
|
||
|
final boolean ifTrue = flagMapping.outputIf();
|
||
|
final int maskResult = flags & flagMapping.mask();
|
||
|
final boolean test = maskResult == flagMapping.equals();
|
||
|
if (test && ifTrue) {
|
||
|
final String name = flagMapping.name();
|
||
|
result.append(name).append(' ');
|
||
|
}
|
||
|
}
|
||
|
if (result.length() > 0) {
|
||
|
result.deleteCharAt(result.length() - 1);
|
||
|
}
|
||
|
return result.toString();
|
||
|
}
|
||
|
|
||
|
private static FlagToString[] getFlagMapping(Class<?> clazz, String field) {
|
||
|
try {
|
||
|
return clazz.getDeclaredField(field).getAnnotation(ExportedProperty.class)
|
||
|
.flagMapping();
|
||
|
} catch (NoSuchFieldException e) {
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static IntToString[] getMapping(Class<?> clazz, String field) {
|
||
|
try {
|
||
|
return clazz.getDeclaredField(field).getAnnotation(ExportedProperty.class).mapping();
|
||
|
} catch (NoSuchFieldException e) {
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static void exportUnrolledArray(Context context, BufferedWriter out,
|
||
|
ExportedProperty property, int[] array, String prefix, String suffix)
|
||
|
throws IOException {
|
||
|
|
||
|
final IntToString[] indexMapping = property.indexMapping();
|
||
|
final boolean hasIndexMapping = indexMapping.length > 0;
|
||
|
|
||
|
final IntToString[] mapping = property.mapping();
|
||
|
final boolean hasMapping = mapping.length > 0;
|
||
|
|
||
|
final boolean resolveId = property.resolveId() && context != null;
|
||
|
final int valuesCount = array.length;
|
||
|
|
||
|
for (int j = 0; j < valuesCount; j++) {
|
||
|
String name;
|
||
|
String value = null;
|
||
|
|
||
|
final int intValue = array[j];
|
||
|
|
||
|
name = String.valueOf(j);
|
||
|
if (hasIndexMapping) {
|
||
|
int mappingCount = indexMapping.length;
|
||
|
for (int k = 0; k < mappingCount; k++) {
|
||
|
final IntToString mapped = indexMapping[k];
|
||
|
if (mapped.from() == j) {
|
||
|
name = mapped.to();
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (hasMapping) {
|
||
|
int mappingCount = mapping.length;
|
||
|
for (int k = 0; k < mappingCount; k++) {
|
||
|
final IntToString mapped = mapping[k];
|
||
|
if (mapped.from() == intValue) {
|
||
|
value = mapped.to();
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (resolveId) {
|
||
|
if (value == null) value = (String) resolveId(context, intValue);
|
||
|
} else {
|
||
|
value = String.valueOf(intValue);
|
||
|
}
|
||
|
|
||
|
writeEntry(out, prefix, name, suffix, value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static Object resolveId(Context context, int id) {
|
||
|
Object fieldValue;
|
||
|
final Resources resources = context.getResources();
|
||
|
if (id >= 0) {
|
||
|
try {
|
||
|
fieldValue = resources.getResourceTypeName(id) + '/' +
|
||
|
resources.getResourceEntryName(id);
|
||
|
} catch (Resources.NotFoundException e) {
|
||
|
fieldValue = "id/" + formatIntToHexString(id);
|
||
|
}
|
||
|
} else {
|
||
|
fieldValue = "NO_ID";
|
||
|
}
|
||
|
return fieldValue;
|
||
|
}
|
||
|
|
||
|
private static void writeValue(BufferedWriter out, Object value) throws IOException {
|
||
|
if (value != null) {
|
||
|
String output = "[EXCEPTION]";
|
||
|
try {
|
||
|
output = value.toString().replace("\n", "\\n");
|
||
|
} finally {
|
||
|
out.write(String.valueOf(output.length()));
|
||
|
out.write(",");
|
||
|
out.write(output);
|
||
|
}
|
||
|
} else {
|
||
|
out.write("4,null");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static PropertyInfo<CapturedViewProperty, ?>[] getCapturedViewProperties(
|
||
|
Class<?> klass) {
|
||
|
if (sCapturedViewProperties == null) {
|
||
|
sCapturedViewProperties = new HashMap<>();
|
||
|
}
|
||
|
final HashMap<Class<?>, PropertyInfo<CapturedViewProperty, ?>[]> map =
|
||
|
sCapturedViewProperties;
|
||
|
|
||
|
PropertyInfo<CapturedViewProperty, ?>[] infos = map.get(klass);
|
||
|
if (infos == null) {
|
||
|
infos = convertToPropertyInfos(klass.getMethods(), klass.getFields(),
|
||
|
CapturedViewProperty.class);
|
||
|
map.put(klass, infos);
|
||
|
}
|
||
|
return infos;
|
||
|
}
|
||
|
|
||
|
private static String exportCapturedViewProperties(Object obj, Class<?> klass, String prefix) {
|
||
|
if (obj == null) {
|
||
|
return "null";
|
||
|
}
|
||
|
|
||
|
StringBuilder sb = new StringBuilder();
|
||
|
|
||
|
for (PropertyInfo<CapturedViewProperty, ?> pi : getCapturedViewProperties(klass)) {
|
||
|
try {
|
||
|
Object methodValue = pi.invoke(obj);
|
||
|
|
||
|
if (pi.property.retrieveReturn()) {
|
||
|
//we are interested in the second level data only
|
||
|
sb.append(exportCapturedViewProperties(methodValue, pi.returnType,
|
||
|
pi.name + "#"));
|
||
|
} else {
|
||
|
sb.append(prefix).append(pi.name).append(pi.entrySuffix).append("=");
|
||
|
|
||
|
if (methodValue != null) {
|
||
|
final String value = methodValue.toString().replace("\n", "\\n");
|
||
|
sb.append(value);
|
||
|
} else {
|
||
|
sb.append("null");
|
||
|
}
|
||
|
sb.append(pi.valueSuffix).append(" ");
|
||
|
}
|
||
|
} catch (Exception e) {
|
||
|
//It is OK here, we simply ignore this property
|
||
|
}
|
||
|
}
|
||
|
return sb.toString();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Dump view info for id based instrument test generation
|
||
|
* (and possibly further data analysis). The results are dumped
|
||
|
* to the log.
|
||
|
* @param tag for log
|
||
|
* @param view for dump
|
||
|
*/
|
||
|
public static void dumpCapturedView(String tag, Object view) {
|
||
|
Class<?> klass = view.getClass();
|
||
|
StringBuilder sb = new StringBuilder(klass.getName() + ": ");
|
||
|
sb.append(exportCapturedViewProperties(view, klass, ""));
|
||
|
Log.d(tag, sb.toString());
|
||
|
}
|
||
|
|
||
|
private static void invokeViewMethod(View root, OutputStream clientStream, String[] params)
|
||
|
throws IOException {
|
||
|
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
|
||
|
try {
|
||
|
if (params.length < 2) {
|
||
|
throw new IllegalArgumentException("Missing parameter");
|
||
|
}
|
||
|
View targetView = findView(root, params[0]);
|
||
|
if (targetView == null) {
|
||
|
throw new IllegalArgumentException("View not found: " + params[0]);
|
||
|
}
|
||
|
String method = params[1];
|
||
|
ByteBuffer args = ByteBuffer.wrap(params.length < 2
|
||
|
? new byte[0]
|
||
|
: Base64.decode(params[2], Base64.NO_WRAP));
|
||
|
byte[] result = invokeViewMethod(targetView, method, args);
|
||
|
out.write("1");
|
||
|
out.newLine();
|
||
|
out.write(Base64.encodeToString(result, Base64.NO_WRAP));
|
||
|
out.newLine();
|
||
|
} catch (Exception e) {
|
||
|
out.write("-1");
|
||
|
out.newLine();
|
||
|
out.write(e.getMessage());
|
||
|
out.newLine();
|
||
|
} finally {
|
||
|
out.close();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Invoke a particular method on given view.
|
||
|
* The given method is always invoked on the UI thread. The caller thread will stall until the
|
||
|
* method invocation is complete. Returns an object equal to the result of the method
|
||
|
* invocation, null if the method is declared to return void
|
||
|
* @param params all the method parameters encoded in a byteArray
|
||
|
* @throws Exception if the method invocation caused any exception
|
||
|
* @hide
|
||
|
*/
|
||
|
public static byte[] invokeViewMethod(View targetView, String methodName, ByteBuffer params)
|
||
|
throws ViewMethodInvocationSerializationException {
|
||
|
Class<?>[] argTypes;
|
||
|
Object[] args;
|
||
|
if (!params.hasRemaining()) {
|
||
|
argTypes = new Class<?>[0];
|
||
|
args = new Object[0];
|
||
|
} else {
|
||
|
int nArgs = params.getInt();
|
||
|
argTypes = new Class<?>[nArgs];
|
||
|
args = new Object[nArgs];
|
||
|
|
||
|
deserializeMethodParameters(args, argTypes, params);
|
||
|
}
|
||
|
|
||
|
Method method;
|
||
|
try {
|
||
|
method = targetView.getClass().getMethod(methodName, argTypes);
|
||
|
} catch (NoSuchMethodException e) {
|
||
|
Log.e(TAG, "No such method: " + e.getMessage());
|
||
|
throw new ViewMethodInvocationSerializationException(
|
||
|
"No such method: " + e.getMessage());
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
// Invoke the method on Views handler
|
||
|
FutureTask<Object> task = new FutureTask<>(() -> method.invoke(targetView, args));
|
||
|
targetView.post(task);
|
||
|
Object result = task.get();
|
||
|
Class<?> returnType = method.getReturnType();
|
||
|
return serializeReturnValue(returnType, returnType.cast(result));
|
||
|
} catch (Exception e) {
|
||
|
Log.e(TAG, "Exception while invoking method: " + e.getCause().getMessage());
|
||
|
String msg = e.getCause().getMessage();
|
||
|
if (msg == null) {
|
||
|
msg = e.getCause().toString();
|
||
|
}
|
||
|
throw new RuntimeException(msg);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public static void setLayoutParameter(final View view, final String param, final int value)
|
||
|
throws NoSuchFieldException, IllegalAccessException {
|
||
|
final ViewGroup.LayoutParams p = view.getLayoutParams();
|
||
|
final Field f = p.getClass().getField(param);
|
||
|
if (f.getType() != int.class) {
|
||
|
throw new RuntimeException("Only integer layout parameters can be set. Field "
|
||
|
+ param + " is of type " + f.getType().getSimpleName());
|
||
|
}
|
||
|
|
||
|
f.set(p, Integer.valueOf(value));
|
||
|
|
||
|
view.post(new Runnable() {
|
||
|
@Override
|
||
|
public void run() {
|
||
|
view.setLayoutParams(p);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public static class SoftwareCanvasProvider implements CanvasProvider {
|
||
|
|
||
|
private Canvas mCanvas;
|
||
|
private Bitmap mBitmap;
|
||
|
private boolean mEnabledHwFeaturesInSwMode;
|
||
|
|
||
|
@Override
|
||
|
public Canvas getCanvas(View view, int width, int height) {
|
||
|
mBitmap = Bitmap.createBitmap(view.getResources().getDisplayMetrics(),
|
||
|
width, height, Bitmap.Config.ARGB_8888);
|
||
|
if (mBitmap == null) {
|
||
|
throw new OutOfMemoryError();
|
||
|
}
|
||
|
mBitmap.setDensity(view.getResources().getDisplayMetrics().densityDpi);
|
||
|
|
||
|
if (view.mAttachInfo != null) {
|
||
|
mCanvas = view.mAttachInfo.mCanvas;
|
||
|
}
|
||
|
if (mCanvas == null) {
|
||
|
mCanvas = new Canvas();
|
||
|
}
|
||
|
mEnabledHwFeaturesInSwMode = mCanvas.isHwFeaturesInSwModeEnabled();
|
||
|
mCanvas.setBitmap(mBitmap);
|
||
|
return mCanvas;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public Bitmap createBitmap() {
|
||
|
mCanvas.setBitmap(null);
|
||
|
mCanvas.setHwFeaturesInSwModeEnabled(mEnabledHwFeaturesInSwMode);
|
||
|
return mBitmap;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public static class HardwareCanvasProvider implements CanvasProvider {
|
||
|
private Picture mPicture;
|
||
|
|
||
|
@Override
|
||
|
public Canvas getCanvas(View view, int width, int height) {
|
||
|
mPicture = new Picture();
|
||
|
return mPicture.beginRecording(width, height);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public Bitmap createBitmap() {
|
||
|
mPicture.endRecording();
|
||
|
return Bitmap.createBitmap(mPicture);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public interface CanvasProvider {
|
||
|
|
||
|
/**
|
||
|
* Returns a canvas which can be used to draw {@param view}
|
||
|
*/
|
||
|
Canvas getCanvas(View view, int width, int height);
|
||
|
|
||
|
/**
|
||
|
* Creates a bitmap from previously returned canvas
|
||
|
* @return
|
||
|
*/
|
||
|
Bitmap createBitmap();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Deserializes parameters according to the VUOP_INVOKE_VIEW_METHOD protocol the {@code in}
|
||
|
* buffer.
|
||
|
*
|
||
|
* The length of {@code args} determines how many arguments are read. The {@code argTypes} must
|
||
|
* be the same length, and will be set to the argument types of the data read.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@VisibleForTesting
|
||
|
public static void deserializeMethodParameters(
|
||
|
Object[] args, Class<?>[] argTypes, ByteBuffer in) throws
|
||
|
ViewMethodInvocationSerializationException {
|
||
|
checkArgument(args.length == argTypes.length);
|
||
|
|
||
|
for (int i = 0; i < args.length; i++) {
|
||
|
char typeSignature = in.getChar();
|
||
|
boolean isArray = typeSignature == SIG_ARRAY;
|
||
|
if (isArray) {
|
||
|
char arrayType = in.getChar();
|
||
|
if (arrayType != SIG_BYTE) {
|
||
|
// This implementation only supports byte-arrays for now.
|
||
|
throw new ViewMethodInvocationSerializationException(
|
||
|
"Unsupported array parameter type (" + typeSignature
|
||
|
+ ") to invoke view method @argument " + i);
|
||
|
}
|
||
|
|
||
|
int arrayLength = in.getInt();
|
||
|
if (arrayLength > in.remaining()) {
|
||
|
// The sender did not actually sent the specified amount of bytes. This
|
||
|
// avoids a malformed packet to trigger an out-of-memory error.
|
||
|
throw new BufferUnderflowException();
|
||
|
}
|
||
|
|
||
|
byte[] byteArray = new byte[arrayLength];
|
||
|
in.get(byteArray);
|
||
|
|
||
|
argTypes[i] = byte[].class;
|
||
|
args[i] = byteArray;
|
||
|
} else {
|
||
|
switch (typeSignature) {
|
||
|
case SIG_BOOLEAN:
|
||
|
argTypes[i] = boolean.class;
|
||
|
args[i] = in.get() != 0;
|
||
|
break;
|
||
|
case SIG_BYTE:
|
||
|
argTypes[i] = byte.class;
|
||
|
args[i] = in.get();
|
||
|
break;
|
||
|
case SIG_CHAR:
|
||
|
argTypes[i] = char.class;
|
||
|
args[i] = in.getChar();
|
||
|
break;
|
||
|
case SIG_SHORT:
|
||
|
argTypes[i] = short.class;
|
||
|
args[i] = in.getShort();
|
||
|
break;
|
||
|
case SIG_INT:
|
||
|
argTypes[i] = int.class;
|
||
|
args[i] = in.getInt();
|
||
|
break;
|
||
|
case SIG_LONG:
|
||
|
argTypes[i] = long.class;
|
||
|
args[i] = in.getLong();
|
||
|
break;
|
||
|
case SIG_FLOAT:
|
||
|
argTypes[i] = float.class;
|
||
|
args[i] = in.getFloat();
|
||
|
break;
|
||
|
case SIG_DOUBLE:
|
||
|
argTypes[i] = double.class;
|
||
|
args[i] = in.getDouble();
|
||
|
break;
|
||
|
case SIG_STRING: {
|
||
|
argTypes[i] = String.class;
|
||
|
int stringUtf8ByteCount = Short.toUnsignedInt(in.getShort());
|
||
|
byte[] rawStringBuffer = new byte[stringUtf8ByteCount];
|
||
|
in.get(rawStringBuffer);
|
||
|
args[i] = new String(rawStringBuffer, StandardCharsets.UTF_8);
|
||
|
break;
|
||
|
}
|
||
|
default:
|
||
|
Log.e(TAG, "arg " + i + ", unrecognized type: " + typeSignature);
|
||
|
throw new ViewMethodInvocationSerializationException(
|
||
|
"Unsupported parameter type (" + typeSignature
|
||
|
+ ") to invoke view method.");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Serializes {@code value} to the wire protocol of VUOP_INVOKE_VIEW_METHOD.
|
||
|
* @hide
|
||
|
*/
|
||
|
@VisibleForTesting
|
||
|
public static byte[] serializeReturnValue(Class<?> type, Object value)
|
||
|
throws ViewMethodInvocationSerializationException, IOException {
|
||
|
ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(1024);
|
||
|
DataOutputStream dos = new DataOutputStream(byteOutStream);
|
||
|
|
||
|
if (type.isArray()) {
|
||
|
if (!type.equals(byte[].class)) {
|
||
|
// Only byte arrays are supported currently.
|
||
|
throw new ViewMethodInvocationSerializationException(
|
||
|
"Unsupported array return type (" + type + ")");
|
||
|
}
|
||
|
byte[] byteArray = (byte[]) value;
|
||
|
dos.writeChar(SIG_ARRAY);
|
||
|
dos.writeChar(SIG_BYTE);
|
||
|
dos.writeInt(byteArray.length);
|
||
|
dos.write(byteArray);
|
||
|
} else if (boolean.class.equals(type)) {
|
||
|
dos.writeChar(SIG_BOOLEAN);
|
||
|
dos.write((boolean) value ? 1 : 0);
|
||
|
} else if (byte.class.equals(type)) {
|
||
|
dos.writeChar(SIG_BYTE);
|
||
|
dos.writeByte((byte) value);
|
||
|
} else if (char.class.equals(type)) {
|
||
|
dos.writeChar(SIG_CHAR);
|
||
|
dos.writeChar((char) value);
|
||
|
} else if (short.class.equals(type)) {
|
||
|
dos.writeChar(SIG_SHORT);
|
||
|
dos.writeShort((short) value);
|
||
|
} else if (int.class.equals(type)) {
|
||
|
dos.writeChar(SIG_INT);
|
||
|
dos.writeInt((int) value);
|
||
|
} else if (long.class.equals(type)) {
|
||
|
dos.writeChar(SIG_LONG);
|
||
|
dos.writeLong((long) value);
|
||
|
} else if (double.class.equals(type)) {
|
||
|
dos.writeChar(SIG_DOUBLE);
|
||
|
dos.writeDouble((double) value);
|
||
|
} else if (float.class.equals(type)) {
|
||
|
dos.writeChar(SIG_FLOAT);
|
||
|
dos.writeFloat((float) value);
|
||
|
} else if (String.class.equals(type)) {
|
||
|
dos.writeChar(SIG_STRING);
|
||
|
dos.writeUTF(value != null ? (String) value : "");
|
||
|
} else {
|
||
|
dos.writeChar(SIG_VOID);
|
||
|
}
|
||
|
|
||
|
return byteOutStream.toByteArray();
|
||
|
}
|
||
|
|
||
|
// Prefixes for simple primitives. These match the JNI definitions.
|
||
|
private static final char SIG_ARRAY = '[';
|
||
|
private static final char SIG_BOOLEAN = 'Z';
|
||
|
private static final char SIG_BYTE = 'B';
|
||
|
private static final char SIG_SHORT = 'S';
|
||
|
private static final char SIG_CHAR = 'C';
|
||
|
private static final char SIG_INT = 'I';
|
||
|
private static final char SIG_LONG = 'J';
|
||
|
private static final char SIG_FLOAT = 'F';
|
||
|
private static final char SIG_DOUBLE = 'D';
|
||
|
private static final char SIG_VOID = 'V';
|
||
|
// Prefixes for some commonly used objects
|
||
|
private static final char SIG_STRING = 'R';
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
@VisibleForTesting
|
||
|
public static class ViewMethodInvocationSerializationException extends Exception {
|
||
|
ViewMethodInvocationSerializationException(String message) {
|
||
|
super(message);
|
||
|
}
|
||
|
}
|
||
|
}
|