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