diff options
Diffstat (limited to 'core/java/android/view/ViewDebug.java')
-rw-r--r-- | core/java/android/view/ViewDebug.java | 927 |
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; + } +} |