1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.view; 18 19 import android.annotation.NonNull; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.graphics.Bitmap; 23 import android.graphics.Canvas; 24 import android.graphics.Rect; 25 import android.os.Debug; 26 import android.os.Handler; 27 import android.os.RemoteException; 28 import android.util.DisplayMetrics; 29 import android.util.Log; 30 import android.util.TypedValue; 31 32 import java.io.BufferedOutputStream; 33 import java.io.BufferedWriter; 34 import java.io.ByteArrayOutputStream; 35 import java.io.DataOutputStream; 36 import java.io.IOException; 37 import java.io.OutputStream; 38 import java.io.OutputStreamWriter; 39 import java.lang.annotation.ElementType; 40 import java.lang.annotation.Retention; 41 import java.lang.annotation.RetentionPolicy; 42 import java.lang.annotation.Target; 43 import java.lang.reflect.AccessibleObject; 44 import java.lang.reflect.Field; 45 import java.lang.reflect.InvocationTargetException; 46 import java.lang.reflect.Method; 47 import java.util.ArrayList; 48 import java.util.HashMap; 49 import java.util.concurrent.Callable; 50 import java.util.concurrent.CancellationException; 51 import java.util.concurrent.CountDownLatch; 52 import java.util.concurrent.ExecutionException; 53 import java.util.concurrent.FutureTask; 54 import java.util.concurrent.TimeUnit; 55 import java.util.concurrent.TimeoutException; 56 import java.util.concurrent.atomic.AtomicReference; 57 58 /** 59 * Various debugging/tracing tools related to {@link View} and the view hierarchy. 60 */ 61 public class ViewDebug { 62 /** 63 * @deprecated This flag is now unused 64 */ 65 @Deprecated 66 public static final boolean TRACE_HIERARCHY = false; 67 68 /** 69 * @deprecated This flag is now unused 70 */ 71 @Deprecated 72 public static final boolean TRACE_RECYCLER = false; 73 74 /** 75 * Enables detailed logging of drag/drop operations. 76 * @hide 77 */ 78 public static final boolean DEBUG_DRAG = false; 79 80 /** 81 * Enables detailed logging of task positioning operations. 82 * @hide 83 */ 84 public static final boolean DEBUG_POSITIONING = false; 85 86 /** 87 * This annotation can be used to mark fields and methods to be dumped by 88 * the view server. Only non-void methods with no arguments can be annotated 89 * by this annotation. 90 */ 91 @Target({ ElementType.FIELD, ElementType.METHOD }) 92 @Retention(RetentionPolicy.RUNTIME) 93 public @interface ExportedProperty { 94 /** 95 * When resolveId is true, and if the annotated field/method return value 96 * is an int, the value is converted to an Android's resource name. 97 * 98 * @return true if the property's value must be transformed into an Android 99 * resource name, false otherwise 100 */ resolveId()101 boolean resolveId() default false; 102 103 /** 104 * A mapping can be defined to map int values to specific strings. For 105 * instance, View.getVisibility() returns 0, 4 or 8. However, these values 106 * actually mean VISIBLE, INVISIBLE and GONE. A mapping can be used to see 107 * these human readable values: 108 * 109 * <pre> 110 * {@literal @}ViewDebug.ExportedProperty(mapping = { 111 * {@literal @}ViewDebug.IntToString(from = 0, to = "VISIBLE"), 112 * {@literal @}ViewDebug.IntToString(from = 4, to = "INVISIBLE"), 113 * {@literal @}ViewDebug.IntToString(from = 8, to = "GONE") 114 * }) 115 * public int getVisibility() { ... 116 * <pre> 117 * 118 * @return An array of int to String mappings 119 * 120 * @see android.view.ViewDebug.IntToString 121 */ mapping()122 IntToString[] mapping() default { }; 123 124 /** 125 * A mapping can be defined to map array indices to specific strings. 126 * A mapping can be used to see human readable values for the indices 127 * of an array: 128 * 129 * <pre> 130 * {@literal @}ViewDebug.ExportedProperty(indexMapping = { 131 * {@literal @}ViewDebug.IntToString(from = 0, to = "INVALID"), 132 * {@literal @}ViewDebug.IntToString(from = 1, to = "FIRST"), 133 * {@literal @}ViewDebug.IntToString(from = 2, to = "SECOND") 134 * }) 135 * private int[] mElements; 136 * <pre> 137 * 138 * @return An array of int to String mappings 139 * 140 * @see android.view.ViewDebug.IntToString 141 * @see #mapping() 142 */ indexMapping()143 IntToString[] indexMapping() default { }; 144 145 /** 146 * A flags mapping can be defined to map flags encoded in an integer to 147 * specific strings. A mapping can be used to see human readable values 148 * for the flags of an integer: 149 * 150 * <pre> 151 * {@literal @}ViewDebug.ExportedProperty(flagMapping = { 152 * {@literal @}ViewDebug.FlagToString(mask = ENABLED_MASK, equals = ENABLED, 153 * name = "ENABLED"), 154 * {@literal @}ViewDebug.FlagToString(mask = ENABLED_MASK, equals = DISABLED, 155 * name = "DISABLED"), 156 * }) 157 * private int mFlags; 158 * <pre> 159 * 160 * A specified String is output when the following is true: 161 * 162 * @return An array of int to String mappings 163 */ flagMapping()164 FlagToString[] flagMapping() default { }; 165 166 /** 167 * When deep export is turned on, this property is not dumped. Instead, the 168 * properties contained in this property are dumped. Each child property 169 * is prefixed with the name of this property. 170 * 171 * @return true if the properties of this property should be dumped 172 * 173 * @see #prefix() 174 */ deepExport()175 boolean deepExport() default false; 176 177 /** 178 * The prefix to use on child properties when deep export is enabled 179 * 180 * @return a prefix as a String 181 * 182 * @see #deepExport() 183 */ prefix()184 String prefix() default ""; 185 186 /** 187 * Specifies the category the property falls into, such as measurement, 188 * layout, drawing, etc. 189 * 190 * @return the category as String 191 */ category()192 String category() default ""; 193 194 /** 195 * Indicates whether or not to format an {@code int} or {@code byte} value as a hex string. 196 * 197 * @return true if the supported values should be formatted as a hex string. 198 */ formatToHexString()199 boolean formatToHexString() default false; 200 201 /** 202 * Indicates whether or not the key to value mappings are held in adjacent indices. 203 * 204 * Note: Applies only to fields and methods that return String[]. 205 * 206 * @return true if the key to value mappings are held in adjacent indices. 207 */ hasAdjacentMapping()208 boolean hasAdjacentMapping() default false; 209 } 210 211 /** 212 * Defines a mapping from an int value to a String. Such a mapping can be used 213 * in an @ExportedProperty to provide more meaningful values to the end user. 214 * 215 * @see android.view.ViewDebug.ExportedProperty 216 */ 217 @Target({ ElementType.TYPE }) 218 @Retention(RetentionPolicy.RUNTIME) 219 public @interface IntToString { 220 /** 221 * The original int value to map to a String. 222 * 223 * @return An arbitrary int value. 224 */ from()225 int from(); 226 227 /** 228 * The String to use in place of the original int value. 229 * 230 * @return An arbitrary non-null String. 231 */ to()232 String to(); 233 } 234 235 /** 236 * Defines a mapping from a flag to a String. Such a mapping can be used 237 * in an @ExportedProperty to provide more meaningful values to the end user. 238 * 239 * @see android.view.ViewDebug.ExportedProperty 240 */ 241 @Target({ ElementType.TYPE }) 242 @Retention(RetentionPolicy.RUNTIME) 243 public @interface FlagToString { 244 /** 245 * The mask to apply to the original value. 246 * 247 * @return An arbitrary int value. 248 */ mask()249 int mask(); 250 251 /** 252 * The value to compare to the result of: 253 * <code>original value & {@link #mask()}</code>. 254 * 255 * @return An arbitrary value. 256 */ equals()257 int equals(); 258 259 /** 260 * The String to use in place of the original int value. 261 * 262 * @return An arbitrary non-null String. 263 */ name()264 String name(); 265 266 /** 267 * Indicates whether to output the flag when the test is true, 268 * or false. Defaults to true. 269 */ outputIf()270 boolean outputIf() default true; 271 } 272 273 /** 274 * This annotation can be used to mark fields and methods to be dumped when 275 * the view is captured. Methods with this annotation must have no arguments 276 * and must return a valid type of data. 277 */ 278 @Target({ ElementType.FIELD, ElementType.METHOD }) 279 @Retention(RetentionPolicy.RUNTIME) 280 public @interface CapturedViewProperty { 281 /** 282 * When retrieveReturn is true, we need to retrieve second level methods 283 * e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod() 284 * we will set retrieveReturn = true on the annotation of 285 * myView.getFirstLevelMethod() 286 * @return true if we need the second level methods 287 */ retrieveReturn()288 boolean retrieveReturn() default false; 289 } 290 291 /** 292 * Allows a View to inject custom children into HierarchyViewer. For example, 293 * WebView uses this to add its internal layer tree as a child to itself 294 * @hide 295 */ 296 public interface HierarchyHandler { 297 /** 298 * Dumps custom children to hierarchy viewer. 299 * See ViewDebug.dumpViewWithProperties(Context, View, BufferedWriter, int) 300 * for the format 301 * 302 * An empty implementation should simply do nothing 303 * 304 * @param out The output writer 305 * @param level The indentation level 306 */ dumpViewHierarchyWithProperties(BufferedWriter out, int level)307 public void dumpViewHierarchyWithProperties(BufferedWriter out, int level); 308 309 /** 310 * Returns a View to enable grabbing screenshots from custom children 311 * returned in dumpViewHierarchyWithProperties. 312 * 313 * @param className The className of the view to find 314 * @param hashCode The hashCode of the view to find 315 * @return the View to capture from, or null if not found 316 */ findHierarchyView(String className, int hashCode)317 public View findHierarchyView(String className, int hashCode); 318 } 319 320 private static HashMap<Class<?>, Method[]> mCapturedViewMethodsForClasses = null; 321 private static HashMap<Class<?>, Field[]> mCapturedViewFieldsForClasses = null; 322 323 // Maximum delay in ms after which we stop trying to capture a View's drawing 324 private static final int CAPTURE_TIMEOUT = 4000; 325 326 private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE"; 327 private static final String REMOTE_COMMAND_DUMP = "DUMP"; 328 private static final String REMOTE_COMMAND_DUMP_THEME = "DUMP_THEME"; 329 private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE"; 330 private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT"; 331 private static final String REMOTE_PROFILE = "PROFILE"; 332 private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS"; 333 private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST"; 334 335 private static HashMap<Class<?>, Field[]> sFieldsForClasses; 336 private static HashMap<Class<?>, Method[]> sMethodsForClasses; 337 private static HashMap<AccessibleObject, ExportedProperty> sAnnotations; 338 339 /** 340 * @deprecated This enum is now unused 341 */ 342 @Deprecated 343 public enum HierarchyTraceType { 344 INVALIDATE, 345 INVALIDATE_CHILD, 346 INVALIDATE_CHILD_IN_PARENT, 347 REQUEST_LAYOUT, 348 ON_LAYOUT, 349 ON_MEASURE, 350 DRAW, 351 BUILD_CACHE 352 } 353 354 /** 355 * @deprecated This enum is now unused 356 */ 357 @Deprecated 358 public enum RecyclerTraceType { 359 NEW_VIEW, 360 BIND_VIEW, 361 RECYCLE_FROM_ACTIVE_HEAP, 362 RECYCLE_FROM_SCRAP_HEAP, 363 MOVE_TO_SCRAP_HEAP, 364 MOVE_FROM_ACTIVE_TO_SCRAP_HEAP 365 } 366 367 /** 368 * Returns the number of instanciated Views. 369 * 370 * @return The number of Views instanciated in the current process. 371 * 372 * @hide 373 */ getViewInstanceCount()374 public static long getViewInstanceCount() { 375 return Debug.countInstancesOfClass(View.class); 376 } 377 378 /** 379 * Returns the number of instanciated ViewAncestors. 380 * 381 * @return The number of ViewAncestors instanciated in the current process. 382 * 383 * @hide 384 */ getViewRootImplCount()385 public static long getViewRootImplCount() { 386 return Debug.countInstancesOfClass(ViewRootImpl.class); 387 } 388 389 /** 390 * @deprecated This method is now unused and invoking it is a no-op 391 */ 392 @Deprecated 393 @SuppressWarnings({ "UnusedParameters", "deprecation" }) trace(View view, RecyclerTraceType type, int... parameters)394 public static void trace(View view, RecyclerTraceType type, int... parameters) { 395 } 396 397 /** 398 * @deprecated This method is now unused and invoking it is a no-op 399 */ 400 @Deprecated 401 @SuppressWarnings("UnusedParameters") startRecyclerTracing(String prefix, View view)402 public static void startRecyclerTracing(String prefix, View view) { 403 } 404 405 /** 406 * @deprecated This method is now unused and invoking it is a no-op 407 */ 408 @Deprecated 409 @SuppressWarnings("UnusedParameters") stopRecyclerTracing()410 public static void stopRecyclerTracing() { 411 } 412 413 /** 414 * @deprecated This method is now unused and invoking it is a no-op 415 */ 416 @Deprecated 417 @SuppressWarnings({ "UnusedParameters", "deprecation" }) trace(View view, HierarchyTraceType type)418 public static void trace(View view, HierarchyTraceType type) { 419 } 420 421 /** 422 * @deprecated This method is now unused and invoking it is a no-op 423 */ 424 @Deprecated 425 @SuppressWarnings("UnusedParameters") startHierarchyTracing(String prefix, View view)426 public static void startHierarchyTracing(String prefix, View view) { 427 } 428 429 /** 430 * @deprecated This method is now unused and invoking it is a no-op 431 */ 432 @Deprecated stopHierarchyTracing()433 public static void stopHierarchyTracing() { 434 } 435 dispatchCommand(View view, String command, String parameters, OutputStream clientStream)436 static void dispatchCommand(View view, String command, String parameters, 437 OutputStream clientStream) throws IOException { 438 439 // Paranoid but safe... 440 view = view.getRootView(); 441 442 if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) { 443 dump(view, false, true, clientStream); 444 } else if (REMOTE_COMMAND_DUMP_THEME.equalsIgnoreCase(command)) { 445 dumpTheme(view, clientStream); 446 } else if (REMOTE_COMMAND_CAPTURE_LAYERS.equalsIgnoreCase(command)) { 447 captureLayers(view, new DataOutputStream(clientStream)); 448 } else { 449 final String[] params = parameters.split(" "); 450 if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) { 451 capture(view, clientStream, params[0]); 452 } else if (REMOTE_COMMAND_OUTPUT_DISPLAYLIST.equalsIgnoreCase(command)) { 453 outputDisplayList(view, params[0]); 454 } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) { 455 invalidate(view, params[0]); 456 } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) { 457 requestLayout(view, params[0]); 458 } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) { 459 profile(view, clientStream, params[0]); 460 } 461 } 462 } 463 464 /** @hide */ findView(View root, String parameter)465 public static View findView(View root, String parameter) { 466 // Look by type/hashcode 467 if (parameter.indexOf('@') != -1) { 468 final String[] ids = parameter.split("@"); 469 final String className = ids[0]; 470 final int hashCode = (int) Long.parseLong(ids[1], 16); 471 472 View view = root.getRootView(); 473 if (view instanceof ViewGroup) { 474 return findView((ViewGroup) view, className, hashCode); 475 } 476 } else { 477 // Look by id 478 final int id = root.getResources().getIdentifier(parameter, null, null); 479 return root.getRootView().findViewById(id); 480 } 481 482 return null; 483 } 484 invalidate(View root, String parameter)485 private static void invalidate(View root, String parameter) { 486 final View view = findView(root, parameter); 487 if (view != null) { 488 view.postInvalidate(); 489 } 490 } 491 requestLayout(View root, String parameter)492 private static void requestLayout(View root, String parameter) { 493 final View view = findView(root, parameter); 494 if (view != null) { 495 root.post(new Runnable() { 496 public void run() { 497 view.requestLayout(); 498 } 499 }); 500 } 501 } 502 profile(View root, OutputStream clientStream, String parameter)503 private static void profile(View root, OutputStream clientStream, String parameter) 504 throws IOException { 505 506 final View view = findView(root, parameter); 507 BufferedWriter out = null; 508 try { 509 out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024); 510 511 if (view != null) { 512 profileViewAndChildren(view, out); 513 } else { 514 out.write("-1 -1 -1"); 515 out.newLine(); 516 } 517 out.write("DONE."); 518 out.newLine(); 519 } catch (Exception e) { 520 android.util.Log.w("View", "Problem profiling the view:", e); 521 } finally { 522 if (out != null) { 523 out.close(); 524 } 525 } 526 } 527 528 /** @hide */ profileViewAndChildren(final View view, BufferedWriter out)529 public static void profileViewAndChildren(final View view, BufferedWriter out) 530 throws IOException { 531 profileViewAndChildren(view, out, true); 532 } 533 profileViewAndChildren(final View view, BufferedWriter out, boolean root)534 private static void profileViewAndChildren(final View view, BufferedWriter out, boolean root) 535 throws IOException { 536 537 long durationMeasure = 538 (root || (view.mPrivateFlags & View.PFLAG_MEASURED_DIMENSION_SET) != 0) 539 ? profileViewOperation(view, new ViewOperation<Void>() { 540 public Void[] pre() { 541 forceLayout(view); 542 return null; 543 } 544 545 private void forceLayout(View view) { 546 view.forceLayout(); 547 if (view instanceof ViewGroup) { 548 ViewGroup group = (ViewGroup) view; 549 final int count = group.getChildCount(); 550 for (int i = 0; i < count; i++) { 551 forceLayout(group.getChildAt(i)); 552 } 553 } 554 } 555 556 public void run(Void... data) { 557 view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec); 558 } 559 560 public void post(Void... data) { 561 } 562 }) 563 : 0; 564 long durationLayout = 565 (root || (view.mPrivateFlags & View.PFLAG_LAYOUT_REQUIRED) != 0) 566 ? profileViewOperation(view, new ViewOperation<Void>() { 567 public Void[] pre() { 568 return null; 569 } 570 571 public void run(Void... data) { 572 view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom); 573 } 574 575 public void post(Void... data) { 576 } 577 }) : 0; 578 long durationDraw = 579 (root || !view.willNotDraw() || (view.mPrivateFlags & View.PFLAG_DRAWN) != 0) 580 ? profileViewOperation(view, new ViewOperation<Object>() { 581 public Object[] pre() { 582 final DisplayMetrics metrics = 583 (view != null && view.getResources() != null) ? 584 view.getResources().getDisplayMetrics() : null; 585 final Bitmap bitmap = metrics != null ? 586 Bitmap.createBitmap(metrics, metrics.widthPixels, 587 metrics.heightPixels, Bitmap.Config.RGB_565) : null; 588 final Canvas canvas = bitmap != null ? new Canvas(bitmap) : null; 589 return new Object[] { 590 bitmap, canvas 591 }; 592 } 593 594 public void run(Object... data) { 595 if (data[1] != null) { 596 view.draw((Canvas) data[1]); 597 } 598 } 599 600 public void post(Object... data) { 601 if (data[1] != null) { 602 ((Canvas) data[1]).setBitmap(null); 603 } 604 if (data[0] != null) { 605 ((Bitmap) data[0]).recycle(); 606 } 607 } 608 }) : 0; 609 out.write(String.valueOf(durationMeasure)); 610 out.write(' '); 611 out.write(String.valueOf(durationLayout)); 612 out.write(' '); 613 out.write(String.valueOf(durationDraw)); 614 out.newLine(); 615 if (view instanceof ViewGroup) { 616 ViewGroup group = (ViewGroup) view; 617 final int count = group.getChildCount(); 618 for (int i = 0; i < count; i++) { 619 profileViewAndChildren(group.getChildAt(i), out, false); 620 } 621 } 622 } 623 624 interface ViewOperation<T> { pre()625 T[] pre(); run(T... data)626 void run(T... data); post(T... data)627 void post(T... data); 628 } 629 profileViewOperation(View view, final ViewOperation<T> operation)630 private static <T> long profileViewOperation(View view, final ViewOperation<T> operation) { 631 final CountDownLatch latch = new CountDownLatch(1); 632 final long[] duration = new long[1]; 633 634 view.post(new Runnable() { 635 public void run() { 636 try { 637 T[] data = operation.pre(); 638 long start = Debug.threadCpuTimeNanos(); 639 //noinspection unchecked 640 operation.run(data); 641 duration[0] = Debug.threadCpuTimeNanos() - start; 642 //noinspection unchecked 643 operation.post(data); 644 } finally { 645 latch.countDown(); 646 } 647 } 648 }); 649 650 try { 651 if (!latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS)) { 652 Log.w("View", "Could not complete the profiling of the view " + view); 653 return -1; 654 } 655 } catch (InterruptedException e) { 656 Log.w("View", "Could not complete the profiling of the view " + view); 657 Thread.currentThread().interrupt(); 658 return -1; 659 } 660 661 return duration[0]; 662 } 663 664 /** @hide */ captureLayers(View root, final DataOutputStream clientStream)665 public static void captureLayers(View root, final DataOutputStream clientStream) 666 throws IOException { 667 668 try { 669 Rect outRect = new Rect(); 670 try { 671 root.mAttachInfo.mSession.getDisplayFrame(root.mAttachInfo.mWindow, outRect); 672 } catch (RemoteException e) { 673 // Ignore 674 } 675 676 clientStream.writeInt(outRect.width()); 677 clientStream.writeInt(outRect.height()); 678 679 captureViewLayer(root, clientStream, true); 680 681 clientStream.write(2); 682 } finally { 683 clientStream.close(); 684 } 685 } 686 captureViewLayer(View view, DataOutputStream clientStream, boolean visible)687 private static void captureViewLayer(View view, DataOutputStream clientStream, boolean visible) 688 throws IOException { 689 690 final boolean localVisible = view.getVisibility() == View.VISIBLE && visible; 691 692 if ((view.mPrivateFlags & View.PFLAG_SKIP_DRAW) != View.PFLAG_SKIP_DRAW) { 693 final int id = view.getId(); 694 String name = view.getClass().getSimpleName(); 695 if (id != View.NO_ID) { 696 name = resolveId(view.getContext(), id).toString(); 697 } 698 699 clientStream.write(1); 700 clientStream.writeUTF(name); 701 clientStream.writeByte(localVisible ? 1 : 0); 702 703 int[] position = new int[2]; 704 // XXX: Should happen on the UI thread 705 view.getLocationInWindow(position); 706 707 clientStream.writeInt(position[0]); 708 clientStream.writeInt(position[1]); 709 clientStream.flush(); 710 711 Bitmap b = performViewCapture(view, true); 712 if (b != null) { 713 ByteArrayOutputStream arrayOut = new ByteArrayOutputStream(b.getWidth() * 714 b.getHeight() * 2); 715 b.compress(Bitmap.CompressFormat.PNG, 100, arrayOut); 716 clientStream.writeInt(arrayOut.size()); 717 arrayOut.writeTo(clientStream); 718 } 719 clientStream.flush(); 720 } 721 722 if (view instanceof ViewGroup) { 723 ViewGroup group = (ViewGroup) view; 724 int count = group.getChildCount(); 725 726 for (int i = 0; i < count; i++) { 727 captureViewLayer(group.getChildAt(i), clientStream, localVisible); 728 } 729 } 730 731 if (view.mOverlay != null) { 732 ViewGroup overlayContainer = view.getOverlay().mOverlayViewGroup; 733 captureViewLayer(overlayContainer, clientStream, localVisible); 734 } 735 } 736 outputDisplayList(View root, String parameter)737 private static void outputDisplayList(View root, String parameter) throws IOException { 738 final View view = findView(root, parameter); 739 view.getViewRootImpl().outputDisplayList(view); 740 } 741 742 /** @hide */ outputDisplayList(View root, View target)743 public static void outputDisplayList(View root, View target) { 744 root.getViewRootImpl().outputDisplayList(target); 745 } 746 capture(View root, final OutputStream clientStream, String parameter)747 private static void capture(View root, final OutputStream clientStream, String parameter) 748 throws IOException { 749 750 final View captureView = findView(root, parameter); 751 capture(root, clientStream, captureView); 752 } 753 754 /** @hide */ capture(View root, final OutputStream clientStream, View captureView)755 public static void capture(View root, final OutputStream clientStream, View captureView) 756 throws IOException { 757 Bitmap b = performViewCapture(captureView, false); 758 759 if (b == null) { 760 Log.w("View", "Failed to create capture bitmap!"); 761 // Send an empty one so that it doesn't get stuck waiting for 762 // something. 763 b = Bitmap.createBitmap(root.getResources().getDisplayMetrics(), 764 1, 1, Bitmap.Config.ARGB_8888); 765 } 766 767 BufferedOutputStream out = null; 768 try { 769 out = new BufferedOutputStream(clientStream, 32 * 1024); 770 b.compress(Bitmap.CompressFormat.PNG, 100, out); 771 out.flush(); 772 } finally { 773 if (out != null) { 774 out.close(); 775 } 776 b.recycle(); 777 } 778 } 779 performViewCapture(final View captureView, final boolean skipChildren)780 private static Bitmap performViewCapture(final View captureView, final boolean skipChildren) { 781 if (captureView != null) { 782 final CountDownLatch latch = new CountDownLatch(1); 783 final Bitmap[] cache = new Bitmap[1]; 784 785 captureView.post(new Runnable() { 786 public void run() { 787 try { 788 cache[0] = captureView.createSnapshot( 789 Bitmap.Config.ARGB_8888, 0, skipChildren); 790 } catch (OutOfMemoryError e) { 791 Log.w("View", "Out of memory for bitmap"); 792 } finally { 793 latch.countDown(); 794 } 795 } 796 }); 797 798 try { 799 latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS); 800 return cache[0]; 801 } catch (InterruptedException e) { 802 Log.w("View", "Could not complete the capture of the view " + captureView); 803 Thread.currentThread().interrupt(); 804 } 805 } 806 807 return null; 808 } 809 810 /** 811 * Dumps the view hierarchy starting from the given view. 812 * @deprecated See {@link #dumpv2(View, ByteArrayOutputStream)} below. 813 * @hide 814 */ 815 @Deprecated dump(View root, boolean skipChildren, boolean includeProperties, OutputStream clientStream)816 public static void dump(View root, boolean skipChildren, boolean includeProperties, 817 OutputStream clientStream) throws IOException { 818 BufferedWriter out = null; 819 try { 820 out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024); 821 View view = root.getRootView(); 822 if (view instanceof ViewGroup) { 823 ViewGroup group = (ViewGroup) view; 824 dumpViewHierarchy(group.getContext(), group, out, 0, 825 skipChildren, includeProperties); 826 } 827 out.write("DONE."); 828 out.newLine(); 829 } catch (Exception e) { 830 android.util.Log.w("View", "Problem dumping the view:", e); 831 } finally { 832 if (out != null) { 833 out.close(); 834 } 835 } 836 } 837 838 /** 839 * Dumps the view hierarchy starting from the given view. 840 * Rather than using reflection, it uses View's encode method to obtain all the properties. 841 * @hide 842 */ dumpv2(@onNull final View view, @NonNull ByteArrayOutputStream out)843 public static void dumpv2(@NonNull final View view, @NonNull ByteArrayOutputStream out) 844 throws InterruptedException { 845 final ViewHierarchyEncoder encoder = new ViewHierarchyEncoder(out); 846 final CountDownLatch latch = new CountDownLatch(1); 847 848 view.post(new Runnable() { 849 @Override 850 public void run() { 851 encoder.addProperty("window:left", view.mAttachInfo.mWindowLeft); 852 encoder.addProperty("window:top", view.mAttachInfo.mWindowTop); 853 view.encode(encoder); 854 latch.countDown(); 855 } 856 }); 857 858 latch.await(2, TimeUnit.SECONDS); 859 encoder.endStream(); 860 } 861 862 /** 863 * Dumps the theme attributes from the given View. 864 * @hide 865 */ dumpTheme(View view, OutputStream clientStream)866 public static void dumpTheme(View view, OutputStream clientStream) throws IOException { 867 BufferedWriter out = null; 868 try { 869 out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024); 870 String[] attributes = getStyleAttributesDump(view.getContext().getResources(), 871 view.getContext().getTheme()); 872 if (attributes != null) { 873 for (int i = 0; i < attributes.length; i += 2) { 874 if (attributes[i] != null) { 875 out.write(attributes[i] + "\n"); 876 out.write(attributes[i + 1] + "\n"); 877 } 878 } 879 } 880 out.write("DONE."); 881 out.newLine(); 882 } catch (Exception e) { 883 android.util.Log.w("View", "Problem dumping View Theme:", e); 884 } finally { 885 if (out != null) { 886 out.close(); 887 } 888 } 889 } 890 891 /** 892 * Gets the style attributes from the {@link Resources.Theme}. For debugging only. 893 * 894 * @param resources Resources to resolve attributes from. 895 * @param theme Theme to dump. 896 * @return a String array containing pairs of adjacent Theme attribute data: name followed by 897 * its value. 898 * 899 * @hide 900 */ getStyleAttributesDump(Resources resources, Resources.Theme theme)901 private static String[] getStyleAttributesDump(Resources resources, Resources.Theme theme) { 902 TypedValue outValue = new TypedValue(); 903 String nullString = "null"; 904 int i = 0; 905 int[] attributes = theme.getAllAttributes(); 906 String[] data = new String[attributes.length * 2]; 907 for (int attributeId : attributes) { 908 try { 909 data[i] = resources.getResourceName(attributeId); 910 data[i + 1] = theme.resolveAttribute(attributeId, outValue, true) ? 911 outValue.coerceToString().toString() : nullString; 912 i += 2; 913 914 // attempt to replace reference data with its name 915 if (outValue.type == TypedValue.TYPE_REFERENCE) { 916 data[i - 1] = resources.getResourceName(outValue.resourceId); 917 } 918 } catch (Resources.NotFoundException e) { 919 // ignore resources we can't resolve 920 } 921 } 922 return data; 923 } 924 findView(ViewGroup group, String className, int hashCode)925 private static View findView(ViewGroup group, String className, int hashCode) { 926 if (isRequestedView(group, className, hashCode)) { 927 return group; 928 } 929 930 final int count = group.getChildCount(); 931 for (int i = 0; i < count; i++) { 932 final View view = group.getChildAt(i); 933 if (view instanceof ViewGroup) { 934 final View found = findView((ViewGroup) view, className, hashCode); 935 if (found != null) { 936 return found; 937 } 938 } else if (isRequestedView(view, className, hashCode)) { 939 return view; 940 } 941 if (view.mOverlay != null) { 942 final View found = findView((ViewGroup) view.mOverlay.mOverlayViewGroup, 943 className, hashCode); 944 if (found != null) { 945 return found; 946 } 947 } 948 if (view instanceof HierarchyHandler) { 949 final View found = ((HierarchyHandler)view) 950 .findHierarchyView(className, hashCode); 951 if (found != null) { 952 return found; 953 } 954 } 955 } 956 return null; 957 } 958 isRequestedView(View view, String className, int hashCode)959 private static boolean isRequestedView(View view, String className, int hashCode) { 960 if (view.hashCode() == hashCode) { 961 String viewClassName = view.getClass().getName(); 962 if (className.equals("ViewOverlay")) { 963 return viewClassName.equals("android.view.ViewOverlay$OverlayViewGroup"); 964 } else { 965 return className.equals(viewClassName); 966 } 967 } 968 return false; 969 } 970 dumpViewHierarchy(Context context, ViewGroup group, BufferedWriter out, int level, boolean skipChildren, boolean includeProperties)971 private static void dumpViewHierarchy(Context context, ViewGroup group, 972 BufferedWriter out, int level, boolean skipChildren, boolean includeProperties) { 973 if (!dumpView(context, group, out, level, includeProperties)) { 974 return; 975 } 976 977 if (skipChildren) { 978 return; 979 } 980 981 final int count = group.getChildCount(); 982 for (int i = 0; i < count; i++) { 983 final View view = group.getChildAt(i); 984 if (view instanceof ViewGroup) { 985 dumpViewHierarchy(context, (ViewGroup) view, out, level + 1, skipChildren, 986 includeProperties); 987 } else { 988 dumpView(context, view, out, level + 1, includeProperties); 989 } 990 if (view.mOverlay != null) { 991 ViewOverlay overlay = view.getOverlay(); 992 ViewGroup overlayContainer = overlay.mOverlayViewGroup; 993 dumpViewHierarchy(context, overlayContainer, out, level + 2, skipChildren, 994 includeProperties); 995 } 996 } 997 if (group instanceof HierarchyHandler) { 998 ((HierarchyHandler)group).dumpViewHierarchyWithProperties(out, level + 1); 999 } 1000 } 1001 dumpView(Context context, View view, BufferedWriter out, int level, boolean includeProperties)1002 private static boolean dumpView(Context context, View view, 1003 BufferedWriter out, int level, boolean includeProperties) { 1004 1005 try { 1006 for (int i = 0; i < level; i++) { 1007 out.write(' '); 1008 } 1009 String className = view.getClass().getName(); 1010 if (className.equals("android.view.ViewOverlay$OverlayViewGroup")) { 1011 className = "ViewOverlay"; 1012 } 1013 out.write(className); 1014 out.write('@'); 1015 out.write(Integer.toHexString(view.hashCode())); 1016 out.write(' '); 1017 if (includeProperties) { 1018 dumpViewProperties(context, view, out); 1019 } 1020 out.newLine(); 1021 } catch (IOException e) { 1022 Log.w("View", "Error while dumping hierarchy tree"); 1023 return false; 1024 } 1025 return true; 1026 } 1027 getExportedPropertyFields(Class<?> klass)1028 private static Field[] getExportedPropertyFields(Class<?> klass) { 1029 if (sFieldsForClasses == null) { 1030 sFieldsForClasses = new HashMap<Class<?>, Field[]>(); 1031 } 1032 if (sAnnotations == null) { 1033 sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512); 1034 } 1035 1036 final HashMap<Class<?>, Field[]> map = sFieldsForClasses; 1037 1038 Field[] fields = map.get(klass); 1039 if (fields != null) { 1040 return fields; 1041 } 1042 1043 try { 1044 final Field[] declaredFields = klass.getDeclaredFieldsUnchecked(false); 1045 final ArrayList<Field> foundFields = new ArrayList<Field>(); 1046 for (final Field field : declaredFields) { 1047 // Fields which can't be resolved have a null type. 1048 if (field.getType() != null && field.isAnnotationPresent(ExportedProperty.class)) { 1049 field.setAccessible(true); 1050 foundFields.add(field); 1051 sAnnotations.put(field, field.getAnnotation(ExportedProperty.class)); 1052 } 1053 } 1054 fields = foundFields.toArray(new Field[foundFields.size()]); 1055 map.put(klass, fields); 1056 } catch (NoClassDefFoundError e) { 1057 throw new AssertionError(e); 1058 } 1059 1060 return fields; 1061 } 1062 getExportedPropertyMethods(Class<?> klass)1063 private static Method[] getExportedPropertyMethods(Class<?> klass) { 1064 if (sMethodsForClasses == null) { 1065 sMethodsForClasses = new HashMap<Class<?>, Method[]>(100); 1066 } 1067 if (sAnnotations == null) { 1068 sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512); 1069 } 1070 1071 final HashMap<Class<?>, Method[]> map = sMethodsForClasses; 1072 1073 Method[] methods = map.get(klass); 1074 if (methods != null) { 1075 return methods; 1076 } 1077 1078 methods = klass.getDeclaredMethodsUnchecked(false); 1079 1080 final ArrayList<Method> foundMethods = new ArrayList<Method>(); 1081 for (final Method method : methods) { 1082 // Ensure the method return and parameter types can be resolved. 1083 try { 1084 method.getReturnType(); 1085 method.getParameterTypes(); 1086 } catch (NoClassDefFoundError e) { 1087 continue; 1088 } 1089 1090 if (method.getParameterTypes().length == 0 && 1091 method.isAnnotationPresent(ExportedProperty.class) && 1092 method.getReturnType() != Void.class) { 1093 method.setAccessible(true); 1094 foundMethods.add(method); 1095 sAnnotations.put(method, method.getAnnotation(ExportedProperty.class)); 1096 } 1097 } 1098 1099 methods = foundMethods.toArray(new Method[foundMethods.size()]); 1100 map.put(klass, methods); 1101 1102 return methods; 1103 } 1104 dumpViewProperties(Context context, Object view, BufferedWriter out)1105 private static void dumpViewProperties(Context context, Object view, 1106 BufferedWriter out) throws IOException { 1107 1108 dumpViewProperties(context, view, out, ""); 1109 } 1110 dumpViewProperties(Context context, Object view, BufferedWriter out, String prefix)1111 private static void dumpViewProperties(Context context, Object view, 1112 BufferedWriter out, String prefix) throws IOException { 1113 1114 if (view == null) { 1115 out.write(prefix + "=4,null "); 1116 return; 1117 } 1118 1119 Class<?> klass = view.getClass(); 1120 do { 1121 exportFields(context, view, out, klass, prefix); 1122 exportMethods(context, view, out, klass, prefix); 1123 klass = klass.getSuperclass(); 1124 } while (klass != Object.class); 1125 } 1126 callMethodOnAppropriateTheadBlocking(final Method method, final Object object)1127 private static Object callMethodOnAppropriateTheadBlocking(final Method method, 1128 final Object object) throws IllegalAccessException, InvocationTargetException, 1129 TimeoutException { 1130 if (!(object instanceof View)) { 1131 return method.invoke(object, (Object[]) null); 1132 } 1133 1134 final View view = (View) object; 1135 Callable<Object> callable = new Callable<Object>() { 1136 @Override 1137 public Object call() throws IllegalAccessException, InvocationTargetException { 1138 return method.invoke(view, (Object[]) null); 1139 } 1140 }; 1141 FutureTask<Object> future = new FutureTask<Object>(callable); 1142 // Try to use the handler provided by the view 1143 Handler handler = view.getHandler(); 1144 // Fall back on using the main thread 1145 if (handler == null) { 1146 handler = new Handler(android.os.Looper.getMainLooper()); 1147 } 1148 handler.post(future); 1149 while (true) { 1150 try { 1151 return future.get(CAPTURE_TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS); 1152 } catch (ExecutionException e) { 1153 Throwable t = e.getCause(); 1154 if (t instanceof IllegalAccessException) { 1155 throw (IllegalAccessException)t; 1156 } 1157 if (t instanceof InvocationTargetException) { 1158 throw (InvocationTargetException)t; 1159 } 1160 throw new RuntimeException("Unexpected exception", t); 1161 } catch (InterruptedException e) { 1162 // Call get again 1163 } catch (CancellationException e) { 1164 throw new RuntimeException("Unexpected cancellation exception", e); 1165 } 1166 } 1167 } 1168 formatIntToHexString(int value)1169 private static String formatIntToHexString(int value) { 1170 return "0x" + Integer.toHexString(value).toUpperCase(); 1171 } 1172 exportMethods(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)1173 private static void exportMethods(Context context, Object view, BufferedWriter out, 1174 Class<?> klass, String prefix) throws IOException { 1175 1176 final Method[] methods = getExportedPropertyMethods(klass); 1177 int count = methods.length; 1178 for (int i = 0; i < count; i++) { 1179 final Method method = methods[i]; 1180 //noinspection EmptyCatchBlock 1181 try { 1182 Object methodValue = callMethodOnAppropriateTheadBlocking(method, view); 1183 final Class<?> returnType = method.getReturnType(); 1184 final ExportedProperty property = sAnnotations.get(method); 1185 String categoryPrefix = 1186 property.category().length() != 0 ? property.category() + ":" : ""; 1187 1188 if (returnType == int.class) { 1189 if (property.resolveId() && context != null) { 1190 final int id = (Integer) methodValue; 1191 methodValue = resolveId(context, id); 1192 } else { 1193 final FlagToString[] flagsMapping = property.flagMapping(); 1194 if (flagsMapping.length > 0) { 1195 final int intValue = (Integer) methodValue; 1196 final String valuePrefix = 1197 categoryPrefix + prefix + method.getName() + '_'; 1198 exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix); 1199 } 1200 1201 final IntToString[] mapping = property.mapping(); 1202 if (mapping.length > 0) { 1203 final int intValue = (Integer) methodValue; 1204 boolean mapped = false; 1205 int mappingCount = mapping.length; 1206 for (int j = 0; j < mappingCount; j++) { 1207 final IntToString mapper = mapping[j]; 1208 if (mapper.from() == intValue) { 1209 methodValue = mapper.to(); 1210 mapped = true; 1211 break; 1212 } 1213 } 1214 1215 if (!mapped) { 1216 methodValue = intValue; 1217 } 1218 } 1219 } 1220 } else if (returnType == int[].class) { 1221 final int[] array = (int[]) methodValue; 1222 final String valuePrefix = categoryPrefix + prefix + method.getName() + '_'; 1223 final String suffix = "()"; 1224 1225 exportUnrolledArray(context, out, property, array, valuePrefix, suffix); 1226 1227 continue; 1228 } else if (returnType == String[].class) { 1229 final String[] array = (String[]) methodValue; 1230 if (property.hasAdjacentMapping() && array != null) { 1231 for (int j = 0; j < array.length; j += 2) { 1232 if (array[j] != null) { 1233 writeEntry(out, categoryPrefix + prefix, array[j], "()", 1234 array[j + 1] == null ? "null" : array[j + 1]); 1235 } 1236 1237 } 1238 } 1239 1240 continue; 1241 } else if (!returnType.isPrimitive()) { 1242 if (property.deepExport()) { 1243 dumpViewProperties(context, methodValue, out, prefix + property.prefix()); 1244 continue; 1245 } 1246 } 1247 1248 writeEntry(out, categoryPrefix + prefix, method.getName(), "()", methodValue); 1249 } catch (IllegalAccessException e) { 1250 } catch (InvocationTargetException e) { 1251 } catch (TimeoutException e) { 1252 } 1253 } 1254 } 1255 exportFields(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)1256 private static void exportFields(Context context, Object view, BufferedWriter out, 1257 Class<?> klass, String prefix) throws IOException { 1258 1259 final Field[] fields = getExportedPropertyFields(klass); 1260 1261 int count = fields.length; 1262 for (int i = 0; i < count; i++) { 1263 final Field field = fields[i]; 1264 1265 //noinspection EmptyCatchBlock 1266 try { 1267 Object fieldValue = null; 1268 final Class<?> type = field.getType(); 1269 final ExportedProperty property = sAnnotations.get(field); 1270 String categoryPrefix = 1271 property.category().length() != 0 ? property.category() + ":" : ""; 1272 1273 if (type == int.class || type == byte.class) { 1274 if (property.resolveId() && context != null) { 1275 final int id = field.getInt(view); 1276 fieldValue = resolveId(context, id); 1277 } else { 1278 final FlagToString[] flagsMapping = property.flagMapping(); 1279 if (flagsMapping.length > 0) { 1280 final int intValue = field.getInt(view); 1281 final String valuePrefix = 1282 categoryPrefix + prefix + field.getName() + '_'; 1283 exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix); 1284 } 1285 1286 final IntToString[] mapping = property.mapping(); 1287 if (mapping.length > 0) { 1288 final int intValue = field.getInt(view); 1289 int mappingCount = mapping.length; 1290 for (int j = 0; j < mappingCount; j++) { 1291 final IntToString mapped = mapping[j]; 1292 if (mapped.from() == intValue) { 1293 fieldValue = mapped.to(); 1294 break; 1295 } 1296 } 1297 1298 if (fieldValue == null) { 1299 fieldValue = intValue; 1300 } 1301 } 1302 1303 if (property.formatToHexString()) { 1304 fieldValue = field.get(view); 1305 if (type == int.class) { 1306 fieldValue = formatIntToHexString((Integer) fieldValue); 1307 } else if (type == byte.class) { 1308 fieldValue = "0x" + Byte.toHexString((Byte) fieldValue, true); 1309 } 1310 } 1311 } 1312 } else if (type == int[].class) { 1313 final int[] array = (int[]) field.get(view); 1314 final String valuePrefix = categoryPrefix + prefix + field.getName() + '_'; 1315 final String suffix = ""; 1316 1317 exportUnrolledArray(context, out, property, array, valuePrefix, suffix); 1318 1319 continue; 1320 } else if (type == String[].class) { 1321 final String[] array = (String[]) field.get(view); 1322 if (property.hasAdjacentMapping() && array != null) { 1323 for (int j = 0; j < array.length; j += 2) { 1324 if (array[j] != null) { 1325 writeEntry(out, categoryPrefix + prefix, array[j], "", 1326 array[j + 1] == null ? "null" : array[j + 1]); 1327 } 1328 } 1329 } 1330 1331 continue; 1332 } else if (!type.isPrimitive()) { 1333 if (property.deepExport()) { 1334 dumpViewProperties(context, field.get(view), out, prefix + 1335 property.prefix()); 1336 continue; 1337 } 1338 } 1339 1340 if (fieldValue == null) { 1341 fieldValue = field.get(view); 1342 } 1343 1344 writeEntry(out, categoryPrefix + prefix, field.getName(), "", fieldValue); 1345 } catch (IllegalAccessException e) { 1346 } 1347 } 1348 } 1349 writeEntry(BufferedWriter out, String prefix, String name, String suffix, Object value)1350 private static void writeEntry(BufferedWriter out, String prefix, String name, 1351 String suffix, Object value) throws IOException { 1352 1353 out.write(prefix); 1354 out.write(name); 1355 out.write(suffix); 1356 out.write("="); 1357 writeValue(out, value); 1358 out.write(' '); 1359 } 1360 exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping, int intValue, String prefix)1361 private static void exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping, 1362 int intValue, String prefix) throws IOException { 1363 1364 final int count = mapping.length; 1365 for (int j = 0; j < count; j++) { 1366 final FlagToString flagMapping = mapping[j]; 1367 final boolean ifTrue = flagMapping.outputIf(); 1368 final int maskResult = intValue & flagMapping.mask(); 1369 final boolean test = maskResult == flagMapping.equals(); 1370 if ((test && ifTrue) || (!test && !ifTrue)) { 1371 final String name = flagMapping.name(); 1372 final String value = formatIntToHexString(maskResult); 1373 writeEntry(out, prefix, name, "", value); 1374 } 1375 } 1376 } 1377 exportUnrolledArray(Context context, BufferedWriter out, ExportedProperty property, int[] array, String prefix, String suffix)1378 private static void exportUnrolledArray(Context context, BufferedWriter out, 1379 ExportedProperty property, int[] array, String prefix, String suffix) 1380 throws IOException { 1381 1382 final IntToString[] indexMapping = property.indexMapping(); 1383 final boolean hasIndexMapping = indexMapping.length > 0; 1384 1385 final IntToString[] mapping = property.mapping(); 1386 final boolean hasMapping = mapping.length > 0; 1387 1388 final boolean resolveId = property.resolveId() && context != null; 1389 final int valuesCount = array.length; 1390 1391 for (int j = 0; j < valuesCount; j++) { 1392 String name; 1393 String value = null; 1394 1395 final int intValue = array[j]; 1396 1397 name = String.valueOf(j); 1398 if (hasIndexMapping) { 1399 int mappingCount = indexMapping.length; 1400 for (int k = 0; k < mappingCount; k++) { 1401 final IntToString mapped = indexMapping[k]; 1402 if (mapped.from() == j) { 1403 name = mapped.to(); 1404 break; 1405 } 1406 } 1407 } 1408 1409 if (hasMapping) { 1410 int mappingCount = mapping.length; 1411 for (int k = 0; k < mappingCount; k++) { 1412 final IntToString mapped = mapping[k]; 1413 if (mapped.from() == intValue) { 1414 value = mapped.to(); 1415 break; 1416 } 1417 } 1418 } 1419 1420 if (resolveId) { 1421 if (value == null) value = (String) resolveId(context, intValue); 1422 } else { 1423 value = String.valueOf(intValue); 1424 } 1425 1426 writeEntry(out, prefix, name, suffix, value); 1427 } 1428 } 1429 resolveId(Context context, int id)1430 static Object resolveId(Context context, int id) { 1431 Object fieldValue; 1432 final Resources resources = context.getResources(); 1433 if (id >= 0) { 1434 try { 1435 fieldValue = resources.getResourceTypeName(id) + '/' + 1436 resources.getResourceEntryName(id); 1437 } catch (Resources.NotFoundException e) { 1438 fieldValue = "id/" + formatIntToHexString(id); 1439 } 1440 } else { 1441 fieldValue = "NO_ID"; 1442 } 1443 return fieldValue; 1444 } 1445 writeValue(BufferedWriter out, Object value)1446 private static void writeValue(BufferedWriter out, Object value) throws IOException { 1447 if (value != null) { 1448 String output = "[EXCEPTION]"; 1449 try { 1450 output = value.toString().replace("\n", "\\n"); 1451 } finally { 1452 out.write(String.valueOf(output.length())); 1453 out.write(","); 1454 out.write(output); 1455 } 1456 } else { 1457 out.write("4,null"); 1458 } 1459 } 1460 capturedViewGetPropertyFields(Class<?> klass)1461 private static Field[] capturedViewGetPropertyFields(Class<?> klass) { 1462 if (mCapturedViewFieldsForClasses == null) { 1463 mCapturedViewFieldsForClasses = new HashMap<Class<?>, Field[]>(); 1464 } 1465 final HashMap<Class<?>, Field[]> map = mCapturedViewFieldsForClasses; 1466 1467 Field[] fields = map.get(klass); 1468 if (fields != null) { 1469 return fields; 1470 } 1471 1472 final ArrayList<Field> foundFields = new ArrayList<Field>(); 1473 fields = klass.getFields(); 1474 1475 int count = fields.length; 1476 for (int i = 0; i < count; i++) { 1477 final Field field = fields[i]; 1478 if (field.isAnnotationPresent(CapturedViewProperty.class)) { 1479 field.setAccessible(true); 1480 foundFields.add(field); 1481 } 1482 } 1483 1484 fields = foundFields.toArray(new Field[foundFields.size()]); 1485 map.put(klass, fields); 1486 1487 return fields; 1488 } 1489 capturedViewGetPropertyMethods(Class<?> klass)1490 private static Method[] capturedViewGetPropertyMethods(Class<?> klass) { 1491 if (mCapturedViewMethodsForClasses == null) { 1492 mCapturedViewMethodsForClasses = new HashMap<Class<?>, Method[]>(); 1493 } 1494 final HashMap<Class<?>, Method[]> map = mCapturedViewMethodsForClasses; 1495 1496 Method[] methods = map.get(klass); 1497 if (methods != null) { 1498 return methods; 1499 } 1500 1501 final ArrayList<Method> foundMethods = new ArrayList<Method>(); 1502 methods = klass.getMethods(); 1503 1504 int count = methods.length; 1505 for (int i = 0; i < count; i++) { 1506 final Method method = methods[i]; 1507 if (method.getParameterTypes().length == 0 && 1508 method.isAnnotationPresent(CapturedViewProperty.class) && 1509 method.getReturnType() != Void.class) { 1510 method.setAccessible(true); 1511 foundMethods.add(method); 1512 } 1513 } 1514 1515 methods = foundMethods.toArray(new Method[foundMethods.size()]); 1516 map.put(klass, methods); 1517 1518 return methods; 1519 } 1520 capturedViewExportMethods(Object obj, Class<?> klass, String prefix)1521 private static String capturedViewExportMethods(Object obj, Class<?> klass, 1522 String prefix) { 1523 1524 if (obj == null) { 1525 return "null"; 1526 } 1527 1528 StringBuilder sb = new StringBuilder(); 1529 final Method[] methods = capturedViewGetPropertyMethods(klass); 1530 1531 int count = methods.length; 1532 for (int i = 0; i < count; i++) { 1533 final Method method = methods[i]; 1534 try { 1535 Object methodValue = method.invoke(obj, (Object[]) null); 1536 final Class<?> returnType = method.getReturnType(); 1537 1538 CapturedViewProperty property = method.getAnnotation(CapturedViewProperty.class); 1539 if (property.retrieveReturn()) { 1540 //we are interested in the second level data only 1541 sb.append(capturedViewExportMethods(methodValue, returnType, method.getName() + "#")); 1542 } else { 1543 sb.append(prefix); 1544 sb.append(method.getName()); 1545 sb.append("()="); 1546 1547 if (methodValue != null) { 1548 final String value = methodValue.toString().replace("\n", "\\n"); 1549 sb.append(value); 1550 } else { 1551 sb.append("null"); 1552 } 1553 sb.append("; "); 1554 } 1555 } catch (IllegalAccessException e) { 1556 //Exception IllegalAccess, it is OK here 1557 //we simply ignore this method 1558 } catch (InvocationTargetException e) { 1559 //Exception InvocationTarget, it is OK here 1560 //we simply ignore this method 1561 } 1562 } 1563 return sb.toString(); 1564 } 1565 capturedViewExportFields(Object obj, Class<?> klass, String prefix)1566 private static String capturedViewExportFields(Object obj, Class<?> klass, String prefix) { 1567 if (obj == null) { 1568 return "null"; 1569 } 1570 1571 StringBuilder sb = new StringBuilder(); 1572 final Field[] fields = capturedViewGetPropertyFields(klass); 1573 1574 int count = fields.length; 1575 for (int i = 0; i < count; i++) { 1576 final Field field = fields[i]; 1577 try { 1578 Object fieldValue = field.get(obj); 1579 1580 sb.append(prefix); 1581 sb.append(field.getName()); 1582 sb.append("="); 1583 1584 if (fieldValue != null) { 1585 final String value = fieldValue.toString().replace("\n", "\\n"); 1586 sb.append(value); 1587 } else { 1588 sb.append("null"); 1589 } 1590 sb.append(' '); 1591 } catch (IllegalAccessException e) { 1592 //Exception IllegalAccess, it is OK here 1593 //we simply ignore this field 1594 } 1595 } 1596 return sb.toString(); 1597 } 1598 1599 /** 1600 * Dump view info for id based instrument test generation 1601 * (and possibly further data analysis). The results are dumped 1602 * to the log. 1603 * @param tag for log 1604 * @param view for dump 1605 */ dumpCapturedView(String tag, Object view)1606 public static void dumpCapturedView(String tag, Object view) { 1607 Class<?> klass = view.getClass(); 1608 StringBuilder sb = new StringBuilder(klass.getName() + ": "); 1609 sb.append(capturedViewExportFields(view, klass, "")); 1610 sb.append(capturedViewExportMethods(view, klass, "")); 1611 Log.d(tag, sb.toString()); 1612 } 1613 1614 /** 1615 * Invoke a particular method on given view. 1616 * The given method is always invoked on the UI thread. The caller thread will stall until the 1617 * method invocation is complete. Returns an object equal to the result of the method 1618 * invocation, null if the method is declared to return void 1619 * @throws Exception if the method invocation caused any exception 1620 * @hide 1621 */ invokeViewMethod(final View view, final Method method, final Object[] args)1622 public static Object invokeViewMethod(final View view, final Method method, 1623 final Object[] args) { 1624 final CountDownLatch latch = new CountDownLatch(1); 1625 final AtomicReference<Object> result = new AtomicReference<Object>(); 1626 final AtomicReference<Throwable> exception = new AtomicReference<Throwable>(); 1627 1628 view.post(new Runnable() { 1629 @Override 1630 public void run() { 1631 try { 1632 result.set(method.invoke(view, args)); 1633 } catch (InvocationTargetException e) { 1634 exception.set(e.getCause()); 1635 } catch (Exception e) { 1636 exception.set(e); 1637 } 1638 1639 latch.countDown(); 1640 } 1641 }); 1642 1643 try { 1644 latch.await(); 1645 } catch (InterruptedException e) { 1646 throw new RuntimeException(e); 1647 } 1648 1649 if (exception.get() != null) { 1650 throw new RuntimeException(exception.get()); 1651 } 1652 1653 return result.get(); 1654 } 1655 1656 /** 1657 * @hide 1658 */ setLayoutParameter(final View view, final String param, final int value)1659 public static void setLayoutParameter(final View view, final String param, final int value) 1660 throws NoSuchFieldException, IllegalAccessException { 1661 final ViewGroup.LayoutParams p = view.getLayoutParams(); 1662 final Field f = p.getClass().getField(param); 1663 if (f.getType() != int.class) { 1664 throw new RuntimeException("Only integer layout parameters can be set. Field " 1665 + param + " is of type " + f.getType().getSimpleName()); 1666 } 1667 1668 f.set(p, Integer.valueOf(value)); 1669 1670 view.post(new Runnable() { 1671 @Override 1672 public void run() { 1673 view.setLayoutParams(p); 1674 } 1675 }); 1676 } 1677 } 1678