package android.view;
import android.annotation.NonNull;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Picture;
import android.graphics.Rect;
import android.os.Debug;
import android.os.Handler;
import android.os.RemoteException;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
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.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.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
public class ViewDebug {
@Deprecated
public static final boolean TRACE_HIERARCHY = false;
@Deprecated
public static final boolean TRACE_RECYCLER = false;
public static final boolean DEBUG_DRAG = false;
public static final boolean DEBUG_POSITIONING = false;
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ExportedProperty {
boolean resolveId() default false;
IntToString[] mapping() default { };
IntToString[] indexMapping() default { };
FlagToString[] flagMapping() default { };
boolean deepExport() default false;
String prefix() default "";
String category() default "";
boolean formatToHexString() default false;
boolean hasAdjacentMapping() default false;
}
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface IntToString {
int from();
String to();
}
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface FlagToString {
int mask();
int equals();
String name();
boolean outputIf() default true;
}
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface CapturedViewProperty {
boolean retrieveReturn() default false;
}
public interface HierarchyHandler {
public void dumpViewHierarchyWithProperties(BufferedWriter out, int level);
public View findHierarchyView(String className, int hashCode);
}
private static HashMap<Class<?>, Method[]> mCapturedViewMethodsForClasses = null;
private static HashMap<Class<?>, Field[]> mCapturedViewFieldsForClasses = null;
private static final int CAPTURE_TIMEOUT = 4000;
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";
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<Class<?>, Field[]> sFieldsForClasses;
private static HashMap<Class<?>, Method[]> sMethodsForClasses;
private static HashMap<AccessibleObject, ExportedProperty> sAnnotations;
@Deprecated
public enum HierarchyTraceType {
INVALIDATE,
INVALIDATE_CHILD,
INVALIDATE_CHILD_IN_PARENT,
REQUEST_LAYOUT,
ON_LAYOUT,
ON_MEASURE,
DRAW,
BUILD_CACHE
}
@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
}
public static long getViewInstanceCount() {
return Debug.countInstancesOfClass(View.class);
}
public static long getViewRootImplCount() {
return Debug.countInstancesOfClass(ViewRootImpl.class);
}
@Deprecated
@SuppressWarnings({ "UnusedParameters", "deprecation" })
public static void trace(View view, RecyclerTraceType type, int... parameters) {
}
@Deprecated
@SuppressWarnings("UnusedParameters")
public static void startRecyclerTracing(String prefix, View view) {
}
@Deprecated
@SuppressWarnings("UnusedParameters")
public static void stopRecyclerTracing() {
}
@Deprecated
@SuppressWarnings({ "UnusedParameters", "deprecation" })
public static void trace(View view, HierarchyTraceType type) {
}
@Deprecated
@SuppressWarnings("UnusedParameters")
public static void startHierarchyTracing(String prefix, View view) {
}
@Deprecated
public static void stopHierarchyTracing() {
}
static void dispatchCommand(View view, String command, String parameters,
OutputStream clientStream) throws IOException {
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_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]);
}
}
}
public static View findView(View root, String parameter) {
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 {
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();
}
}
}
public static void profileViewAndChildren(final View view, BufferedWriter out)
throws IOException {
RenderNode node = RenderNode.create("ViewDebug", null);
profileViewAndChildren(view, node, out, true);
node.destroy();
}
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()) {
DisplayListCanvas canvas = node.start(dm.widthPixels, dm.heightPixels);
try {
return profileViewOperation(view, () -> view.draw(canvas));
} finally {
node.end(canvas);
}
} 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();
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];
}
public static void captureLayers(View root, final DataOutputStream clientStream)
throws IOException {
try {
Rect outRect = new Rect();
try {
root.mAttachInfo.mSession.getDisplayFrame(root.mAttachInfo.mWindow, outRect);
} catch (RemoteException e) {
}
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];
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);
}
public static void outputDisplayList(View root, View target) {
root.getViewRootImpl().outputDisplayList(target);
}
private static void capture(View root, final OutputStream clientStream, String parameter)
throws IOException {
final View captureView = findView(root, parameter);
capture(root, clientStream, captureView);
}
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!");
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;
}
@Deprecated
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();
}
}
}
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();
}
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();
}
}
}
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;
if (outValue.type == TypedValue.TYPE_REFERENCE) {
data[i - 1] = resources.getResourceName(outValue.resourceId);
}
} catch (Resources.NotFoundException e) {
}
}
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) {
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) {
dumpViewHierarchy(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;
dumpViewHierarchy(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 Field[] getExportedPropertyFields(Class<?> klass) {
if (sFieldsForClasses == null) {
sFieldsForClasses = new HashMap<Class<?>, Field[]>();
}
if (sAnnotations == null) {
sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512);
}
final HashMap<Class<?>, Field[]> map = sFieldsForClasses;
Field[] fields = map.get(klass);
if (fields != null) {
return fields;
}
try {
final Field[] declaredFields = klass.getDeclaredFieldsUnchecked(false);
final ArrayList<Field> foundFields = new ArrayList<Field>();
for (final Field field : declaredFields) {
if (field.getType() != null && field.isAnnotationPresent(ExportedProperty.class)) {
field.setAccessible(true);
foundFields.add(field);
sAnnotations.put(field, field.getAnnotation(ExportedProperty.class));
}
}
fields = foundFields.toArray(new Field[foundFields.size()]);
map.put(klass, fields);
} catch (NoClassDefFoundError e) {
throw new AssertionError(e);
}
return fields;
}
private static Method[] getExportedPropertyMethods(Class<?> klass) {
if (sMethodsForClasses == null) {
sMethodsForClasses = new HashMap<Class<?>, Method[]>(100);
}
if (sAnnotations == null) {
sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512);
}
final HashMap<Class<?>, Method[]> map = sMethodsForClasses;
Method[] methods = map.get(klass);
if (methods != null) {
return methods;
}
methods = klass.getDeclaredMethodsUnchecked(false);
final ArrayList<Method> foundMethods = new ArrayList<Method>();
for (final Method method : methods) {
try {
method.getReturnType();
method.getParameterTypes();
} catch (NoClassDefFoundError e) {
continue;
}
if (method.getParameterTypes().length == 0 &&
method.isAnnotationPresent(ExportedProperty.class) &&
method.getReturnType() != Void.class) {
method.setAccessible(true);
foundMethods.add(method);
sAnnotations.put(method, method.getAnnotation(ExportedProperty.class));
}
}
methods = foundMethods.toArray(new Method[foundMethods.size()]);
map.put(klass, methods);
return methods;
}
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 {
exportFields(context, view, out, klass, prefix);
exportMethods(context, view, out, klass, prefix);
klass = klass.getSuperclass();
} while (klass != Object.class);
}
private static Object callMethodOnAppropriateTheadBlocking(final Method method,
final Object object) throws IllegalAccessException, InvocationTargetException,
TimeoutException {
if (!(object instanceof View)) {
return method.invoke(object, (Object[]) null);
}
final View view = (View) object;
Callable<Object> callable = new Callable<Object>() {
@Override
public Object call() throws IllegalAccessException, InvocationTargetException {
return method.invoke(view, (Object[]) null);
}
};
FutureTask<Object> future = new FutureTask<Object>(callable);
Handler handler = view.getHandler();
if (handler == null) {
handler = new Handler(android.os.Looper.getMainLooper());
}
handler.post(future);
while (true) {
try {
return future.get(CAPTURE_TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
} catch (ExecutionException e) {
Throwable t = e.getCause();
if (t instanceof IllegalAccessException) {
throw (IllegalAccessException)t;
}
if (t instanceof InvocationTargetException) {
throw (InvocationTargetException)t;
}
throw new RuntimeException("Unexpected exception", t);
} catch (InterruptedException e) {
} catch (CancellationException e) {
throw new RuntimeException("Unexpected cancellation exception", e);
}
}
}
private static String formatIntToHexString(int value) {
return "0x" + Integer.toHexString(value).toUpperCase();
}
private static void exportMethods(Context context, Object view, BufferedWriter out,
Class<?> klass, String prefix) throws IOException {
final Method[] methods = getExportedPropertyMethods(klass);
int count = methods.length;
for (int i = 0; i < count; i++) {
final Method method = methods[i];
try {
Object methodValue = callMethodOnAppropriateTheadBlocking(method, view);
final Class<?> returnType = method.getReturnType();
final ExportedProperty property = sAnnotations.get(method);
String categoryPrefix =
property.category().length() != 0 ? property.category() + ":" : "";
if (returnType == int.class) {
if (property.resolveId() && context != null) {
final int id = (Integer) methodValue;
methodValue = resolveId(context, id);
} else {
final FlagToString[] flagsMapping = property.flagMapping();
if (flagsMapping.length > 0) {
final int intValue = (Integer) methodValue;
final String valuePrefix =
categoryPrefix + prefix + method.getName() + '_';
exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
}
final IntToString[] mapping = property.mapping();
if (mapping.length > 0) {
final int intValue = (Integer) methodValue;
boolean mapped = false;
int mappingCount = mapping.length;
for (int j = 0; j < mappingCount; j++) {
final IntToString mapper = mapping[j];
if (mapper.from() == intValue) {
methodValue = mapper.to();
mapped = true;
break;
}
}
if (!mapped) {
methodValue = intValue;
}
}
}
} else if (returnType == int[].class) {
final int[] array = (int[]) methodValue;
final String valuePrefix = categoryPrefix + prefix + method.getName() + '_';
final String suffix = "()";
exportUnrolledArray(context, out, property, array, valuePrefix, suffix);
continue;
} else if (returnType == String[].class) {
final String[] array = (String[]) methodValue;
if (property.hasAdjacentMapping() && array != null) {
for (int j = 0; j < array.length; j += 2) {
if (array[j] != null) {
writeEntry(out, categoryPrefix + prefix, array[j], "()",
array[j + 1] == null ? "null" : array[j + 1]);
}
}
}
continue;
} else if (!returnType.isPrimitive()) {
if (property.deepExport()) {
dumpViewProperties(context, methodValue, out, prefix + property.prefix());
continue;
}
}
writeEntry(out, categoryPrefix + prefix, method.getName(), "()", methodValue);
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
} catch (TimeoutException e) {
}
}
}
private static void exportFields(Context context, Object view, BufferedWriter out,
Class<?> klass, String prefix) throws IOException {
final Field[] fields = getExportedPropertyFields(klass);
int count = fields.length;
for (int i = 0; i < count; i++) {
final Field field = fields[i];
try {
Object fieldValue = null;
final Class<?> type = field.getType();
final ExportedProperty property = sAnnotations.get(field);
String categoryPrefix =
property.category().length() != 0 ? property.category() + ":" : "";
if (type == int.class || type == byte.class) {
if (property.resolveId() && context != null) {
final int id = field.getInt(view);
fieldValue = resolveId(context, id);
} else {
final FlagToString[] flagsMapping = property.flagMapping();
if (flagsMapping.length > 0) {
final int intValue = field.getInt(view);
final String valuePrefix =
categoryPrefix + prefix + field.getName() + '_';
exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
}
final IntToString[] mapping = property.mapping();
if (mapping.length > 0) {
final int intValue = field.getInt(view);
int mappingCount = mapping.length;
for (int j = 0; j < mappingCount; j++) {
final IntToString mapped = mapping[j];
if (mapped.from() == intValue) {
fieldValue = mapped.to();
break;
}
}
if (fieldValue == null) {
fieldValue = intValue;
}
}
if (property.formatToHexString()) {
fieldValue = field.get(view);
if (type == int.class) {
fieldValue = formatIntToHexString((Integer) fieldValue);
} else if (type == byte.class) {
fieldValue = "0x" + Byte.toHexString((Byte) fieldValue, true);
}
}
}
} else if (type == int[].class) {
final int[] array = (int[]) field.get(view);
final String valuePrefix = categoryPrefix + prefix + field.getName() + '_';
final String suffix = "";
exportUnrolledArray(context, out, property, array, valuePrefix, suffix);
continue;
} else if (type == String[].class) {
final String[] array = (String[]) field.get(view);
if (property.hasAdjacentMapping() && array != null) {
for (int j = 0; j < array.length; j += 2) {
if (array[j] != null) {
writeEntry(out, categoryPrefix + prefix, array[j], "",
array[j + 1] == null ? "null" : array[j + 1]);
}
}
}
continue;
} else if (!type.isPrimitive()) {
if (property.deepExport()) {
dumpViewProperties(context, field.get(view), out, prefix +
property.prefix());
continue;
}
}
if (fieldValue == null) {
fieldValue = field.get(view);
}
writeEntry(out, categoryPrefix + prefix, field.getName(), "", fieldValue);
} catch (IllegalAccessException e) {
}
}
}
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);
}
}
}
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);
}
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 Field[] capturedViewGetPropertyFields(Class<?> klass) {
if (mCapturedViewFieldsForClasses == null) {
mCapturedViewFieldsForClasses = new HashMap<Class<?>, Field[]>();
}
final HashMap<Class<?>, Field[]> map = mCapturedViewFieldsForClasses;
Field[] fields = map.get(klass);
if (fields != null) {
return fields;
}
final ArrayList<Field> foundFields = new ArrayList<Field>();
fields = klass.getFields();
int count = fields.length;
for (int i = 0; i < count; i++) {
final Field field = fields[i];
if (field.isAnnotationPresent(CapturedViewProperty.class)) {
field.setAccessible(true);
foundFields.add(field);
}
}
fields = foundFields.toArray(new Field[foundFields.size()]);
map.put(klass, fields);
return fields;
}
private static Method[] capturedViewGetPropertyMethods(Class<?> klass) {
if (mCapturedViewMethodsForClasses == null) {
mCapturedViewMethodsForClasses = new HashMap<Class<?>, Method[]>();
}
final HashMap<Class<?>, Method[]> map = mCapturedViewMethodsForClasses;
Method[] methods = map.get(klass);
if (methods != null) {
return methods;
}
final ArrayList<Method> foundMethods = new ArrayList<Method>();
methods = klass.getMethods();
int count = methods.length;
for (int i = 0; i < count; i++) {
final Method method = methods[i];
if (method.getParameterTypes().length == 0 &&
method.isAnnotationPresent(CapturedViewProperty.class) &&
method.getReturnType() != Void.class) {
method.setAccessible(true);
foundMethods.add(method);
}
}
methods = foundMethods.toArray(new Method[foundMethods.size()]);
map.put(klass, methods);
return methods;
}
private static String capturedViewExportMethods(Object obj, Class<?> klass,
String prefix) {
if (obj == null) {
return "null";
}
StringBuilder sb = new StringBuilder();
final Method[] methods = capturedViewGetPropertyMethods(klass);
int count = methods.length;
for (int i = 0; i < count; i++) {
final Method method = methods[i];
try {
Object methodValue = method.invoke(obj, (Object[]) null);
final Class<?> returnType = method.getReturnType();
CapturedViewProperty property = method.getAnnotation(CapturedViewProperty.class);
if (property.retrieveReturn()) {
sb.append(capturedViewExportMethods(methodValue, returnType, method.getName() + "#"));
} else {
sb.append(prefix);
sb.append(method.getName());
sb.append("()=");
if (methodValue != null) {
final String value = methodValue.toString().replace("\n", "\\n");
sb.append(value);
} else {
sb.append("null");
}
sb.append("; ");
}
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
}
}
return sb.toString();
}
private static String capturedViewExportFields(Object obj, Class<?> klass, String prefix) {
if (obj == null) {
return "null";
}
StringBuilder sb = new StringBuilder();
final Field[] fields = capturedViewGetPropertyFields(klass);
int count = fields.length;
for (int i = 0; i < count; i++) {
final Field field = fields[i];
try {
Object fieldValue = field.get(obj);
sb.append(prefix);
sb.append(field.getName());
sb.append("=");
if (fieldValue != null) {
final String value = fieldValue.toString().replace("\n", "\\n");
sb.append(value);
} else {
sb.append("null");
}
sb.append(' ');
} catch (IllegalAccessException e) {
}
}
return sb.toString();
}
public static void dumpCapturedView(String tag, Object view) {
Class<?> klass = view.getClass();
StringBuilder sb = new StringBuilder(klass.getName() + ": ");
sb.append(capturedViewExportFields(view, klass, ""));
sb.append(capturedViewExportMethods(view, klass, ""));
Log.d(tag, sb.toString());
}
public static Object invokeViewMethod(final View view, final Method method,
final Object[] args) {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference<Object> result = new AtomicReference<Object>();
final AtomicReference<Throwable> exception = new AtomicReference<Throwable>();
view.post(new Runnable() {
@Override
public void run() {
try {
result.set(method.invoke(view, args));
} catch (InvocationTargetException e) {
exception.set(e.getCause());
} catch (Exception e) {
exception.set(e);
}
latch.countDown();
}
});
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (exception.get() != null) {
throw new RuntimeException(exception.get());
}
return result.get();
}
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);
}
});
}
public static class SoftwareCanvasProvider implements CanvasProvider {
private Canvas mCanvas;
private Bitmap mBitmap;
private boolean mEnabledHwBitmapsInSwMode;
@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();
}
mEnabledHwBitmapsInSwMode = mCanvas.isHwBitmapsInSwModeEnabled();
mCanvas.setBitmap(mBitmap);
return mCanvas;
}
@Override
public Bitmap createBitmap() {
mCanvas.setBitmap(null);
mCanvas.setHwBitmapsInSwModeEnabled(mEnabledHwBitmapsInSwMode);
return mBitmap;
}
}
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);
}
}
public interface CanvasProvider {
Canvas getCanvas(View view, int width, int height);
Bitmap createBitmap();
}
}