| /* |
| * 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; |
| } |
| } |