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