/* * 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, softwareViewDebug * 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 static com.android.internal.util.Preconditions.checkArgument; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.compat.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.Build; import android.os.Debug; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Base64; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import com.android.internal.annotations.VisibleForTesting; import libcore.util.HexEncoding; 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.Annotation; 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.Member; import java.lang.reflect.Method; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayDeque; import java.util.Arrays; import java.util.HashMap; import java.util.concurrent.Callable; 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.locks.ReentrantLock; import java.util.function.Function; import java.util.stream.Stream; /** * Various debugging/tracing tools related to {@link View} and the view hierarchy. */ public class ViewDebug { private static final String TAG = "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: * *
* {@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() { ... ** * @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: * ** {@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; ** * @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: * ** {@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; ** * 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: *original value & {@link #mask()}
. * * @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 abstract static class PropertyInfo{ public final R member; public final T property; public final String name; public final Class> returnType; public String entrySuffix = ""; public String valueSuffix = ""; PropertyInfo(Class property, R member, Class> returnType) { this.member = member; this.name = member.getName(); this.property = member.getAnnotation(property); this.returnType = returnType; } public abstract Object invoke(Object target) throws Exception; static PropertyInfo forMethod(Method method, Class property) { // Ensure the method return and parameter types can be resolved. try { if ((method.getReturnType() == Void.class) || (method.getParameterTypes().length != 0)) { return null; } } catch (NoClassDefFoundError e) { return null; } if (!method.isAnnotationPresent(property)) { return null; } method.setAccessible(true); PropertyInfo info = new MethodPI(method, property); info.entrySuffix = "()"; info.valueSuffix = ";"; return info; } static PropertyInfo forField(Field field, Class property) { if (!field.isAnnotationPresent(property)) { return null; } field.setAccessible(true); return new FieldPI<>(field, property); } } private static class MethodPI extends PropertyInfo { MethodPI(Method method, Class property) { super(property, method, method.getReturnType()); } @Override public Object invoke(Object target) throws Exception { return member.invoke(target); } } private static class FieldPI extends PropertyInfo { FieldPI(Field field, Class property) { super(property, field, field.getType()); } @Override public Object invoke(Object target) throws Exception { return member.get(target); } } // Maximum delay in ms after which we stop trying to capture a View's drawing private static final int CAPTURE_TIMEOUT = 6000; 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"; /** * Similar to REMOTE_COMMAND_DUMP but uses ViewHierarchyEncoder instead of flat text * @hide */ public static final String REMOTE_COMMAND_DUMP_ENCODED = "DUMP_ENCODED"; 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 final String REMOTE_COMMAND_INVOKE_METHOD = "INVOKE_METHOD"; private static HashMap , PropertyInfo []> sExportProperties; private static HashMap , PropertyInfo []> sCapturedViewProperties; /** * @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(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 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 { // Just being cautious... 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_DUMP_ENCODED.equalsIgnoreCase(command)) { dumpEncoded(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]); } else if (REMOTE_COMMAND_INVOKE_METHOD.equals(command)) { invokeViewMethod(view, clientStream, params); } } } /** @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(); root.mAttachInfo.mViewRootImpl.getDisplayFrame(outRect); 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 mCallback; private final Executor mExecutor; private final ReentrantLock mLock = new ReentrantLock(false); private final ArrayDeque mQueue = new ArrayDeque<>(3); private boolean mStopListening; private Thread mRenderThread; private PictureCallbackHandler(HardwareRenderer renderer, Function 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 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 mCallback; private final Executor mExecutor; private final ReentrantLock mLock = new ReentrantLock(false); private final ArrayDeque mQueue = new ArrayDeque<>(3); private boolean mStopListening; private Thread mRenderThread; private StreamingPictureCallbackHandler(HardwareRenderer renderer, Callable 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; } mQueue.add(picture); mLock.unlock(); if (needsInvoke) { mExecutor.execute(this); } } @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) { 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 { picture.writeToStream(stream); stream.flush(); } 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 @UnsupportedAppUsage // Visible for Studio; least-worst option available public static AutoCloseable startRenderingCommandsCapture(View tree, Executor executor, Callable 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(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 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(); } private static void dumpEncoded(@NonNull final View view, @NonNull OutputStream out) throws IOException { ByteArrayOutputStream baOut = new ByteArrayOutputStream(); final ViewHierarchyEncoder encoder = new ViewHierarchyEncoder(baOut); encoder.setUserPropertiesEnabled(false); encoder.addProperty("window:left", view.mAttachInfo.mWindowLeft); encoder.addProperty("window:top", view.mAttachInfo.mWindowTop); view.encode(encoder); encoder.endStream(); out.write(baOut.toByteArray()); } /** * 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) { cacheExportedProperties(group.getClass()); if (!skipChildren) { cacheExportedPropertiesForChildren(group); } // Try to use the handler provided by the view Handler handler = group.getHandler(); // Fall back on using the main thread if (handler == null) { handler = new Handler(Looper.getMainLooper()); } if (handler.getLooper() == Looper.myLooper()) { dumpViewHierarchyOnUIThread(context, group, out, level, skipChildren, includeProperties); } else { FutureTask task = new FutureTask(() -> dumpViewHierarchyOnUIThread(context, group, out, level, skipChildren, includeProperties), null); Message msg = Message.obtain(handler, task); msg.setAsynchronous(true); handler.sendMessage(msg); while (true) { try { task.get(CAPTURE_TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS); return; } catch (InterruptedException e) { // try again } catch (ExecutionException | TimeoutException e) { // Something unexpected happened. throw new RuntimeException(e); } } } } private static void cacheExportedPropertiesForChildren(ViewGroup group) { final int count = group.getChildCount(); for (int i = 0; i < count; i++) { final View view = group.getChildAt(i); cacheExportedProperties(view.getClass()); if (view instanceof ViewGroup) { cacheExportedPropertiesForChildren((ViewGroup) view); } } } private static void cacheExportedProperties(Class> klass) { if (sExportProperties != null && sExportProperties.containsKey(klass)) { return; } do { for (PropertyInfo info : getExportedProperties(klass)) { if (!info.returnType.isPrimitive() && info.property.deepExport()) { cacheExportedProperties(info.returnType); } } klass = klass.getSuperclass(); } while (klass != Object.class); } private static void dumpViewHierarchyOnUIThread(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) { dumpViewHierarchyOnUIThread(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; dumpViewHierarchyOnUIThread(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 PropertyInfo [] convertToPropertyInfos( Method[] methods, Field[] fields, Class property) { return Stream.of(Arrays.stream(methods).map(m -> PropertyInfo.forMethod(m, property)), Arrays.stream(fields).map(f -> PropertyInfo.forField(f, property))) .flatMap(Function.identity()) .filter(i -> i != null) .toArray(PropertyInfo[]::new); } private static PropertyInfo [] getExportedProperties(Class> klass) { if (sExportProperties == null) { sExportProperties = new HashMap<>(); } final HashMap , PropertyInfo []> map = sExportProperties; PropertyInfo [] properties = sExportProperties.get(klass); if (properties == null) { properties = convertToPropertyInfos(klass.getDeclaredMethods(), klass.getDeclaredFields(), ExportedProperty.class); map.put(klass, properties); } return properties; } 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 { writeExportedProperties(context, view, out, klass, prefix); klass = klass.getSuperclass(); } while (klass != Object.class); } private static String formatIntToHexString(int value) { return "0x" + Integer.toHexString(value).toUpperCase(); } private static void writeExportedProperties(Context context, Object view, BufferedWriter out, Class> klass, String prefix) throws IOException { for (PropertyInfo info : getExportedProperties(klass)) { //noinspection EmptyCatchBlock Object value; try { value = info.invoke(view); } catch (Exception e) { // ignore continue; } String categoryPrefix = info.property.category().length() != 0 ? info.property.category() + ":" : ""; if (info.returnType == int.class || info.returnType == byte.class) { if (info.property.resolveId() && context != null) { final int id = (Integer) value; value = resolveId(context, id); } else if (info.property.formatToHexString()) { if (info.returnType == int.class) { value = formatIntToHexString((Integer) value); } else if (info.returnType == byte.class) { value = "0x" + HexEncoding.encodeToString((Byte) value, true); } } else { final ViewDebug.FlagToString[] flagsMapping = info.property.flagMapping(); if (flagsMapping.length > 0) { final int intValue = (Integer) value; final String valuePrefix = categoryPrefix + prefix + info.name + '_'; exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix); } final ViewDebug.IntToString[] mapping = info.property.mapping(); if (mapping.length > 0) { final int intValue = (Integer) value; boolean mapped = false; int mappingCount = mapping.length; for (int j = 0; j < mappingCount; j++) { final ViewDebug.IntToString mapper = mapping[j]; if (mapper.from() == intValue) { value = mapper.to(); mapped = true; break; } } if (!mapped) { value = intValue; } } } } else if (info.returnType == int[].class) { final int[] array = (int[]) value; final String valuePrefix = categoryPrefix + prefix + info.name + '_'; exportUnrolledArray(context, out, info.property, array, valuePrefix, info.entrySuffix); continue; } else if (info.returnType == String[].class) { final String[] array = (String[]) value; if (info.property.hasAdjacentMapping() && array != null) { for (int j = 0; j < array.length; j += 2) { if (array[j] != null) { writeEntry(out, categoryPrefix + prefix, array[j], info.entrySuffix, array[j + 1] == null ? "null" : array[j + 1]); } } } continue; } else if (!info.returnType.isPrimitive()) { if (info.property.deepExport()) { dumpViewProperties(context, value, out, prefix + info.property.prefix()); continue; } } writeEntry(out, categoryPrefix + prefix, info.name, info.entrySuffix, value); } } 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 PropertyInfo [] getCapturedViewProperties( Class> klass) { if (sCapturedViewProperties == null) { sCapturedViewProperties = new HashMap<>(); } final HashMap , PropertyInfo []> map = sCapturedViewProperties; PropertyInfo [] infos = map.get(klass); if (infos == null) { infos = convertToPropertyInfos(klass.getMethods(), klass.getFields(), CapturedViewProperty.class); map.put(klass, infos); } return infos; } private static String exportCapturedViewProperties(Object obj, Class> klass, String prefix) { if (obj == null) { return "null"; } StringBuilder sb = new StringBuilder(); for (PropertyInfo pi : getCapturedViewProperties(klass)) { try { Object methodValue = pi.invoke(obj); if (pi.property.retrieveReturn()) { //we are interested in the second level data only sb.append(exportCapturedViewProperties(methodValue, pi.returnType, pi.name + "#")); } else { sb.append(prefix).append(pi.name).append(pi.entrySuffix).append("="); if (methodValue != null) { final String value = methodValue.toString().replace("\n", "\\n"); sb.append(value); } else { sb.append("null"); } sb.append(pi.valueSuffix).append(" "); } } catch (Exception e) { //It is OK here, we simply ignore this property } } 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(exportCapturedViewProperties(view, klass, "")); Log.d(tag, sb.toString()); } private static void invokeViewMethod(View root, OutputStream clientStream, String[] params) throws IOException { BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024); try { if (params.length < 2) { throw new IllegalArgumentException("Missing parameter"); } View targetView = findView(root, params[0]); if (targetView == null) { throw new IllegalArgumentException("View not found: " + params[0]); } String method = params[1]; ByteBuffer args = ByteBuffer.wrap(params.length < 2 ? new byte[0] : Base64.decode(params[2], Base64.NO_WRAP)); byte[] result = invokeViewMethod(targetView, method, args); out.write("1"); out.newLine(); out.write(Base64.encodeToString(result, Base64.NO_WRAP)); out.newLine(); } catch (Exception e) { out.write("-1"); out.newLine(); out.write(e.getMessage()); out.newLine(); } finally { out.close(); } } /** * 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 * @param params all the method parameters encoded in a byteArray * @throws Exception if the method invocation caused any exception * @hide */ public static byte[] invokeViewMethod(View targetView, String methodName, ByteBuffer params) throws ViewMethodInvocationSerializationException { Class>[] argTypes; Object[] args; if (!params.hasRemaining()) { argTypes = new Class>[0]; args = new Object[0]; } else { int nArgs = params.getInt(); argTypes = new Class>[nArgs]; args = new Object[nArgs]; deserializeMethodParameters(args, argTypes, params); } Method method; try { method = targetView.getClass().getMethod(methodName, argTypes); } catch (NoSuchMethodException e) { Log.e(TAG, "No such method: " + e.getMessage()); throw new ViewMethodInvocationSerializationException( "No such method: " + e.getMessage()); } try { // Invoke the method on Views handler FutureTask