content: /*
   * 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.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.TestApi;
  import android.annotation.UnsupportedAppUsage;
  import android.content.Context;
  import android.content.res.Resources;
  import android.graphics.Bitmap;
  import android.graphics.Canvas;
  import android.graphics.HardwareRenderer;
  import android.graphics.Picture;
  import android.graphics.RecordingCanvas;
  import android.graphics.Rect;
  import android.graphics.RenderNode;
  import android.os.Debug;
  import android.os.Handler;
  import android.os.Looper;
  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.ArrayDeque;
  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.Executor;
  import java.util.concurrent.FutureTask;
  import java.util.concurrent.TimeUnit;
  import java.util.concurrent.TimeoutException;
  import java.util.concurrent.atomic.AtomicReference;
  import java.util.concurrent.locks.ReentrantLock;
  import java.util.function.Function;
  
  /**
   * Various debugging/tracing tools related to {@link View} and the view hierarchy.
   */
  public class ViewDebug {
      /**
       * @deprecated This flag is now unused
       */
      @Deprecated
      public static final boolean TRACE_HIERARCHY = false;
  
      /**
       * @deprecated This flag is now unused
       */
      @Deprecated
      public static final boolean TRACE_RECYCLER = false;
  
      /**
       * Enables detailed logging of drag/drop operations.
       * @hide
       */
      public static final boolean DEBUG_DRAG = false;
  
      /**
       * Enables detailed logging of task positioning operations.
       * @hide
       */
      public static final boolean DEBUG_POSITIONING = false;
  
      /**
       * This annotation can be used to mark fields and methods to be dumped by
       * the view server. Only non-void methods with no arguments can be annotated
       * by this annotation.
       */
      @Target({ ElementType.FIELD, ElementType.METHOD })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface ExportedProperty {
          /**
           * When resolveId is true, and if the annotated field/method return value
           * is an int, the value is converted to an Android's resource name.
           *
           * @return true if the property's value must be transformed into an Android
           *         resource name, false otherwise
           */
          boolean resolveId() default false;
  
          /**
           * A mapping can be defined to map int values to specific strings. For
           * instance, View.getVisibility() returns 0, 4 or 8. However, these values
           * actually mean VISIBLE, INVISIBLE and GONE. A mapping can be used to see
           * these human readable values:
           *
           * <pre>
           * {@literal @}ViewDebug.ExportedProperty(mapping = {
           *     {@literal @}ViewDebug.IntToString(from = 0, to = "VISIBLE"),
           *     {@literal @}ViewDebug.IntToString(from = 4, to = "INVISIBLE"),
           *     {@literal @}ViewDebug.IntToString(from = 8, to = "GONE")
           * })
           * public int getVisibility() { ...
           * <pre>
           *
           * @return An array of int to String mappings
           *
           * @see android.view.ViewDebug.IntToString
           */
          IntToString[] mapping() default { };
  
          /**
           * A mapping can be defined to map array indices to specific strings.
           * A mapping can be used to see human readable values for the indices
           * of an array:
           *
           * <pre>
           * {@literal @}ViewDebug.ExportedProperty(indexMapping = {
           *     {@literal @}ViewDebug.IntToString(from = 0, to = "INVALID"),
           *     {@literal @}ViewDebug.IntToString(from = 1, to = "FIRST"),
           *     {@literal @}ViewDebug.IntToString(from = 2, to = "SECOND")
           * })
           * private int[] mElements;
           * <pre>
           *
           * @return An array of int to String mappings
           *
           * @see android.view.ViewDebug.IntToString
           * @see #mapping()
           */
          IntToString[] indexMapping() default { };
  
          /**
           * A flags mapping can be defined to map flags encoded in an integer to
           * specific strings. A mapping can be used to see human readable values
           * for the flags of an integer:
           *
           * <pre>
           * {@literal @}ViewDebug.ExportedProperty(flagMapping = {
           *     {@literal @}ViewDebug.FlagToString(mask = ENABLED_MASK, equals = ENABLED,
           *             name = "ENABLED"),
           *     {@literal @}ViewDebug.FlagToString(mask = ENABLED_MASK, equals = DISABLED,
           *             name = "DISABLED"),
           * })
           * private int mFlags;
           * <pre>
           *
           * A specified String is output when the following is true:
           *
           * @return An array of int to String mappings
           */
          FlagToString[] flagMapping() default { };
  
          /**
           * When deep export is turned on, this property is not dumped. Instead, the
           * properties contained in this property are dumped. Each child property
           * is prefixed with the name of this property.
           *
           * @return true if the properties of this property should be dumped
           *
           * @see #prefix()
           */
          boolean deepExport() default false;
  
          /**
           * The prefix to use on child properties when deep export is enabled
           *
           * @return a prefix as a String
           *
           * @see #deepExport()
           */
          String prefix() default "";
  
          /**
           * Specifies the category the property falls into, such as measurement,
           * layout, drawing, etc.
           *
           * @return the category as String
           */
          String category() default "";
  
          /**
           * Indicates whether or not to format an {@code int} or {@code byte} value as a hex string.
           *
           * @return true if the supported values should be formatted as a hex string.
           */
          boolean formatToHexString() default false;
  
          /**
           * Indicates whether or not the key to value mappings are held in adjacent indices.
           *
           * Note: Applies only to fields and methods that return String[].
           *
           * @return true if the key to value mappings are held in adjacent indices.
           */
          boolean hasAdjacentMapping() default false;
      }
  
      /**
       * Defines a mapping from an int value to a String. Such a mapping can be used
       * in an @ExportedProperty to provide more meaningful values to the end user.
       *
       * @see android.view.ViewDebug.ExportedProperty
       */
      @Target({ ElementType.TYPE })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface IntToString {
          /**
           * The original int value to map to a String.
           *
           * @return An arbitrary int value.
           */
          int from();
  
          /**
           * The String to use in place of the original int value.
           *
           * @return An arbitrary non-null String.
           */
          String to();
      }
  
      /**
       * Defines a mapping from a flag to a String. Such a mapping can be used
       * in an @ExportedProperty to provide more meaningful values to the end user.
       *
       * @see android.view.ViewDebug.ExportedProperty
       */
      @Target({ ElementType.TYPE })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface FlagToString {
          /**
           * The mask to apply to the original value.
           *
           * @return An arbitrary int value.
           */
          int mask();
  
          /**
           * The value to compare to the result of:
           * <code>original value &amp; {@link #mask()}</code>.
           *
           * @return An arbitrary value.
           */
          int equals();
  
          /**
           * The String to use in place of the original int value.
           *
           * @return An arbitrary non-null String.
           */
          String name();
  
          /**
           * Indicates whether to output the flag when the test is true,
           * or false. Defaults to true.
           */
          boolean outputIf() default true;
      }
  
      /**
       * This annotation can be used to mark fields and methods to be dumped when
       * the view is captured. Methods with this annotation must have no arguments
       * and must return a valid type of data.
       */
      @Target({ ElementType.FIELD, ElementType.METHOD })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface CapturedViewProperty {
          /**
           * When retrieveReturn is true, we need to retrieve second level methods
           * e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod()
           * we will set retrieveReturn = true on the annotation of
           * myView.getFirstLevelMethod()
           * @return true if we need the second level methods
           */
          boolean retrieveReturn() default false;
      }
  
      /**
       * Allows a View to inject custom children into HierarchyViewer. For example,
       * WebView uses this to add its internal layer tree as a child to itself
       * @hide
       */
      public interface HierarchyHandler {
          /**
           * Dumps custom children to hierarchy viewer.
           * See ViewDebug.dumpViewWithProperties(Context, View, BufferedWriter, int)
           * for the format
           *
           * An empty implementation should simply do nothing
           *
           * @param out The output writer
           * @param level The indentation level
           */
          public void dumpViewHierarchyWithProperties(BufferedWriter out, int level);
  
          /**
           * Returns a View to enable grabbing screenshots from custom children
           * returned in dumpViewHierarchyWithProperties.
           *
           * @param className The className of the view to find
           * @param hashCode The hashCode of the view to find
           * @return the View to capture from, or null if not found
           */
          public View findHierarchyView(String className, int hashCode);
      }
  
      private static HashMap<Class<?>, Method[]> mCapturedViewMethodsForClasses = null;
      private static HashMap<Class<?>, Field[]> mCapturedViewFieldsForClasses = null;
  
      // 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_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 This enum is now unused
       */
      @Deprecated
      public enum HierarchyTraceType {
          INVALIDATE,
          INVALIDATE_CHILD,
          INVALIDATE_CHILD_IN_PARENT,
          REQUEST_LAYOUT,
          ON_LAYOUT,
          ON_MEASURE,
          DRAW,
          BUILD_CACHE
      }
  
      /**
       * @deprecated This enum is now unused
       */
      @Deprecated
      public enum RecyclerTraceType {
          NEW_VIEW,
          BIND_VIEW,
          RECYCLE_FROM_ACTIVE_HEAP,
          RECYCLE_FROM_SCRAP_HEAP,
          MOVE_TO_SCRAP_HEAP,
          MOVE_FROM_ACTIVE_TO_SCRAP_HEAP
      }
  
      /**
       * Returns the number of instanciated Views.
       *
       * @return The number of Views instanciated in the current process.
       *
       * @hide
       */
      @UnsupportedAppUsage
      public static long getViewInstanceCount() {
          return Debug.countInstancesOfClass(View.class);
      }
  
      /**
       * Returns the number of instanciated ViewAncestors.
       *
       * @return The number of ViewAncestors instanciated in the current process.
       *
       * @hide
       */
      @UnsupportedAppUsage
      public static long getViewRootImplCount() {
          return Debug.countInstancesOfClass(ViewRootImpl.class);
      }
  
      /**
       * @deprecated This method is now unused and invoking it is a no-op
       */
      @Deprecated
      @SuppressWarnings({ "UnusedParameters", "deprecation" })
      public static void trace(View view, RecyclerTraceType type, int... parameters) {
      }
  
      /**
       * @deprecated This method is now unused and invoking it is a no-op
       */
      @Deprecated
      @SuppressWarnings("UnusedParameters")
      public static void startRecyclerTracing(String prefix, View view) {
      }
  
      /**
       * @deprecated This method is now unused and invoking it is a no-op
       */
      @Deprecated
      @SuppressWarnings("UnusedParameters")
      public static void stopRecyclerTracing() {
      }
  
      /**
       * @deprecated This method is now unused and invoking it is a no-op
       */
      @Deprecated
      @SuppressWarnings({ "UnusedParameters", "deprecation" })
      public static void trace(View view, HierarchyTraceType type) {
      }
  
      /**
       * @deprecated This method is now unused and invoking it is a no-op
       */
      @Deprecated
      @SuppressWarnings("UnusedParameters")
      public static void startHierarchyTracing(String prefix, View view) {
      }
  
      /**
       * @deprecated This method is now unused and invoking it is a no-op
       */
      @Deprecated
      public static void stopHierarchyTracing() {
      }
  
      @UnsupportedAppUsage
      static void dispatchCommand(View view, String command, String parameters,
              OutputStream clientStream) throws IOException {
  
          // Paranoid but safe...
          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]);
              }
          }
      }
  
      /** @hide */
      public static View findView(View root, String parameter) {
          // Look by type/hashcode
          if (parameter.indexOf('@') != -1) {
              final String[] ids = parameter.split("@");
              final String className = ids[0];
              final int hashCode = (int) Long.parseLong(ids[1], 16);
  
              View view = root.getRootView();
              if (view instanceof ViewGroup) {
                  return findView((ViewGroup) view, className, hashCode);
              }
          } else {
              // Look by id
              final int id = root.getResources().getIdentifier(parameter, null, null);
              return root.getRootView().findViewById(id);
          }
  
          return null;
      }
  
      private static void invalidate(View root, String parameter) {
          final View view = findView(root, parameter);
          if (view != null) {
              view.postInvalidate();
          }
      }
  
      private static void requestLayout(View root, String parameter) {
          final View view = findView(root, parameter);
          if (view != null) {
              root.post(new Runnable() {
                  public void run() {
                      view.requestLayout();
                  }
              });
          }
      }
  
      private static void profile(View root, OutputStream clientStream, String parameter)
              throws IOException {
  
          final View view = findView(root, parameter);
          BufferedWriter out = null;
          try {
              out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
  
              if (view != null) {
                  profileViewAndChildren(view, out);
              } else {
                  out.write("-1 -1 -1");
                  out.newLine();
              }
              out.write("DONE.");
              out.newLine();
          } catch (Exception e) {
              android.util.Log.w("View", "Problem profiling the view:", e);
          } finally {
              if (out != null) {
                  out.close();
              }
          }
      }
  
      /** @hide */
      public static void profileViewAndChildren(final View view, BufferedWriter out)
              throws IOException {
          RenderNode node = RenderNode.create("ViewDebug", null);
          profileViewAndChildren(view, node, out, true);
      }
  
      private static void profileViewAndChildren(View view, RenderNode node, BufferedWriter out,
              boolean root) throws IOException {
          long durationMeasure =
                  (root || (view.mPrivateFlags & View.PFLAG_MEASURED_DIMENSION_SET) != 0)
                          ? profileViewMeasure(view) : 0;
          long durationLayout =
                  (root || (view.mPrivateFlags & View.PFLAG_LAYOUT_REQUIRED) != 0)
                          ? profileViewLayout(view) : 0;
          long durationDraw =
                  (root || !view.willNotDraw() || (view.mPrivateFlags & View.PFLAG_DRAWN) != 0)
                          ? profileViewDraw(view, node) : 0;
  
          out.write(String.valueOf(durationMeasure));
          out.write(' ');
          out.write(String.valueOf(durationLayout));
          out.write(' ');
          out.write(String.valueOf(durationDraw));
          out.newLine();
          if (view instanceof ViewGroup) {
              ViewGroup group = (ViewGroup) view;
              final int count = group.getChildCount();
              for (int i = 0; i < count; i++) {
                  profileViewAndChildren(group.getChildAt(i), node, out, false);
              }
          }
      }
  
      private static long profileViewMeasure(final View view) {
          return profileViewOperation(view, new ViewOperation() {
              @Override
              public void pre() {
                  forceLayout(view);
              }
  
              private void forceLayout(View view) {
                  view.forceLayout();
                  if (view instanceof ViewGroup) {
                      ViewGroup group = (ViewGroup) view;
                      final int count = group.getChildCount();
                      for (int i = 0; i < count; i++) {
                          forceLayout(group.getChildAt(i));
                      }
                  }
              }
  
              @Override
              public void run() {
                  view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec);
              }
          });
      }
  
      private static long profileViewLayout(View view) {
          return profileViewOperation(view,
                  () -> view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom));
      }
  
      private static long profileViewDraw(View view, RenderNode node) {
          DisplayMetrics dm = view.getResources().getDisplayMetrics();
          if (dm == null) {
              return 0;
          }
  
          if (view.isHardwareAccelerated()) {
              RecordingCanvas canvas = node.beginRecording(dm.widthPixels, dm.heightPixels);
              try {
                  return profileViewOperation(view, () -> view.draw(canvas));
              } finally {
                  node.endRecording();
              }
          } else {
              Bitmap bitmap = Bitmap.createBitmap(
                      dm, dm.widthPixels, dm.heightPixels, Bitmap.Config.RGB_565);
              Canvas canvas = new Canvas(bitmap);
              try {
                  return profileViewOperation(view, () -> view.draw(canvas));
              } finally {
                  canvas.setBitmap(null);
                  bitmap.recycle();
              }
          }
      }
  
      interface ViewOperation {
          default void pre() {}
  
          void run();
      }
  
      private static long profileViewOperation(View view, final ViewOperation operation) {
          final CountDownLatch latch = new CountDownLatch(1);
          final long[] duration = new long[1];
  
          view.post(() -> {
              try {
                  operation.pre();
                  long start = Debug.threadCpuTimeNanos();
                  //noinspection unchecked
                  operation.run();
                  duration[0] = Debug.threadCpuTimeNanos() - start;
              } finally {
                  latch.countDown();
              }
          });
  
          try {
              if (!latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS)) {
                  Log.w("View", "Could not complete the profiling of the view " + view);
                  return -1;
              }
          } catch (InterruptedException e) {
              Log.w("View", "Could not complete the profiling of the view " + view);
              Thread.currentThread().interrupt();
              return -1;
          }
  
          return duration[0];
      }
  
      /** @hide */
      public static void captureLayers(View root, final DataOutputStream clientStream)
              throws IOException {
  
          try {
              Rect outRect = new Rect();
              try {
                  root.mAttachInfo.mSession.getDisplayFrame(root.mAttachInfo.mWindow, outRect);
              } catch (RemoteException e) {
                  // Ignore
              }
  
              clientStream.writeInt(outRect.width());
              clientStream.writeInt(outRect.height());
  
              captureViewLayer(root, clientStream, true);
  
              clientStream.write(2);
          } finally {
              clientStream.close();
          }
      }
  
      private static void captureViewLayer(View view, DataOutputStream clientStream, boolean visible)
              throws IOException {
  
          final boolean localVisible = view.getVisibility() == View.VISIBLE && visible;
  
          if ((view.mPrivateFlags & View.PFLAG_SKIP_DRAW) != View.PFLAG_SKIP_DRAW) {
              final int id = view.getId();
              String name = view.getClass().getSimpleName();
              if (id != View.NO_ID) {
                  name = resolveId(view.getContext(), id).toString();
              }
  
              clientStream.write(1);
              clientStream.writeUTF(name);
              clientStream.writeByte(localVisible ? 1 : 0);
  
              int[] position = new int[2];
              // XXX: Should happen on the UI thread
              view.getLocationInWindow(position);
  
              clientStream.writeInt(position[0]);
              clientStream.writeInt(position[1]);
              clientStream.flush();
  
              Bitmap b = performViewCapture(view, true);
              if (b != null) {
                  ByteArrayOutputStream arrayOut = new ByteArrayOutputStream(b.getWidth() *
                          b.getHeight() * 2);
                  b.compress(Bitmap.CompressFormat.PNG, 100, arrayOut);
                  clientStream.writeInt(arrayOut.size());
                  arrayOut.writeTo(clientStream);
              }
              clientStream.flush();
          }
  
          if (view instanceof ViewGroup) {
              ViewGroup group = (ViewGroup) view;
              int count = group.getChildCount();
  
              for (int i = 0; i < count; i++) {
                  captureViewLayer(group.getChildAt(i), clientStream, localVisible);
              }
          }
  
          if (view.mOverlay != null) {
              ViewGroup overlayContainer = view.getOverlay().mOverlayViewGroup;
              captureViewLayer(overlayContainer, clientStream, localVisible);
          }
      }
  
      private static void outputDisplayList(View root, String parameter) throws IOException {
          final View view = findView(root, parameter);
          view.getViewRootImpl().outputDisplayList(view);
      }
  
      /** @hide */
      public static void outputDisplayList(View root, View target) {
          root.getViewRootImpl().outputDisplayList(target);
      }
  
      private static class PictureCallbackHandler implements AutoCloseable,
              HardwareRenderer.PictureCapturedCallback, Runnable {
          private final HardwareRenderer mRenderer;
          private final Function<Picture, Boolean> mCallback;
          private final Executor mExecutor;
          private final ReentrantLock mLock = new ReentrantLock(false);
          private final ArrayDeque<Picture> mQueue = new ArrayDeque<>(3);
          private boolean mStopListening;
          private Thread mRenderThread;
  
          private PictureCallbackHandler(HardwareRenderer renderer,
                  Function<Picture, Boolean> callback, Executor executor) {
              mRenderer = renderer;
              mCallback = callback;
              mExecutor = executor;
              mRenderer.setPictureCaptureCallback(this);
          }
  
          @Override
          public void close() {
              mLock.lock();
              mStopListening = true;
              mLock.unlock();
              mRenderer.setPictureCaptureCallback(null);
          }
  
          @Override
          public void onPictureCaptured(Picture picture) {
              mLock.lock();
              if (mStopListening) {
                  mLock.unlock();
                  mRenderer.setPictureCaptureCallback(null);
                  return;
              }
              if (mRenderThread == null) {
                  mRenderThread = Thread.currentThread();
              }
              Picture toDestroy = null;
              if (mQueue.size() == 3) {
                  toDestroy = mQueue.removeLast();
              }
              mQueue.add(picture);
              mLock.unlock();
              if (toDestroy == null) {
                  mExecutor.execute(this);
              } else {
                  toDestroy.close();
              }
          }
  
          @Override
          public void run() {
              mLock.lock();
              final Picture picture = mQueue.poll();
              final boolean isStopped = mStopListening;
              mLock.unlock();
              if (Thread.currentThread() == mRenderThread) {
                  close();
                  throw new IllegalStateException(
                          "ViewDebug#startRenderingCommandsCapture must be given an executor that "
                          + "invokes asynchronously");
              }
              if (isStopped) {
                  picture.close();
                  return;
              }
              final boolean keepReceiving = mCallback.apply(picture);
              if (!keepReceiving) {
                  close();
              }
          }
      }
  
      /**
       * Begins capturing the entire rendering commands for the view tree referenced by the given
       * view. The view passed may be any View in the tree as long as it is attached. That is,
       * {@link View#isAttachedToWindow()} must be true.
       *
       * Every time a frame is rendered a Picture will be passed to the given callback via the given
       * executor. As long as the callback returns 'true' it will continue to receive new frames.
       * The system will only invoke the callback at a rate that the callback is able to keep up with.
       * That is, if it takes 48ms for the callback to complete and there is a 60fps animation running
       * then the callback will only receive 33% of the frames produced.
       *
       * This method must be called on the same thread as the View tree.
       *
       * @param tree The View tree to capture the rendering commands.
       * @param callback The callback to invoke on every frame produced. Should return true to
       *                 continue receiving new frames, false to stop capturing.
       * @param executor The executor to invoke the callback on. Recommend using a background thread
       *                 to avoid stalling the UI thread. Must be an asynchronous invoke or an
       *                 exception will be thrown.
       * @return a closeable that can be used to stop capturing. May be invoked on any thread. Note
       * that the callback may continue to receive another frame or two depending on thread timings.
       * Returns null if the capture stream cannot be started, such as if there's no
       * HardwareRenderer for the given view tree.
       * @hide
       * @deprecated use {@link #startRenderingCommandsCapture(View, Executor, Callable)} instead.
       */
      @TestApi
      @Nullable
      @Deprecated
      public static AutoCloseable startRenderingCommandsCapture(View tree, Executor executor,
              Function<Picture, Boolean> callback) {
          final View.AttachInfo attachInfo = tree.mAttachInfo;
          if (attachInfo == null) {
              throw new IllegalArgumentException("Given view isn't attached");
          }
          if (attachInfo.mHandler.getLooper() != Looper.myLooper()) {
              throw new IllegalStateException("Called on the wrong thread."
                      + " Must be called on the thread that owns the given View");
          }
          final HardwareRenderer renderer = attachInfo.mThreadedRenderer;
          if (renderer != null) {
              return new PictureCallbackHandler(renderer, callback, executor);
          }
          return null;
      }
  
      private static class StreamingPictureCallbackHandler implements AutoCloseable,
              HardwareRenderer.PictureCapturedCallback, Runnable {
          private final HardwareRenderer mRenderer;
          private final Callable<OutputStream> mCallback;
          private final Executor mExecutor;
          private final ReentrantLock mLock = new ReentrantLock(false);
          private final ArrayDeque<byte[]> mQueue = new ArrayDeque<>(3);
          private final ByteArrayOutputStream mByteStream = new ByteArrayOutputStream();
          private boolean mStopListening;
          private Thread mRenderThread;
  
          private StreamingPictureCallbackHandler(HardwareRenderer renderer,
                  Callable<OutputStream> callback, Executor executor) {
              mRenderer = renderer;
              mCallback = callback;
              mExecutor = executor;
              mRenderer.setPictureCaptureCallback(this);
          }
  
          @Override
          public void close() {
              mLock.lock();
              mStopListening = true;
              mLock.unlock();
              mRenderer.setPictureCaptureCallback(null);
          }
  
          @Override
          public void onPictureCaptured(Picture picture) {
              mLock.lock();
              if (mStopListening) {
                  mLock.unlock();
                  mRenderer.setPictureCaptureCallback(null);
                  return;
              }
              if (mRenderThread == null) {
                  mRenderThread = Thread.currentThread();
              }
              boolean needsInvoke = true;
              if (mQueue.size() == 3) {
                  mQueue.removeLast();
                  needsInvoke = false;
              }
              picture.writeToStream(mByteStream);
              mQueue.add(mByteStream.toByteArray());
              mByteStream.reset();
              mLock.unlock();
  
              if (needsInvoke) {
                  mExecutor.execute(this);
              }
          }
  
          @Override
          public void run() {
              mLock.lock();
              final byte[] picture = mQueue.poll();
              final boolean isStopped = mStopListening;
              mLock.unlock();
              if (Thread.currentThread() == mRenderThread) {
                  close();
                  throw new IllegalStateException(
                          "ViewDebug#startRenderingCommandsCapture must be given an executor that "
                          + "invokes asynchronously");
              }
              if (isStopped) {
                  return;
              }
              OutputStream stream = null;
              try {
                  stream = mCallback.call();
              } catch (Exception ex) {
                  Log.w("ViewDebug", "Aborting rendering commands capture "
                          + "because callback threw exception", ex);
              }
              if (stream != null) {
                  try {
                      stream.write(picture);
                  } catch (IOException ex) {
                      Log.w("ViewDebug", "Aborting rendering commands capture "
                              + "due to IOException writing to output stream", ex);
                  }
              } else {
                  close();
              }
          }
      }
  
      /**
       * Begins capturing the entire rendering commands for the view tree referenced by the given
       * view. The view passed may be any View in the tree as long as it is attached. That is,
       * {@link View#isAttachedToWindow()} must be true.
       *
       * Every time a frame is rendered the callback will be invoked on the given executor to
       * provide an OutputStream to serialize to. As long as the callback returns a valid
       * OutputStream the capturing will continue. The system will only invoke the callback at a rate
       * that the callback & OutputStream is able to keep up with. That is, if it takes 48ms for the
       * callback & serialization to complete and there is a 60fps animation running
       * then the callback will only receive 33% of the frames produced.
       *
       * This method must be called on the same thread as the View tree.
       *
       * @param tree The View tree to capture the rendering commands.
       * @param callback The callback to invoke on every frame produced. Should return an
       *                 OutputStream to write the data to. Return null to cancel capture. The
       *                 same stream may be returned each time as the serialized data contains
       *                 start & end markers. The callback will not be invoked while a previous
       *                 serialization is being performed, so if a single continuous stream is being
       *                 used it is valid for the callback to write its own metadata to that stream
       *                 in response to callback invocation.
       * @param executor The executor to invoke the callback on. Recommend using a background thread
       *                 to avoid stalling the UI thread. Must be an asynchronous invoke or an
       *                 exception will be thrown.
       * @return a closeable that can be used to stop capturing. May be invoked on any thread. Note
       * that the callback may continue to receive another frame or two depending on thread timings.
       * Returns null if the capture stream cannot be started, such as if there's no
       * HardwareRenderer for the given view tree.
       * @hide
       */
      @TestApi
      @Nullable
      public static AutoCloseable startRenderingCommandsCapture(View tree, Executor executor,
              Callable<OutputStream> callback) {
          final View.AttachInfo attachInfo = tree.mAttachInfo;
          if (attachInfo == null) {
              throw new IllegalArgumentException("Given view isn't attached");
          }
          if (attachInfo.mHandler.getLooper() != Looper.myLooper()) {
              throw new IllegalStateException("Called on the wrong thread."
                      + " Must be called on the thread that owns the given View");
          }
          final HardwareRenderer renderer = attachInfo.mThreadedRenderer;
          if (renderer != null) {
              return new StreamingPictureCallbackHandler(renderer, callback, executor);
          }
          return null;
      }
  
      private static void capture(View root, final OutputStream clientStream, String parameter)
              throws IOException {
  
          final View captureView = findView(root, parameter);
          capture(root, clientStream, captureView);
      }
  
      /** @hide */
      public static void capture(View root, final OutputStream clientStream, View captureView)
              throws IOException {
          Bitmap b = performViewCapture(captureView, false);
  
          if (b == null) {
              Log.w("View", "Failed to create capture bitmap!");
              // Send an empty one so that it doesn't get stuck waiting for
              // something.
              b = Bitmap.createBitmap(root.getResources().getDisplayMetrics(),
                      1, 1, Bitmap.Config.ARGB_8888);
          }
  
          BufferedOutputStream out = null;
          try {
              out = new BufferedOutputStream(clientStream, 32 * 1024);
              b.compress(Bitmap.CompressFormat.PNG, 100, out);
              out.flush();
          } finally {
              if (out != null) {
                  out.close();
              }
              b.recycle();
          }
      }
  
      private static Bitmap performViewCapture(final View captureView, final boolean skipChildren) {
          if (captureView != null) {
              final CountDownLatch latch = new CountDownLatch(1);
              final Bitmap[] cache = new Bitmap[1];
  
              captureView.post(() -> {
                  try {
                      CanvasProvider provider = captureView.isHardwareAccelerated()
                              ? new HardwareCanvasProvider() : new SoftwareCanvasProvider();
                      cache[0] = captureView.createSnapshot(provider, skipChildren);
                  } catch (OutOfMemoryError e) {
                      Log.w("View", "Out of memory for bitmap");
                  } finally {
                      latch.countDown();
                  }
              });
  
              try {
                  latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS);
                  return cache[0];
              } catch (InterruptedException e) {
                  Log.w("View", "Could not complete the capture of the view " + captureView);
                  Thread.currentThread().interrupt();
              }
          }
  
          return null;
      }
  
      /**
       * Dumps the view hierarchy starting from the given view.
       * @deprecated See {@link #dumpv2(View, ByteArrayOutputStream)} below.
       * @hide
       */
      @Deprecated
      @UnsupportedAppUsage
      public static void dump(View root, boolean skipChildren, boolean includeProperties,
              OutputStream clientStream) throws IOException {
          BufferedWriter out = null;
          try {
              out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024);
              View view = root.getRootView();
              if (view instanceof ViewGroup) {
                  ViewGroup group = (ViewGroup) view;
                  dumpViewHierarchy(group.getContext(), group, out, 0,
                          skipChildren, includeProperties);
              }
              out.write("DONE.");
              out.newLine();
          } catch (Exception e) {
              android.util.Log.w("View", "Problem dumping the view:", e);
          } finally {
              if (out != null) {
                  out.close();
              }
          }
      }
  
      /**
       * Dumps the view hierarchy starting from the given view.
       * Rather than using reflection, it uses View's encode method to obtain all the properties.
       * @hide
       */
      public static void dumpv2(@NonNull final View view, @NonNull ByteArrayOutputStream out)
              throws InterruptedException {
          final ViewHierarchyEncoder encoder = new ViewHierarchyEncoder(out);
          final CountDownLatch latch = new CountDownLatch(1);
  
          view.post(new Runnable() {
              @Override
              public void run() {
                  encoder.addProperty("window:left", view.mAttachInfo.mWindowLeft);
                  encoder.addProperty("window:top", view.mAttachInfo.mWindowTop);
                  view.encode(encoder);
                  latch.countDown();
              }
          });
  
          latch.await(2, TimeUnit.SECONDS);
          encoder.endStream();
      }
  
      /**
       * Dumps the theme attributes from the given View.
       * @hide
       */
      public static void dumpTheme(View view, OutputStream clientStream) throws IOException {
          BufferedWriter out = null;
          try {
              out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024);
              String[] attributes = getStyleAttributesDump(view.getContext().getResources(),
                      view.getContext().getTheme());
              if (attributes != null) {
                  for (int i = 0; i < attributes.length; i += 2) {
                      if (attributes[i] != null) {
                          out.write(attributes[i] + "\n");
                          out.write(attributes[i + 1] + "\n");
                      }
                  }
              }
              out.write("DONE.");
              out.newLine();
          } catch (Exception e) {
              android.util.Log.w("View", "Problem dumping View Theme:", e);
          } finally {
              if (out != null) {
                  out.close();
              }
          }
      }
  
      /**
       * Gets the style attributes from the {@link Resources.Theme}. For debugging only.
       *
       * @param resources Resources to resolve attributes from.
       * @param theme Theme to dump.
       * @return a String array containing pairs of adjacent Theme attribute data: name followed by
       * its value.
       *
       * @hide
       */
      private static String[] getStyleAttributesDump(Resources resources, Resources.Theme theme) {
          TypedValue outValue = new TypedValue();
          String nullString = "null";
          int i = 0;
          int[] attributes = theme.getAllAttributes();
          String[] data = new String[attributes.length * 2];
          for (int attributeId : attributes) {
              try {
                  data[i] = resources.getResourceName(attributeId);
                  data[i + 1] = theme.resolveAttribute(attributeId, outValue, true) ?
                          outValue.coerceToString().toString() :  nullString;
                  i += 2;
  
                  // attempt to replace reference data with its name
                  if (outValue.type == TypedValue.TYPE_REFERENCE) {
                      data[i - 1] = resources.getResourceName(outValue.resourceId);
                  }
              } catch (Resources.NotFoundException e) {
                  // ignore resources we can't resolve
              }
          }
          return data;
      }
  
      private static View findView(ViewGroup group, String className, int hashCode) {
          if (isRequestedView(group, className, hashCode)) {
              return group;
          }
  
          final int count = group.getChildCount();
          for (int i = 0; i < count; i++) {
              final View view = group.getChildAt(i);
              if (view instanceof ViewGroup) {
                  final View found = findView((ViewGroup) view, className, hashCode);
                  if (found != null) {
                      return found;
                  }
              } else if (isRequestedView(view, className, hashCode)) {
                  return view;
              }
              if (view.mOverlay != null) {
                  final View found = findView((ViewGroup) view.mOverlay.mOverlayViewGroup,
                          className, hashCode);
                  if (found != null) {
                      return found;
                  }
              }
              if (view instanceof HierarchyHandler) {
                  final View found = ((HierarchyHandler)view)
                          .findHierarchyView(className, hashCode);
                  if (found != null) {
                      return found;
                  }
              }
          }
          return null;
      }
  
      private static boolean isRequestedView(View view, String className, int hashCode) {
          if (view.hashCode() == hashCode) {
              String viewClassName = view.getClass().getName();
              if (className.equals("ViewOverlay")) {
                  return viewClassName.equals("android.view.ViewOverlay$OverlayViewGroup");
              } else {
                  return className.equals(viewClassName);
              }
          }
          return false;
      }
  
      private static void dumpViewHierarchy(Context context, ViewGroup group,
              BufferedWriter out, int level, boolean skipChildren, boolean includeProperties) {
          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) {
                // Fields which can't be resolved have a null type.
                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) {
              // Ensure the method return and parameter types can be resolved.
              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);
          // Try to use the handler provided by the view
          Handler handler = view.getHandler();
          // Fall back on using the main thread
          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) {
                  // Call get again
              } 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];
              //noinspection EmptyCatchBlock
              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];
  
              //noinspection EmptyCatchBlock
              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);
              }
          }
      }
  
      /**
       * Converts an integer from a field that is mapped with {@link IntToString} to its string
       * representation.
       *
       * @param clazz The class the field is defined on.
       * @param field The field on which the {@link ExportedProperty} is defined on.
       * @param integer The value to convert.
       * @return The value converted into its string representation.
       * @hide
       */
      public static String intToString(Class<?> clazz, String field, int integer) {
          final IntToString[] mapping = getMapping(clazz, field);
          if (mapping == null) {
              return Integer.toString(integer);
          }
          final int count = mapping.length;
          for (int j = 0; j < count; j++) {
              final IntToString map = mapping[j];
              if (map.from() == integer) {
                  return map.to();
              }
          }
          return Integer.toString(integer);
      }
  
      /**
       * Converts a set of flags from a field that is mapped with {@link FlagToString} to its string
       * representation.
       *
       * @param clazz The class the field is defined on.
       * @param field The field on which the {@link ExportedProperty} is defined on.
       * @param flags The flags to convert.
       * @return The flags converted into their string representations.
       * @hide
       */
      public static String flagsToString(Class<?> clazz, String field, int flags) {
          final FlagToString[] mapping = getFlagMapping(clazz, field);
          if (mapping == null) {
              return Integer.toHexString(flags);
          }
          final StringBuilder result = new StringBuilder();
          final int count = mapping.length;
          for (int j = 0; j < count; j++) {
              final FlagToString flagMapping = mapping[j];
              final boolean ifTrue = flagMapping.outputIf();
              final int maskResult = flags & flagMapping.mask();
              final boolean test = maskResult == flagMapping.equals();
              if (test && ifTrue) {
                  final String name = flagMapping.name();
                  result.append(name).append(' ');
              }
          }
          if (result.length() > 0) {
              result.deleteCharAt(result.length() - 1);
          }
          return result.toString();
      }
  
      private static FlagToString[] getFlagMapping(Class<?> clazz, String field) {
          try {
              return clazz.getDeclaredField(field).getAnnotation(ExportedProperty.class)
                      .flagMapping();
          } catch (NoSuchFieldException e) {
              return null;
          }
      }
  
      private static IntToString[] getMapping(Class<?> clazz, String field) {
          try {
              return clazz.getDeclaredField(field).getAnnotation(ExportedProperty.class).mapping();
          } catch (NoSuchFieldException e) {
              return null;
          }
      }
  
      private static void exportUnrolledArray(Context context, BufferedWriter out,
              ExportedProperty property, int[] array, String prefix, String suffix)
              throws IOException {
  
          final IntToString[] indexMapping = property.indexMapping();
          final boolean hasIndexMapping = indexMapping.length > 0;
  
          final IntToString[] mapping = property.mapping();
          final boolean hasMapping = mapping.length > 0;
  
          final boolean resolveId = property.resolveId() && context != null;
          final int valuesCount = array.length;
  
          for (int j = 0; j < valuesCount; j++) {
              String name;
              String value = null;
  
              final int intValue = array[j];
  
              name = String.valueOf(j);
              if (hasIndexMapping) {
                  int mappingCount = indexMapping.length;
                  for (int k = 0; k < mappingCount; k++) {
                      final IntToString mapped = indexMapping[k];
                      if (mapped.from() == j) {
                          name = mapped.to();
                          break;
                      }
                  }
              }
  
              if (hasMapping) {
                  int mappingCount = mapping.length;
                  for (int k = 0; k < mappingCount; k++) {
                      final IntToString mapped = mapping[k];
                      if (mapped.from() == intValue) {
                          value = mapped.to();
                          break;
                      }
                  }
              }
  
              if (resolveId) {
                  if (value == null) value = (String) resolveId(context, intValue);
              } else {
                  value = String.valueOf(intValue);
              }
  
              writeEntry(out, prefix, name, suffix, value);
          }
      }
  
      static Object resolveId(Context context, int id) {
          Object fieldValue;
          final Resources resources = context.getResources();
          if (id >= 0) {
              try {
                  fieldValue = resources.getResourceTypeName(id) + '/' +
                          resources.getResourceEntryName(id);
              } catch (Resources.NotFoundException e) {
                  fieldValue = "id/" + formatIntToHexString(id);
              }
          } else {
              fieldValue = "NO_ID";
          }
          return fieldValue;
      }
  
      private static void writeValue(BufferedWriter out, Object value) throws IOException {
          if (value != null) {
              String output = "[EXCEPTION]";
              try {
                  output = value.toString().replace("\n", "\\n");
              } finally {
                  out.write(String.valueOf(output.length()));
                  out.write(",");
                  out.write(output);
              }
          } else {
              out.write("4,null");
          }
      }
  
      private static 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()) {
                      //we are interested in the second level data only
                      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) {
                  //Exception IllegalAccess, it is OK here
                  //we simply ignore this method
              } catch (InvocationTargetException e) {
                  //Exception InvocationTarget, it is OK here
                  //we simply ignore this method
              }
          }
          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) {
                  //Exception IllegalAccess, it is OK here
                  //we simply ignore this field
              }
          }
          return sb.toString();
      }
  
      /**
       * Dump view info for id based instrument test generation
       * (and possibly further data analysis). The results are dumped
       * to the log.
       * @param tag for log
       * @param view for dump
       */
      public static void dumpCapturedView(String tag, Object view) {
          Class<?> klass = view.getClass();
          StringBuilder sb = new StringBuilder(klass.getName() + ": ");
          sb.append(capturedViewExportFields(view, klass, ""));
          sb.append(capturedViewExportMethods(view, klass, ""));
          Log.d(tag, sb.toString());
      }
  
      /**
       * Invoke a particular method on given view.
       * The given method is always invoked on the UI thread. The caller thread will stall until the
       * method invocation is complete. Returns an object equal to the result of the method
       * invocation, null if the method is declared to return void
       * @throws Exception if the method invocation caused any exception
       * @hide
       */
      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();
      }
  
      /**
       * @hide
       */
      public static void setLayoutParameter(final View view, final String param, final int value)
              throws NoSuchFieldException, IllegalAccessException {
          final ViewGroup.LayoutParams p = view.getLayoutParams();
          final Field f = p.getClass().getField(param);
          if (f.getType() != int.class) {
              throw new RuntimeException("Only integer layout parameters can be set. Field "
                      + param + " is of type " + f.getType().getSimpleName());
          }
  
          f.set(p, Integer.valueOf(value));
  
          view.post(new Runnable() {
              @Override
              public void run() {
                  view.setLayoutParams(p);
              }
          });
      }
  
      /**
       * @hide
       */
      public static class SoftwareCanvasProvider implements CanvasProvider {
  
          private Canvas mCanvas;
          private Bitmap mBitmap;
          private boolean 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;
          }
      }
  
      /**
       * @hide
       */
      public static class HardwareCanvasProvider implements CanvasProvider {
          private Picture mPicture;
  
          @Override
          public Canvas getCanvas(View view, int width, int height) {
              mPicture = new Picture();
              return mPicture.beginRecording(width, height);
          }
  
          @Override
          public Bitmap createBitmap() {
              mPicture.endRecording();
              return Bitmap.createBitmap(mPicture);
          }
      }
  
      /**
       * @hide
       */
      public interface CanvasProvider {
  
          /**
           * Returns a canvas which can be used to draw {@param view}
           */
          Canvas getCanvas(View view, int width, int height);
  
          /**
           * Creates a bitmap from previously returned canvas
           * @return
           */
          Bitmap createBitmap();
      }
  }
  