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