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