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