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