summaryrefslogtreecommitdiffstats
path: root/core/java/android/view/ViewDebug.java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/view/ViewDebug.java')
-rw-r--r--core/java/android/view/ViewDebug.java927
1 files changed, 927 insertions, 0 deletions
diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java
new file mode 100644
index 0000000..1bf46b4
--- /dev/null
+++ b/core/java/android/view/ViewDebug.java
@@ -0,0 +1,927 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.util.Log;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+
+import java.io.File;
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.FileOutputStream;
+import java.io.DataOutputStream;
+import java.io.OutputStreamWriter;
+import java.io.BufferedOutputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.LinkedList;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Various debugging/tracing tools related to {@link View} and the view hierarchy.
+ */
+public class ViewDebug {
+ /**
+ * Enables or disables view hierarchy tracing. Any invoker of
+ * {@link #trace(View, android.view.ViewDebug.HierarchyTraceType)} should first
+ * check that this value is set to true as not to affect performance.
+ */
+ public static final boolean TRACE_HIERARCHY = false;
+
+ /**
+ * Enables or disables view recycler tracing. Any invoker of
+ * {@link #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])} should first
+ * check that this value is set to true as not to affect performance.
+ */
+ public static final boolean TRACE_RECYCLER = 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>
+ * @ViewDebug.ExportedProperty(mapping = {
+ * @ViewDebug.IntToString(from = 0, to = "VISIBLE"),
+ * @ViewDebug.IntToString(from = 4, to = "INVISIBLE"),
+ * @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 { };
+
+ /**
+ * 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 "";
+ }
+
+ /**
+ * Defines a mapping from an int value to a String. Such a mapping can be used
+ * in a @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();
+ }
+
+ // Maximum delay in ms after which we stop trying to capture a View's drawing
+ 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_INVALIDATE = "INVALIDATE";
+ private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT";
+
+ private static HashMap<Class<?>, Field[]> sFieldsForClasses;
+ private static HashMap<Class<?>, Method[]> sMethodsForClasses;
+
+ /**
+ * Defines the type of hierarhcy trace to output to the hierarchy traces file.
+ */
+ public enum HierarchyTraceType {
+ INVALIDATE,
+ INVALIDATE_CHILD,
+ INVALIDATE_CHILD_IN_PARENT,
+ REQUEST_LAYOUT,
+ ON_LAYOUT,
+ ON_MEASURE,
+ DRAW,
+ BUILD_CACHE
+ }
+
+ private static BufferedWriter sHierarchyTraces;
+ private static ViewRoot sHierarhcyRoot;
+ private static String sHierarchyTracePrefix;
+
+ /**
+ * Defines the type of recycler trace to output to the recycler traces file.
+ */
+ public enum RecyclerTraceType {
+ NEW_VIEW,
+ BIND_VIEW,
+ RECYCLE_FROM_ACTIVE_HEAP,
+ RECYCLE_FROM_SCRAP_HEAP,
+ MOVE_TO_ACTIVE_HEAP,
+ MOVE_TO_SCRAP_HEAP,
+ MOVE_FROM_ACTIVE_TO_SCRAP_HEAP
+ }
+
+ private static class RecyclerTrace {
+ public int view;
+ public RecyclerTraceType type;
+ public int position;
+ public int indexOnScreen;
+ }
+
+ private static View sRecyclerOwnerView;
+ private static List<View> sRecyclerViews;
+ private static List<RecyclerTrace> sRecyclerTraces;
+ private static String sRecyclerTracePrefix;
+
+ /**
+ * Returns the number of instanciated Views.
+ *
+ * @return The number of Views instanciated in the current process.
+ *
+ * @hide
+ */
+ public static long getViewInstanceCount() {
+ return View.sInstanceCount;
+ }
+
+ /**
+ * Returns the number of instanciated ViewRoots.
+ *
+ * @return The number of ViewRoots instanciated in the current process.
+ *
+ * @hide
+ */
+ public static long getViewRootInstanceCount() {
+ return ViewRoot.getInstanceCount();
+ }
+
+ /**
+ * Outputs a trace to the currently opened recycler traces. The trace records the type of
+ * recycler action performed on the supplied view as well as a number of parameters.
+ *
+ * @param view the view to trace
+ * @param type the type of the trace
+ * @param parameters parameters depending on the type of the trace
+ */
+ public static void trace(View view, RecyclerTraceType type, int... parameters) {
+ if (sRecyclerOwnerView == null || sRecyclerViews == null) {
+ return;
+ }
+
+ if (!sRecyclerViews.contains(view)) {
+ sRecyclerViews.add(view);
+ }
+
+ final int index = sRecyclerViews.indexOf(view);
+
+ RecyclerTrace trace = new RecyclerTrace();
+ trace.view = index;
+ trace.type = type;
+ trace.position = parameters[0];
+ trace.indexOnScreen = parameters[1];
+
+ sRecyclerTraces.add(trace);
+ }
+
+ /**
+ * Starts tracing the view recycler of the specified view. The trace is identified by a prefix,
+ * used to build the traces files names: <code>/tmp/view-recycler/PREFIX.traces</code> and
+ * <code>/tmp/view-recycler/PREFIX.recycler</code>.
+ *
+ * Only one view recycler can be traced at the same time. After calling this method, any
+ * other invocation will result in a <code>IllegalStateException</code> unless
+ * {@link #stopRecyclerTracing()} is invoked before.
+ *
+ * Traces files are created only after {@link #stopRecyclerTracing()} is invoked.
+ *
+ * This method will return immediately if TRACE_RECYCLER is false.
+ *
+ * @param prefix the traces files name prefix
+ * @param view the view whose recycler must be traced
+ *
+ * @see #stopRecyclerTracing()
+ * @see #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])
+ */
+ public static void startRecyclerTracing(String prefix, View view) {
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (!TRACE_RECYCLER) {
+ return;
+ }
+
+ if (sRecyclerOwnerView != null) {
+ throw new IllegalStateException("You must call stopRecyclerTracing() before running" +
+ " a new trace!");
+ }
+
+ sRecyclerTracePrefix = prefix;
+ sRecyclerOwnerView = view;
+ sRecyclerViews = new ArrayList<View>();
+ sRecyclerTraces = new LinkedList<RecyclerTrace>();
+ }
+
+ /**
+ * Stops the current view recycer tracing.
+ *
+ * Calling this method creates the file <code>/tmp/view-recycler/PREFIX.traces</code>
+ * containing all the traces (or method calls) relative to the specified view's recycler.
+ *
+ * Calling this method creates the file <code>/tmp/view-recycler/PREFIX.recycler</code>
+ * containing all of the views used by the recycler of the view supplied to
+ * {@link #startRecyclerTracing(String, View)}.
+ *
+ * This method will return immediately if TRACE_RECYCLER is false.
+ *
+ * @see #startRecyclerTracing(String, View)
+ * @see #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])
+ */
+ public static void stopRecyclerTracing() {
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (!TRACE_RECYCLER) {
+ return;
+ }
+
+ if (sRecyclerOwnerView == null || sRecyclerViews == null) {
+ throw new IllegalStateException("You must call startRecyclerTracing() before" +
+ " stopRecyclerTracing()!");
+ }
+
+ File recyclerDump = new File("/tmp/view-recycler/");
+ recyclerDump.mkdirs();
+
+ recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".recycler");
+ try {
+ final BufferedWriter out = new BufferedWriter(new FileWriter(recyclerDump), 8 * 1024);
+
+ for (View view : sRecyclerViews) {
+ final String name = view.getClass().getName();
+ out.write(name);
+ out.newLine();
+ }
+
+ out.close();
+ } catch (IOException e) {
+ Log.e("View", "Could not dump recycler content");
+ return;
+ }
+
+ recyclerDump = new File("/tmp/view-recycler/");
+ recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".traces");
+ try {
+ final FileOutputStream file = new FileOutputStream(recyclerDump);
+ final DataOutputStream out = new DataOutputStream(file);
+
+ for (RecyclerTrace trace : sRecyclerTraces) {
+ out.writeInt(trace.view);
+ out.writeInt(trace.type.ordinal());
+ out.writeInt(trace.position);
+ out.writeInt(trace.indexOnScreen);
+ out.flush();
+ }
+
+ out.close();
+ } catch (IOException e) {
+ Log.e("View", "Could not dump recycler traces");
+ return;
+ }
+
+ sRecyclerViews.clear();
+ sRecyclerViews = null;
+
+ sRecyclerTraces.clear();
+ sRecyclerTraces = null;
+
+ sRecyclerOwnerView = null;
+ }
+
+ /**
+ * Outputs a trace to the currently opened traces file. The trace contains the class name
+ * and instance's hashcode of the specified view as well as the supplied trace type.
+ *
+ * @param view the view to trace
+ * @param type the type of the trace
+ */
+ public static void trace(View view, HierarchyTraceType type) {
+ if (sHierarchyTraces == null) {
+ return;
+ }
+
+ try {
+ sHierarchyTraces.write(type.name());
+ sHierarchyTraces.write(' ');
+ sHierarchyTraces.write(view.getClass().getName());
+ sHierarchyTraces.write('@');
+ sHierarchyTraces.write(Integer.toHexString(view.hashCode()));
+ sHierarchyTraces.newLine();
+ } catch (IOException e) {
+ Log.w("View", "Error while dumping trace of type " + type + " for view " + view);
+ }
+ }
+
+ /**
+ * Starts tracing the view hierarchy of the specified view. The trace is identified by a prefix,
+ * used to build the traces files names: <code>/tmp/view-hierarchy/PREFIX.traces</code> and
+ * <code>/tmp/view-hierarchy/PREFIX.tree</code>.
+ *
+ * Only one view hierarchy can be traced at the same time. After calling this method, any
+ * other invocation will result in a <code>IllegalStateException</code> unless
+ * {@link #stopHierarchyTracing()} is invoked before.
+ *
+ * Calling this method creates the file <code>/tmp/view-hierarchy/PREFIX.traces</code>
+ * containing all the traces (or method calls) relative to the specified view's hierarchy.
+ *
+ * This method will return immediately if TRACE_HIERARCHY is false.
+ *
+ * @param prefix the traces files name prefix
+ * @param view the view whose hierarchy must be traced
+ *
+ * @see #stopHierarchyTracing()
+ * @see #trace(View, android.view.ViewDebug.HierarchyTraceType)
+ */
+ public static void startHierarchyTracing(String prefix, View view) {
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (!TRACE_HIERARCHY) {
+ return;
+ }
+
+ if (sHierarhcyRoot != null) {
+ throw new IllegalStateException("You must call stopHierarchyTracing() before running" +
+ " a new trace!");
+ }
+
+ File hierarchyDump = new File("/tmp/view-hierarchy/");
+ hierarchyDump.mkdirs();
+
+ hierarchyDump = new File(hierarchyDump, prefix + ".traces");
+ sHierarchyTracePrefix = prefix;
+
+ try {
+ sHierarchyTraces = new BufferedWriter(new FileWriter(hierarchyDump), 8 * 1024);
+ } catch (IOException e) {
+ Log.e("View", "Could not dump view hierarchy");
+ return;
+ }
+
+ sHierarhcyRoot = (ViewRoot) view.getRootView().getParent();
+ }
+
+ /**
+ * Stops the current view hierarchy tracing. This method closes the file
+ * <code>/tmp/view-hierarchy/PREFIX.traces</code>.
+ *
+ * Calling this method creates the file <code>/tmp/view-hierarchy/PREFIX.tree</code> containing
+ * the view hierarchy of the view supplied to {@link #startHierarchyTracing(String, View)}.
+ *
+ * This method will return immediately if TRACE_HIERARCHY is false.
+ *
+ * @see #startHierarchyTracing(String, View)
+ * @see #trace(View, android.view.ViewDebug.HierarchyTraceType)
+ */
+ public static void stopHierarchyTracing() {
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (!TRACE_HIERARCHY) {
+ return;
+ }
+
+ if (sHierarhcyRoot == null || sHierarchyTraces == null) {
+ throw new IllegalStateException("You must call startHierarchyTracing() before" +
+ " stopHierarchyTracing()!");
+ }
+
+ try {
+ sHierarchyTraces.close();
+ } catch (IOException e) {
+ Log.e("View", "Could not write view traces");
+ }
+ sHierarchyTraces = null;
+
+ File hierarchyDump = new File("/tmp/view-hierarchy/");
+ hierarchyDump.mkdirs();
+ hierarchyDump = new File(hierarchyDump, sHierarchyTracePrefix + ".tree");
+
+ BufferedWriter out;
+ try {
+ out = new BufferedWriter(new FileWriter(hierarchyDump), 8 * 1024);
+ } catch (IOException e) {
+ Log.e("View", "Could not dump view hierarchy");
+ return;
+ }
+
+ View view = sHierarhcyRoot.getView();
+ if (view instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) view;
+ dumpViewHierarchy(group, out, 0);
+ try {
+ out.close();
+ } catch (IOException e) {
+ Log.e("View", "Could not dump view hierarchy");
+ }
+ }
+
+ sHierarhcyRoot = null;
+ }
+
+ static void dispatchCommand(View view, String command, String parameters,
+ OutputStream clientStream) throws IOException {
+
+ // Paranoid but safe...
+ view = view.getRootView();
+
+ if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) {
+ dump(view, clientStream);
+ } else {
+ final String[] params = parameters.split(" ");
+ if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) {
+ capture(view, clientStream, 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]);
+ }
+ }
+ }
+
+ private static View findViewByHashCode(View root, String parameter) {
+ final String[] ids = parameter.split("@");
+ final String className = ids[0];
+ final int hashCode = Integer.parseInt(ids[1], 16);
+
+ View view = root.getRootView();
+ if (view instanceof ViewGroup) {
+ return findView((ViewGroup) view, className, hashCode);
+ }
+
+ return null;
+ }
+
+ private static void invalidate(View root, String parameter) {
+ final View view = findViewByHashCode(root, parameter);
+ if (view != null) {
+ view.postInvalidate();
+ }
+ }
+
+ private static void requestLayout(View root, String parameter) {
+ final View view = findViewByHashCode(root, parameter);
+ if (view != null) {
+ root.post(new Runnable() {
+ public void run() {
+ view.requestLayout();
+ }
+ });
+ }
+ }
+
+ private static void capture(View root, final OutputStream clientStream, String parameter)
+ throws IOException {
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final View captureView = findViewByHashCode(root, parameter);
+
+ if (captureView != null) {
+ final Bitmap[] cache = new Bitmap[1];
+
+ final boolean hasCache = captureView.isDrawingCacheEnabled();
+ final boolean willNotCache = captureView.willNotCacheDrawing();
+
+ if (willNotCache) {
+ captureView.setWillNotCacheDrawing(false);
+ }
+
+ root.post(new Runnable() {
+ public void run() {
+ try {
+ if (!hasCache) {
+ captureView.buildDrawingCache();
+ }
+
+ cache[0] = captureView.getDrawingCache();
+ } finally {
+ latch.countDown();
+ }
+ }
+ });
+
+ try {
+ latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ if (cache[0] != null) {
+ BufferedOutputStream out = null;
+ try {
+ out = new BufferedOutputStream(clientStream, 32 * 1024);
+ cache[0].compress(Bitmap.CompressFormat.PNG, 100, out);
+ out.flush();
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ }
+ }
+ } catch (InterruptedException e) {
+ Log.w("View", "Could not complete the capture of the view " + captureView);
+ } finally {
+ if (willNotCache) {
+ captureView.setWillNotCacheDrawing(true);
+ }
+ if (!hasCache) {
+ captureView.destroyDrawingCache();
+ }
+ }
+ }
+ }
+
+ private static void dump(View root, OutputStream clientStream) throws IOException {
+ BufferedWriter out = null;
+ try {
+ out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
+ View view = root.getRootView();
+ if (view instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) view;
+ dumpViewHierarchyWithProperties(group, out, 0);
+ }
+ out.write("DONE.");
+ out.newLine();
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ }
+ }
+
+ 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;
+ }
+ }
+
+ return null;
+ }
+
+ private static boolean isRequestedView(View view, String className, int hashCode) {
+ return view.getClass().getName().equals(className) && view.hashCode() == hashCode;
+ }
+
+ private static void dumpViewHierarchyWithProperties(ViewGroup group,
+ BufferedWriter out, int level) {
+ if (!dumpViewWithProperties(group, out, level)) {
+ return;
+ }
+
+ final int count = group.getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View view = group.getChildAt(i);
+ if (view instanceof ViewGroup) {
+ dumpViewHierarchyWithProperties((ViewGroup) view, out, level + 1);
+ } else {
+ dumpViewWithProperties(view, out, level + 1);
+ }
+ }
+ }
+
+ private static boolean dumpViewWithProperties(View view, BufferedWriter out, int level) {
+ try {
+ for (int i = 0; i < level; i++) {
+ out.write(' ');
+ }
+ out.write(view.getClass().getName());
+ out.write('@');
+ out.write(Integer.toHexString(view.hashCode()));
+ out.write(' ');
+ dumpViewProperties(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[]>();
+ }
+ final HashMap<Class<?>, Field[]> map = sFieldsForClasses;
+
+ Field[] fields = map.get(klass);
+ if (fields != null) {
+ return fields;
+ }
+
+ final ArrayList<Field> foundFields = new ArrayList<Field>();
+ fields = klass.getDeclaredFields();
+
+ int count = fields.length;
+ for (int i = 0; i < count; i++) {
+ final Field field = fields[i];
+ if (field.isAnnotationPresent(ExportedProperty.class)) {
+ field.setAccessible(true);
+ foundFields.add(field);
+ }
+ }
+
+ fields = foundFields.toArray(new Field[foundFields.size()]);
+ map.put(klass, fields);
+
+ return fields;
+ }
+
+ private static Method[] getExportedPropertyMethods(Class<?> klass) {
+ if (sMethodsForClasses == null) {
+ sMethodsForClasses = new HashMap<Class<?>, Method[]>();
+ }
+ final HashMap<Class<?>, Method[]> map = sMethodsForClasses;
+
+ Method[] methods = map.get(klass);
+ if (methods != null) {
+ return methods;
+ }
+
+ final ArrayList<Method> foundMethods = new ArrayList<Method>();
+ methods = klass.getDeclaredMethods();
+
+ int count = methods.length;
+ for (int i = 0; i < count; i++) {
+ final Method method = methods[i];
+ if (method.getParameterTypes().length == 0 &&
+ method.isAnnotationPresent(ExportedProperty.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 void dumpViewProperties(Object view, BufferedWriter out) throws IOException {
+ dumpViewProperties(view, out, "");
+ }
+
+ private static void dumpViewProperties(Object view, BufferedWriter out, String prefix)
+ throws IOException {
+ Class<?> klass = view.getClass();
+
+ do {
+ exportFields(view, out, klass, prefix);
+ exportMethods(view, out, klass, prefix);
+ klass = klass.getSuperclass();
+ } while (klass != Object.class);
+ }
+
+ private static void exportMethods(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];
+ //noinspection EmptyCatchBlock
+ try {
+ // TODO: This should happen on the UI thread
+ Object methodValue = method.invoke(view, (Object[]) null);
+ final Class<?> returnType = method.getReturnType();
+
+ if (returnType == int.class) {
+ ExportedProperty property = method.getAnnotation(ExportedProperty.class);
+ if (property.resolveId() && view instanceof View) {
+ final Resources resources = ((View) view).getContext().getResources();
+ final int id = (Integer) methodValue;
+ if (id >= 0) {
+ methodValue = resources.getResourceTypeName(id) + '/' +
+ resources.getResourceEntryName(id);
+ } else {
+ methodValue = "NO_ID";
+ }
+ } else {
+ 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.isPrimitive()) {
+ ExportedProperty property = method.getAnnotation(ExportedProperty.class);
+ if (property.deepExport()) {
+ dumpViewProperties(methodValue, out, prefix + property.prefix());
+ continue;
+ }
+ }
+
+ out.write(prefix);
+ out.write(method.getName());
+ out.write("()=");
+
+ if (methodValue != null) {
+ final String value = methodValue.toString().replace("\n", "\\n");
+ out.write(String.valueOf(value.length()));
+ out.write(",");
+ out.write(value);
+ } else {
+ out.write("4,null");
+ }
+
+ out.write(' ');
+ } catch (IllegalAccessException e) {
+ } catch (InvocationTargetException e) {
+ }
+ }
+ }
+
+ private static void exportFields(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];
+
+ //noinspection EmptyCatchBlock
+ try {
+ Object fieldValue = null;
+ final Class<?> type = field.getType();
+
+ if (type == int.class) {
+ ExportedProperty property = field.getAnnotation(ExportedProperty.class);
+ if (property.resolveId() && view instanceof View) {
+ final Resources resources = ((View) view).getContext().getResources();
+ final int id = field.getInt(view);
+ if (id >= 0) {
+ fieldValue = resources.getResourceTypeName(id) + '/' +
+ resources.getResourceEntryName(id);
+ } else {
+ fieldValue = "NO_ID";
+ }
+ } else {
+ 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;
+ }
+ }
+ }
+ } else if (!type.isPrimitive()) {
+ ExportedProperty property = field.getAnnotation(ExportedProperty.class);
+ if (property.deepExport()) {
+ dumpViewProperties(field.get(view), out, prefix + property.prefix());
+ continue;
+ }
+ }
+
+ if (fieldValue == null) {
+ fieldValue = field.get(view);
+ }
+
+ out.write(prefix);
+ out.write(field.getName());
+ out.write('=');
+
+ if (fieldValue != null) {
+ final String value = fieldValue.toString().replace("\n", "\\n");
+ out.write(String.valueOf(value.length()));
+ out.write(",");
+ out.write(value);
+ } else {
+ out.write("4,null");
+ }
+
+ out.write(' ');
+ } catch (IllegalAccessException e) {
+ }
+ }
+ }
+
+ private static void dumpViewHierarchy(ViewGroup group, BufferedWriter out, int level) {
+ if (!dumpView(group, out, level)) {
+ return;
+ }
+
+ final int count = group.getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View view = group.getChildAt(i);
+ if (view instanceof ViewGroup) {
+ dumpViewHierarchy((ViewGroup) view, out, level + 1);
+ } else {
+ dumpView(view, out, level + 1);
+ }
+ }
+ }
+
+ private static boolean dumpView(Object view, BufferedWriter out, int level) {
+ try {
+ for (int i = 0; i < level; i++) {
+ out.write(' ');
+ }
+ out.write(view.getClass().getName());
+ out.write('@');
+ out.write(Integer.toHexString(view.hashCode()));
+ out.newLine();
+ } catch (IOException e) {
+ Log.w("View", "Error while dumping hierarchy tree");
+ return false;
+ }
+ return true;
+ }
+}