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.RemoteException; 26 import android.util.DisplayMetrics; 27 import android.util.Log; 28 29 import java.io.BufferedOutputStream; 30 import java.io.BufferedWriter; 31 import java.io.ByteArrayOutputStream; 32 import java.io.DataOutputStream; 33 import java.io.IOException; 34 import java.io.OutputStream; 35 import java.io.OutputStreamWriter; 36 import java.lang.annotation.ElementType; 37 import java.lang.annotation.Retention; 38 import java.lang.annotation.RetentionPolicy; 39 import java.lang.annotation.Target; 40 import java.lang.reflect.AccessibleObject; 41 import java.lang.reflect.Field; 42 import java.lang.reflect.InvocationTargetException; 43 import java.lang.reflect.Method; 44 import java.util.ArrayList; 45 import java.util.HashMap; 46 import java.util.concurrent.CountDownLatch; 47 import java.util.concurrent.TimeUnit; 48 49 /** 50 * Various debugging/tracing tools related to {@link View} and the view hierarchy. 51 */ 52 public class ViewDebug { 53 /** 54 * @deprecated This flag is now unused 55 */ 56 @Deprecated 57 public static final boolean TRACE_HIERARCHY = false; 58 59 /** 60 * @deprecated This flag is now unused 61 */ 62 @Deprecated 63 public static final boolean TRACE_RECYCLER = false; 64 65 /** 66 * Enables detailed logging of drag/drop operations. 67 * @hide 68 */ 69 public static final boolean DEBUG_DRAG = false; 70 71 /** 72 * This annotation can be used to mark fields and methods to be dumped by 73 * the view server. Only non-void methods with no arguments can be annotated 74 * by this annotation. 75 */ 76 @Target({ ElementType.FIELD, ElementType.METHOD }) 77 @Retention(RetentionPolicy.RUNTIME) 78 public @interface ExportedProperty { 79 /** 80 * When resolveId is true, and if the annotated field/method return value 81 * is an int, the value is converted to an Android's resource name. 82 * 83 * @return true if the property's value must be transformed into an Android 84 * resource name, false otherwise 85 */ resolveId()86 boolean resolveId() default false; 87 88 /** 89 * A mapping can be defined to map int values to specific strings. For 90 * instance, View.getVisibility() returns 0, 4 or 8. However, these values 91 * actually mean VISIBLE, INVISIBLE and GONE. A mapping can be used to see 92 * these human readable values: 93 * 94 * <pre> 95 * @ViewDebug.ExportedProperty(mapping = { 96 * @ViewDebug.IntToString(from = 0, to = "VISIBLE"), 97 * @ViewDebug.IntToString(from = 4, to = "INVISIBLE"), 98 * @ViewDebug.IntToString(from = 8, to = "GONE") 99 * }) 100 * public int getVisibility() { ... 101 * <pre> 102 * 103 * @return An array of int to String mappings 104 * 105 * @see android.view.ViewDebug.IntToString 106 */ mapping()107 IntToString[] mapping() default { }; 108 109 /** 110 * A mapping can be defined to map array indices to specific strings. 111 * A mapping can be used to see human readable values for the indices 112 * of an array: 113 * 114 * <pre> 115 * @ViewDebug.ExportedProperty(indexMapping = { 116 * @ViewDebug.IntToString(from = 0, to = "INVALID"), 117 * @ViewDebug.IntToString(from = 1, to = "FIRST"), 118 * @ViewDebug.IntToString(from = 2, to = "SECOND") 119 * }) 120 * private int[] mElements; 121 * <pre> 122 * 123 * @return An array of int to String mappings 124 * 125 * @see android.view.ViewDebug.IntToString 126 * @see #mapping() 127 */ indexMapping()128 IntToString[] indexMapping() default { }; 129 130 /** 131 * A flags mapping can be defined to map flags encoded in an integer to 132 * specific strings. A mapping can be used to see human readable values 133 * for the flags of an integer: 134 * 135 * <pre> 136 * @ViewDebug.ExportedProperty(flagMapping = { 137 * @ViewDebug.FlagToString(mask = ENABLED_MASK, equals = ENABLED, name = "ENABLED"), 138 * @ViewDebug.FlagToString(mask = ENABLED_MASK, equals = DISABLED, name = "DISABLED"), 139 * }) 140 * private int mFlags; 141 * <pre> 142 * 143 * A specified String is output when the following is true: 144 * 145 * @return An array of int to String mappings 146 */ flagMapping()147 FlagToString[] flagMapping() default { }; 148 149 /** 150 * When deep export is turned on, this property is not dumped. Instead, the 151 * properties contained in this property are dumped. Each child property 152 * is prefixed with the name of this property. 153 * 154 * @return true if the properties of this property should be dumped 155 * 156 * @see #prefix() 157 */ deepExport()158 boolean deepExport() default false; 159 160 /** 161 * The prefix to use on child properties when deep export is enabled 162 * 163 * @return a prefix as a String 164 * 165 * @see #deepExport() 166 */ prefix()167 String prefix() default ""; 168 169 /** 170 * Specifies the category the property falls into, such as measurement, 171 * layout, drawing, etc. 172 * 173 * @return the category as String 174 */ category()175 String category() default ""; 176 } 177 178 /** 179 * Defines a mapping from an int value to a String. Such a mapping can be used 180 * in an @ExportedProperty to provide more meaningful values to the end user. 181 * 182 * @see android.view.ViewDebug.ExportedProperty 183 */ 184 @Target({ ElementType.TYPE }) 185 @Retention(RetentionPolicy.RUNTIME) 186 public @interface IntToString { 187 /** 188 * The original int value to map to a String. 189 * 190 * @return An arbitrary int value. 191 */ from()192 int from(); 193 194 /** 195 * The String to use in place of the original int value. 196 * 197 * @return An arbitrary non-null String. 198 */ to()199 String to(); 200 } 201 202 /** 203 * Defines a mapping from a flag to a String. Such a mapping can be used 204 * in an @ExportedProperty to provide more meaningful values to the end user. 205 * 206 * @see android.view.ViewDebug.ExportedProperty 207 */ 208 @Target({ ElementType.TYPE }) 209 @Retention(RetentionPolicy.RUNTIME) 210 public @interface FlagToString { 211 /** 212 * The mask to apply to the original value. 213 * 214 * @return An arbitrary int value. 215 */ mask()216 int mask(); 217 218 /** 219 * The value to compare to the result of: 220 * <code>original value & {@link #mask()}</code>. 221 * 222 * @return An arbitrary value. 223 */ equals()224 int equals(); 225 226 /** 227 * The String to use in place of the original int value. 228 * 229 * @return An arbitrary non-null String. 230 */ name()231 String name(); 232 233 /** 234 * Indicates whether to output the flag when the test is true, 235 * or false. Defaults to true. 236 */ outputIf()237 boolean outputIf() default true; 238 } 239 240 /** 241 * This annotation can be used to mark fields and methods to be dumped when 242 * the view is captured. Methods with this annotation must have no arguments 243 * and must return a valid type of data. 244 */ 245 @Target({ ElementType.FIELD, ElementType.METHOD }) 246 @Retention(RetentionPolicy.RUNTIME) 247 public @interface CapturedViewProperty { 248 /** 249 * When retrieveReturn is true, we need to retrieve second level methods 250 * e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod() 251 * we will set retrieveReturn = true on the annotation of 252 * myView.getFirstLevelMethod() 253 * @return true if we need the second level methods 254 */ retrieveReturn()255 boolean retrieveReturn() default false; 256 } 257 258 private static HashMap<Class<?>, Method[]> mCapturedViewMethodsForClasses = null; 259 private static HashMap<Class<?>, Field[]> mCapturedViewFieldsForClasses = null; 260 261 // Maximum delay in ms after which we stop trying to capture a View's drawing 262 private static final int CAPTURE_TIMEOUT = 4000; 263 264 private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE"; 265 private static final String REMOTE_COMMAND_DUMP = "DUMP"; 266 private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE"; 267 private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT"; 268 private static final String REMOTE_PROFILE = "PROFILE"; 269 private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS"; 270 private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST"; 271 272 private static HashMap<Class<?>, Field[]> sFieldsForClasses; 273 private static HashMap<Class<?>, Method[]> sMethodsForClasses; 274 private static HashMap<AccessibleObject, ExportedProperty> sAnnotations; 275 276 /** 277 * @deprecated This enum is now unused 278 */ 279 @Deprecated 280 public enum HierarchyTraceType { 281 INVALIDATE, 282 INVALIDATE_CHILD, 283 INVALIDATE_CHILD_IN_PARENT, 284 REQUEST_LAYOUT, 285 ON_LAYOUT, 286 ON_MEASURE, 287 DRAW, 288 BUILD_CACHE 289 } 290 291 /** 292 * @deprecated This enum is now unused 293 */ 294 @Deprecated 295 public enum RecyclerTraceType { 296 NEW_VIEW, 297 BIND_VIEW, 298 RECYCLE_FROM_ACTIVE_HEAP, 299 RECYCLE_FROM_SCRAP_HEAP, 300 MOVE_TO_SCRAP_HEAP, 301 MOVE_FROM_ACTIVE_TO_SCRAP_HEAP 302 } 303 304 /** 305 * Returns the number of instanciated Views. 306 * 307 * @return The number of Views instanciated in the current process. 308 * 309 * @hide 310 */ getViewInstanceCount()311 public static long getViewInstanceCount() { 312 return Debug.countInstancesOfClass(View.class); 313 } 314 315 /** 316 * Returns the number of instanciated ViewAncestors. 317 * 318 * @return The number of ViewAncestors instanciated in the current process. 319 * 320 * @hide 321 */ getViewRootImplCount()322 public static long getViewRootImplCount() { 323 return Debug.countInstancesOfClass(ViewRootImpl.class); 324 } 325 326 /** 327 * @deprecated This method is now unused and invoking it is a no-op 328 */ 329 @Deprecated 330 @SuppressWarnings({ "UnusedParameters", "deprecation" }) trace(View view, RecyclerTraceType type, int... parameters)331 public static void trace(View view, RecyclerTraceType type, int... parameters) { 332 } 333 334 /** 335 * @deprecated This method is now unused and invoking it is a no-op 336 */ 337 @Deprecated 338 @SuppressWarnings("UnusedParameters") startRecyclerTracing(String prefix, View view)339 public static void startRecyclerTracing(String prefix, View view) { 340 } 341 342 /** 343 * @deprecated This method is now unused and invoking it is a no-op 344 */ 345 @Deprecated 346 @SuppressWarnings("UnusedParameters") stopRecyclerTracing()347 public static void stopRecyclerTracing() { 348 } 349 350 /** 351 * @deprecated This method is now unused and invoking it is a no-op 352 */ 353 @Deprecated 354 @SuppressWarnings({ "UnusedParameters", "deprecation" }) trace(View view, HierarchyTraceType type)355 public static void trace(View view, HierarchyTraceType type) { 356 } 357 358 /** 359 * @deprecated This method is now unused and invoking it is a no-op 360 */ 361 @Deprecated 362 @SuppressWarnings("UnusedParameters") startHierarchyTracing(String prefix, View view)363 public static void startHierarchyTracing(String prefix, View view) { 364 } 365 366 /** 367 * @deprecated This method is now unused and invoking it is a no-op 368 */ 369 @Deprecated stopHierarchyTracing()370 public static void stopHierarchyTracing() { 371 } 372 dispatchCommand(View view, String command, String parameters, OutputStream clientStream)373 static void dispatchCommand(View view, String command, String parameters, 374 OutputStream clientStream) throws IOException { 375 376 // Paranoid but safe... 377 view = view.getRootView(); 378 379 if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) { 380 dump(view, clientStream); 381 } else if (REMOTE_COMMAND_CAPTURE_LAYERS.equalsIgnoreCase(command)) { 382 captureLayers(view, new DataOutputStream(clientStream)); 383 } else { 384 final String[] params = parameters.split(" "); 385 if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) { 386 capture(view, clientStream, params[0]); 387 } else if (REMOTE_COMMAND_OUTPUT_DISPLAYLIST.equalsIgnoreCase(command)) { 388 outputDisplayList(view, params[0]); 389 } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) { 390 invalidate(view, params[0]); 391 } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) { 392 requestLayout(view, params[0]); 393 } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) { 394 profile(view, clientStream, params[0]); 395 } 396 } 397 } 398 findView(View root, String parameter)399 private static View findView(View root, String parameter) { 400 // Look by type/hashcode 401 if (parameter.indexOf('@') != -1) { 402 final String[] ids = parameter.split("@"); 403 final String className = ids[0]; 404 final int hashCode = (int) Long.parseLong(ids[1], 16); 405 406 View view = root.getRootView(); 407 if (view instanceof ViewGroup) { 408 return findView((ViewGroup) view, className, hashCode); 409 } 410 } else { 411 // Look by id 412 final int id = root.getResources().getIdentifier(parameter, null, null); 413 return root.getRootView().findViewById(id); 414 } 415 416 return null; 417 } 418 invalidate(View root, String parameter)419 private static void invalidate(View root, String parameter) { 420 final View view = findView(root, parameter); 421 if (view != null) { 422 view.postInvalidate(); 423 } 424 } 425 requestLayout(View root, String parameter)426 private static void requestLayout(View root, String parameter) { 427 final View view = findView(root, parameter); 428 if (view != null) { 429 root.post(new Runnable() { 430 public void run() { 431 view.requestLayout(); 432 } 433 }); 434 } 435 } 436 profile(View root, OutputStream clientStream, String parameter)437 private static void profile(View root, OutputStream clientStream, String parameter) 438 throws IOException { 439 440 final View view = findView(root, parameter); 441 BufferedWriter out = null; 442 try { 443 out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024); 444 445 if (view != null) { 446 profileViewAndChildren(view, out); 447 } else { 448 out.write("-1 -1 -1"); 449 out.newLine(); 450 } 451 out.write("DONE."); 452 out.newLine(); 453 } catch (Exception e) { 454 android.util.Log.w("View", "Problem profiling the view:", e); 455 } finally { 456 if (out != null) { 457 out.close(); 458 } 459 } 460 } 461 profileViewAndChildren(final View view, BufferedWriter out)462 private static void profileViewAndChildren(final View view, BufferedWriter out) 463 throws IOException { 464 profileViewAndChildren(view, out, true); 465 } 466 profileViewAndChildren(final View view, BufferedWriter out, boolean root)467 private static void profileViewAndChildren(final View view, BufferedWriter out, boolean root) 468 throws IOException { 469 470 long durationMeasure = 471 (root || (view.mPrivateFlags & View.MEASURED_DIMENSION_SET) != 0) ? profileViewOperation( 472 view, new ViewOperation<Void>() { 473 public Void[] pre() { 474 forceLayout(view); 475 return null; 476 } 477 478 private void forceLayout(View view) { 479 view.forceLayout(); 480 if (view instanceof ViewGroup) { 481 ViewGroup group = (ViewGroup) view; 482 final int count = group.getChildCount(); 483 for (int i = 0; i < count; i++) { 484 forceLayout(group.getChildAt(i)); 485 } 486 } 487 } 488 489 public void run(Void... data) { 490 view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec); 491 } 492 493 public void post(Void... data) { 494 } 495 }) 496 : 0; 497 long durationLayout = 498 (root || (view.mPrivateFlags & View.LAYOUT_REQUIRED) != 0) ? profileViewOperation( 499 view, new ViewOperation<Void>() { 500 public Void[] pre() { 501 return null; 502 } 503 504 public void run(Void... data) { 505 view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom); 506 } 507 508 public void post(Void... data) { 509 } 510 }) : 0; 511 long durationDraw = 512 (root || !view.willNotDraw() || (view.mPrivateFlags & View.DRAWN) != 0) ? profileViewOperation( 513 view, 514 new ViewOperation<Object>() { 515 public Object[] pre() { 516 final DisplayMetrics metrics = 517 (view != null && view.getResources() != null) ? 518 view.getResources().getDisplayMetrics() : null; 519 final Bitmap bitmap = metrics != null ? 520 Bitmap.createBitmap(metrics.widthPixels, 521 metrics.heightPixels, Bitmap.Config.RGB_565) : null; 522 final Canvas canvas = bitmap != null ? new Canvas(bitmap) : null; 523 return new Object[] { 524 bitmap, canvas 525 }; 526 } 527 528 public void run(Object... data) { 529 if (data[1] != null) { 530 view.draw((Canvas) data[1]); 531 } 532 } 533 534 public void post(Object... data) { 535 if (data[1] != null) { 536 ((Canvas) data[1]).setBitmap(null); 537 } 538 if (data[0] != null) { 539 ((Bitmap) data[0]).recycle(); 540 } 541 } 542 }) : 0; 543 out.write(String.valueOf(durationMeasure)); 544 out.write(' '); 545 out.write(String.valueOf(durationLayout)); 546 out.write(' '); 547 out.write(String.valueOf(durationDraw)); 548 out.newLine(); 549 if (view instanceof ViewGroup) { 550 ViewGroup group = (ViewGroup) view; 551 final int count = group.getChildCount(); 552 for (int i = 0; i < count; i++) { 553 profileViewAndChildren(group.getChildAt(i), out, false); 554 } 555 } 556 } 557 558 interface ViewOperation<T> { pre()559 T[] pre(); run(T... data)560 void run(T... data); post(T... data)561 void post(T... data); 562 } 563 profileViewOperation(View view, final ViewOperation<T> operation)564 private static <T> long profileViewOperation(View view, final ViewOperation<T> operation) { 565 final CountDownLatch latch = new CountDownLatch(1); 566 final long[] duration = new long[1]; 567 568 view.post(new Runnable() { 569 public void run() { 570 try { 571 T[] data = operation.pre(); 572 long start = Debug.threadCpuTimeNanos(); 573 //noinspection unchecked 574 operation.run(data); 575 duration[0] = Debug.threadCpuTimeNanos() - start; 576 //noinspection unchecked 577 operation.post(data); 578 } finally { 579 latch.countDown(); 580 } 581 } 582 }); 583 584 try { 585 if (!latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS)) { 586 Log.w("View", "Could not complete the profiling of the view " + view); 587 return -1; 588 } 589 } catch (InterruptedException e) { 590 Log.w("View", "Could not complete the profiling of the view " + view); 591 Thread.currentThread().interrupt(); 592 return -1; 593 } 594 595 return duration[0]; 596 } 597 captureLayers(View root, final DataOutputStream clientStream)598 private static void captureLayers(View root, final DataOutputStream clientStream) 599 throws IOException { 600 601 try { 602 Rect outRect = new Rect(); 603 try { 604 root.mAttachInfo.mSession.getDisplayFrame(root.mAttachInfo.mWindow, outRect); 605 } catch (RemoteException e) { 606 // Ignore 607 } 608 609 clientStream.writeInt(outRect.width()); 610 clientStream.writeInt(outRect.height()); 611 612 captureViewLayer(root, clientStream, true); 613 614 clientStream.write(2); 615 } finally { 616 clientStream.close(); 617 } 618 } 619 captureViewLayer(View view, DataOutputStream clientStream, boolean visible)620 private static void captureViewLayer(View view, DataOutputStream clientStream, boolean visible) 621 throws IOException { 622 623 final boolean localVisible = view.getVisibility() == View.VISIBLE && visible; 624 625 if ((view.mPrivateFlags & View.SKIP_DRAW) != View.SKIP_DRAW) { 626 final int id = view.getId(); 627 String name = view.getClass().getSimpleName(); 628 if (id != View.NO_ID) { 629 name = resolveId(view.getContext(), id).toString(); 630 } 631 632 clientStream.write(1); 633 clientStream.writeUTF(name); 634 clientStream.writeByte(localVisible ? 1 : 0); 635 636 int[] position = new int[2]; 637 // XXX: Should happen on the UI thread 638 view.getLocationInWindow(position); 639 640 clientStream.writeInt(position[0]); 641 clientStream.writeInt(position[1]); 642 clientStream.flush(); 643 644 Bitmap b = performViewCapture(view, true); 645 if (b != null) { 646 ByteArrayOutputStream arrayOut = new ByteArrayOutputStream(b.getWidth() * 647 b.getHeight() * 2); 648 b.compress(Bitmap.CompressFormat.PNG, 100, arrayOut); 649 clientStream.writeInt(arrayOut.size()); 650 arrayOut.writeTo(clientStream); 651 } 652 clientStream.flush(); 653 } 654 655 if (view instanceof ViewGroup) { 656 ViewGroup group = (ViewGroup) view; 657 int count = group.getChildCount(); 658 659 for (int i = 0; i < count; i++) { 660 captureViewLayer(group.getChildAt(i), clientStream, localVisible); 661 } 662 } 663 } 664 outputDisplayList(View root, String parameter)665 private static void outputDisplayList(View root, String parameter) throws IOException { 666 final View view = findView(root, parameter); 667 view.getViewRootImpl().outputDisplayList(view); 668 } 669 capture(View root, final OutputStream clientStream, String parameter)670 private static void capture(View root, final OutputStream clientStream, String parameter) 671 throws IOException { 672 673 final View captureView = findView(root, parameter); 674 Bitmap b = performViewCapture(captureView, false); 675 676 if (b == null) { 677 Log.w("View", "Failed to create capture bitmap!"); 678 // Send an empty one so that it doesn't get stuck waiting for 679 // something. 680 b = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); 681 } 682 683 BufferedOutputStream out = null; 684 try { 685 out = new BufferedOutputStream(clientStream, 32 * 1024); 686 b.compress(Bitmap.CompressFormat.PNG, 100, out); 687 out.flush(); 688 } finally { 689 if (out != null) { 690 out.close(); 691 } 692 b.recycle(); 693 } 694 } 695 performViewCapture(final View captureView, final boolean skpiChildren)696 private static Bitmap performViewCapture(final View captureView, final boolean skpiChildren) { 697 if (captureView != null) { 698 final CountDownLatch latch = new CountDownLatch(1); 699 final Bitmap[] cache = new Bitmap[1]; 700 701 captureView.post(new Runnable() { 702 public void run() { 703 try { 704 cache[0] = captureView.createSnapshot( 705 Bitmap.Config.ARGB_8888, 0, skpiChildren); 706 } catch (OutOfMemoryError e) { 707 Log.w("View", "Out of memory for bitmap"); 708 } finally { 709 latch.countDown(); 710 } 711 } 712 }); 713 714 try { 715 latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS); 716 return cache[0]; 717 } catch (InterruptedException e) { 718 Log.w("View", "Could not complete the capture of the view " + captureView); 719 Thread.currentThread().interrupt(); 720 } 721 } 722 723 return null; 724 } 725 dump(View root, OutputStream clientStream)726 private static void dump(View root, OutputStream clientStream) throws IOException { 727 BufferedWriter out = null; 728 try { 729 out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024); 730 View view = root.getRootView(); 731 if (view instanceof ViewGroup) { 732 ViewGroup group = (ViewGroup) view; 733 dumpViewHierarchyWithProperties(group.getContext(), group, out, 0); 734 } 735 out.write("DONE."); 736 out.newLine(); 737 } catch (Exception e) { 738 android.util.Log.w("View", "Problem dumping the view:", e); 739 } finally { 740 if (out != null) { 741 out.close(); 742 } 743 } 744 } 745 findView(ViewGroup group, String className, int hashCode)746 private static View findView(ViewGroup group, String className, int hashCode) { 747 if (isRequestedView(group, className, hashCode)) { 748 return group; 749 } 750 751 final int count = group.getChildCount(); 752 for (int i = 0; i < count; i++) { 753 final View view = group.getChildAt(i); 754 if (view instanceof ViewGroup) { 755 final View found = findView((ViewGroup) view, className, hashCode); 756 if (found != null) { 757 return found; 758 } 759 } else if (isRequestedView(view, className, hashCode)) { 760 return view; 761 } 762 } 763 764 return null; 765 } 766 isRequestedView(View view, String className, int hashCode)767 private static boolean isRequestedView(View view, String className, int hashCode) { 768 return view.getClass().getName().equals(className) && view.hashCode() == hashCode; 769 } 770 dumpViewHierarchyWithProperties(Context context, ViewGroup group, BufferedWriter out, int level)771 private static void dumpViewHierarchyWithProperties(Context context, ViewGroup group, 772 BufferedWriter out, int level) { 773 if (!dumpViewWithProperties(context, group, out, level)) { 774 return; 775 } 776 777 final int count = group.getChildCount(); 778 for (int i = 0; i < count; i++) { 779 final View view = group.getChildAt(i); 780 if (view instanceof ViewGroup) { 781 dumpViewHierarchyWithProperties(context, (ViewGroup) view, out, level + 1); 782 } else { 783 dumpViewWithProperties(context, view, out, level + 1); 784 } 785 } 786 } 787 dumpViewWithProperties(Context context, View view, BufferedWriter out, int level)788 private static boolean dumpViewWithProperties(Context context, View view, 789 BufferedWriter out, int level) { 790 791 try { 792 for (int i = 0; i < level; i++) { 793 out.write(' '); 794 } 795 out.write(view.getClass().getName()); 796 out.write('@'); 797 out.write(Integer.toHexString(view.hashCode())); 798 out.write(' '); 799 dumpViewProperties(context, view, out); 800 out.newLine(); 801 } catch (IOException e) { 802 Log.w("View", "Error while dumping hierarchy tree"); 803 return false; 804 } 805 return true; 806 } 807 getExportedPropertyFields(Class<?> klass)808 private static Field[] getExportedPropertyFields(Class<?> klass) { 809 if (sFieldsForClasses == null) { 810 sFieldsForClasses = new HashMap<Class<?>, Field[]>(); 811 } 812 if (sAnnotations == null) { 813 sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512); 814 } 815 816 final HashMap<Class<?>, Field[]> map = sFieldsForClasses; 817 818 Field[] fields = map.get(klass); 819 if (fields != null) { 820 return fields; 821 } 822 823 final ArrayList<Field> foundFields = new ArrayList<Field>(); 824 fields = klass.getDeclaredFields(); 825 826 int count = fields.length; 827 for (int i = 0; i < count; i++) { 828 final Field field = fields[i]; 829 if (field.isAnnotationPresent(ExportedProperty.class)) { 830 field.setAccessible(true); 831 foundFields.add(field); 832 sAnnotations.put(field, field.getAnnotation(ExportedProperty.class)); 833 } 834 } 835 836 fields = foundFields.toArray(new Field[foundFields.size()]); 837 map.put(klass, fields); 838 839 return fields; 840 } 841 getExportedPropertyMethods(Class<?> klass)842 private static Method[] getExportedPropertyMethods(Class<?> klass) { 843 if (sMethodsForClasses == null) { 844 sMethodsForClasses = new HashMap<Class<?>, Method[]>(100); 845 } 846 if (sAnnotations == null) { 847 sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512); 848 } 849 850 final HashMap<Class<?>, Method[]> map = sMethodsForClasses; 851 852 Method[] methods = map.get(klass); 853 if (methods != null) { 854 return methods; 855 } 856 857 final ArrayList<Method> foundMethods = new ArrayList<Method>(); 858 methods = klass.getDeclaredMethods(); 859 860 int count = methods.length; 861 for (int i = 0; i < count; i++) { 862 final Method method = methods[i]; 863 if (method.getParameterTypes().length == 0 && 864 method.isAnnotationPresent(ExportedProperty.class) && 865 method.getReturnType() != Void.class) { 866 method.setAccessible(true); 867 foundMethods.add(method); 868 sAnnotations.put(method, method.getAnnotation(ExportedProperty.class)); 869 } 870 } 871 872 methods = foundMethods.toArray(new Method[foundMethods.size()]); 873 map.put(klass, methods); 874 875 return methods; 876 } 877 dumpViewProperties(Context context, Object view, BufferedWriter out)878 private static void dumpViewProperties(Context context, Object view, 879 BufferedWriter out) throws IOException { 880 881 dumpViewProperties(context, view, out, ""); 882 } 883 dumpViewProperties(Context context, Object view, BufferedWriter out, String prefix)884 private static void dumpViewProperties(Context context, Object view, 885 BufferedWriter out, String prefix) throws IOException { 886 887 Class<?> klass = view.getClass(); 888 889 do { 890 exportFields(context, view, out, klass, prefix); 891 exportMethods(context, view, out, klass, prefix); 892 klass = klass.getSuperclass(); 893 } while (klass != Object.class); 894 } 895 exportMethods(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)896 private static void exportMethods(Context context, Object view, BufferedWriter out, 897 Class<?> klass, String prefix) throws IOException { 898 899 final Method[] methods = getExportedPropertyMethods(klass); 900 901 int count = methods.length; 902 for (int i = 0; i < count; i++) { 903 final Method method = methods[i]; 904 //noinspection EmptyCatchBlock 905 try { 906 // TODO: This should happen on the UI thread 907 Object methodValue = method.invoke(view, (Object[]) null); 908 final Class<?> returnType = method.getReturnType(); 909 final ExportedProperty property = sAnnotations.get(method); 910 String categoryPrefix = 911 property.category().length() != 0 ? property.category() + ":" : ""; 912 913 if (returnType == int.class) { 914 915 if (property.resolveId() && context != null) { 916 final int id = (Integer) methodValue; 917 methodValue = resolveId(context, id); 918 } else { 919 final FlagToString[] flagsMapping = property.flagMapping(); 920 if (flagsMapping.length > 0) { 921 final int intValue = (Integer) methodValue; 922 final String valuePrefix = 923 categoryPrefix + prefix + method.getName() + '_'; 924 exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix); 925 } 926 927 final IntToString[] mapping = property.mapping(); 928 if (mapping.length > 0) { 929 final int intValue = (Integer) methodValue; 930 boolean mapped = false; 931 int mappingCount = mapping.length; 932 for (int j = 0; j < mappingCount; j++) { 933 final IntToString mapper = mapping[j]; 934 if (mapper.from() == intValue) { 935 methodValue = mapper.to(); 936 mapped = true; 937 break; 938 } 939 } 940 941 if (!mapped) { 942 methodValue = intValue; 943 } 944 } 945 } 946 } else if (returnType == int[].class) { 947 final int[] array = (int[]) methodValue; 948 final String valuePrefix = categoryPrefix + prefix + method.getName() + '_'; 949 final String suffix = "()"; 950 951 exportUnrolledArray(context, out, property, array, valuePrefix, suffix); 952 953 // Probably want to return here, same as for fields. 954 return; 955 } else if (!returnType.isPrimitive()) { 956 if (property.deepExport()) { 957 dumpViewProperties(context, methodValue, out, prefix + property.prefix()); 958 continue; 959 } 960 } 961 962 writeEntry(out, categoryPrefix + prefix, method.getName(), "()", methodValue); 963 } catch (IllegalAccessException e) { 964 } catch (InvocationTargetException e) { 965 } 966 } 967 } 968 exportFields(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)969 private static void exportFields(Context context, Object view, BufferedWriter out, 970 Class<?> klass, String prefix) throws IOException { 971 972 final Field[] fields = getExportedPropertyFields(klass); 973 974 int count = fields.length; 975 for (int i = 0; i < count; i++) { 976 final Field field = fields[i]; 977 978 //noinspection EmptyCatchBlock 979 try { 980 Object fieldValue = null; 981 final Class<?> type = field.getType(); 982 final ExportedProperty property = sAnnotations.get(field); 983 String categoryPrefix = 984 property.category().length() != 0 ? property.category() + ":" : ""; 985 986 if (type == int.class) { 987 988 if (property.resolveId() && context != null) { 989 final int id = field.getInt(view); 990 fieldValue = resolveId(context, id); 991 } else { 992 final FlagToString[] flagsMapping = property.flagMapping(); 993 if (flagsMapping.length > 0) { 994 final int intValue = field.getInt(view); 995 final String valuePrefix = 996 categoryPrefix + prefix + field.getName() + '_'; 997 exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix); 998 } 999 1000 final IntToString[] mapping = property.mapping(); 1001 if (mapping.length > 0) { 1002 final int intValue = field.getInt(view); 1003 int mappingCount = mapping.length; 1004 for (int j = 0; j < mappingCount; j++) { 1005 final IntToString mapped = mapping[j]; 1006 if (mapped.from() == intValue) { 1007 fieldValue = mapped.to(); 1008 break; 1009 } 1010 } 1011 1012 if (fieldValue == null) { 1013 fieldValue = intValue; 1014 } 1015 } 1016 } 1017 } else if (type == int[].class) { 1018 final int[] array = (int[]) field.get(view); 1019 final String valuePrefix = categoryPrefix + prefix + field.getName() + '_'; 1020 final String suffix = ""; 1021 1022 exportUnrolledArray(context, out, property, array, valuePrefix, suffix); 1023 1024 // We exit here! 1025 return; 1026 } else if (!type.isPrimitive()) { 1027 if (property.deepExport()) { 1028 dumpViewProperties(context, field.get(view), out, prefix 1029 + property.prefix()); 1030 continue; 1031 } 1032 } 1033 1034 if (fieldValue == null) { 1035 fieldValue = field.get(view); 1036 } 1037 1038 writeEntry(out, categoryPrefix + prefix, field.getName(), "", fieldValue); 1039 } catch (IllegalAccessException e) { 1040 } 1041 } 1042 } 1043 writeEntry(BufferedWriter out, String prefix, String name, String suffix, Object value)1044 private static void writeEntry(BufferedWriter out, String prefix, String name, 1045 String suffix, Object value) throws IOException { 1046 1047 out.write(prefix); 1048 out.write(name); 1049 out.write(suffix); 1050 out.write("="); 1051 writeValue(out, value); 1052 out.write(' '); 1053 } 1054 exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping, int intValue, String prefix)1055 private static void exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping, 1056 int intValue, String prefix) throws IOException { 1057 1058 final int count = mapping.length; 1059 for (int j = 0; j < count; j++) { 1060 final FlagToString flagMapping = mapping[j]; 1061 final boolean ifTrue = flagMapping.outputIf(); 1062 final int maskResult = intValue & flagMapping.mask(); 1063 final boolean test = maskResult == flagMapping.equals(); 1064 if ((test && ifTrue) || (!test && !ifTrue)) { 1065 final String name = flagMapping.name(); 1066 final String value = "0x" + Integer.toHexString(maskResult); 1067 writeEntry(out, prefix, name, "", value); 1068 } 1069 } 1070 } 1071 exportUnrolledArray(Context context, BufferedWriter out, ExportedProperty property, int[] array, String prefix, String suffix)1072 private static void exportUnrolledArray(Context context, BufferedWriter out, 1073 ExportedProperty property, int[] array, String prefix, String suffix) 1074 throws IOException { 1075 1076 final IntToString[] indexMapping = property.indexMapping(); 1077 final boolean hasIndexMapping = indexMapping.length > 0; 1078 1079 final IntToString[] mapping = property.mapping(); 1080 final boolean hasMapping = mapping.length > 0; 1081 1082 final boolean resolveId = property.resolveId() && context != null; 1083 final int valuesCount = array.length; 1084 1085 for (int j = 0; j < valuesCount; j++) { 1086 String name; 1087 String value = null; 1088 1089 final int intValue = array[j]; 1090 1091 name = String.valueOf(j); 1092 if (hasIndexMapping) { 1093 int mappingCount = indexMapping.length; 1094 for (int k = 0; k < mappingCount; k++) { 1095 final IntToString mapped = indexMapping[k]; 1096 if (mapped.from() == j) { 1097 name = mapped.to(); 1098 break; 1099 } 1100 } 1101 } 1102 1103 if (hasMapping) { 1104 int mappingCount = mapping.length; 1105 for (int k = 0; k < mappingCount; k++) { 1106 final IntToString mapped = mapping[k]; 1107 if (mapped.from() == intValue) { 1108 value = mapped.to(); 1109 break; 1110 } 1111 } 1112 } 1113 1114 if (resolveId) { 1115 if (value == null) value = (String) resolveId(context, intValue); 1116 } else { 1117 value = String.valueOf(intValue); 1118 } 1119 1120 writeEntry(out, prefix, name, suffix, value); 1121 } 1122 } 1123 resolveId(Context context, int id)1124 static Object resolveId(Context context, int id) { 1125 Object fieldValue; 1126 final Resources resources = context.getResources(); 1127 if (id >= 0) { 1128 try { 1129 fieldValue = resources.getResourceTypeName(id) + '/' + 1130 resources.getResourceEntryName(id); 1131 } catch (Resources.NotFoundException e) { 1132 fieldValue = "id/0x" + Integer.toHexString(id); 1133 } 1134 } else { 1135 fieldValue = "NO_ID"; 1136 } 1137 return fieldValue; 1138 } 1139 writeValue(BufferedWriter out, Object value)1140 private static void writeValue(BufferedWriter out, Object value) throws IOException { 1141 if (value != null) { 1142 String output = value.toString().replace("\n", "\\n"); 1143 out.write(String.valueOf(output.length())); 1144 out.write(","); 1145 out.write(output); 1146 } else { 1147 out.write("4,null"); 1148 } 1149 } 1150 capturedViewGetPropertyFields(Class<?> klass)1151 private static Field[] capturedViewGetPropertyFields(Class<?> klass) { 1152 if (mCapturedViewFieldsForClasses == null) { 1153 mCapturedViewFieldsForClasses = new HashMap<Class<?>, Field[]>(); 1154 } 1155 final HashMap<Class<?>, Field[]> map = mCapturedViewFieldsForClasses; 1156 1157 Field[] fields = map.get(klass); 1158 if (fields != null) { 1159 return fields; 1160 } 1161 1162 final ArrayList<Field> foundFields = new ArrayList<Field>(); 1163 fields = klass.getFields(); 1164 1165 int count = fields.length; 1166 for (int i = 0; i < count; i++) { 1167 final Field field = fields[i]; 1168 if (field.isAnnotationPresent(CapturedViewProperty.class)) { 1169 field.setAccessible(true); 1170 foundFields.add(field); 1171 } 1172 } 1173 1174 fields = foundFields.toArray(new Field[foundFields.size()]); 1175 map.put(klass, fields); 1176 1177 return fields; 1178 } 1179 capturedViewGetPropertyMethods(Class<?> klass)1180 private static Method[] capturedViewGetPropertyMethods(Class<?> klass) { 1181 if (mCapturedViewMethodsForClasses == null) { 1182 mCapturedViewMethodsForClasses = new HashMap<Class<?>, Method[]>(); 1183 } 1184 final HashMap<Class<?>, Method[]> map = mCapturedViewMethodsForClasses; 1185 1186 Method[] methods = map.get(klass); 1187 if (methods != null) { 1188 return methods; 1189 } 1190 1191 final ArrayList<Method> foundMethods = new ArrayList<Method>(); 1192 methods = klass.getMethods(); 1193 1194 int count = methods.length; 1195 for (int i = 0; i < count; i++) { 1196 final Method method = methods[i]; 1197 if (method.getParameterTypes().length == 0 && 1198 method.isAnnotationPresent(CapturedViewProperty.class) && 1199 method.getReturnType() != Void.class) { 1200 method.setAccessible(true); 1201 foundMethods.add(method); 1202 } 1203 } 1204 1205 methods = foundMethods.toArray(new Method[foundMethods.size()]); 1206 map.put(klass, methods); 1207 1208 return methods; 1209 } 1210 capturedViewExportMethods(Object obj, Class<?> klass, String prefix)1211 private static String capturedViewExportMethods(Object obj, Class<?> klass, 1212 String prefix) { 1213 1214 if (obj == null) { 1215 return "null"; 1216 } 1217 1218 StringBuilder sb = new StringBuilder(); 1219 final Method[] methods = capturedViewGetPropertyMethods(klass); 1220 1221 int count = methods.length; 1222 for (int i = 0; i < count; i++) { 1223 final Method method = methods[i]; 1224 try { 1225 Object methodValue = method.invoke(obj, (Object[]) null); 1226 final Class<?> returnType = method.getReturnType(); 1227 1228 CapturedViewProperty property = method.getAnnotation(CapturedViewProperty.class); 1229 if (property.retrieveReturn()) { 1230 //we are interested in the second level data only 1231 sb.append(capturedViewExportMethods(methodValue, returnType, method.getName() + "#")); 1232 } else { 1233 sb.append(prefix); 1234 sb.append(method.getName()); 1235 sb.append("()="); 1236 1237 if (methodValue != null) { 1238 final String value = methodValue.toString().replace("\n", "\\n"); 1239 sb.append(value); 1240 } else { 1241 sb.append("null"); 1242 } 1243 sb.append("; "); 1244 } 1245 } catch (IllegalAccessException e) { 1246 //Exception IllegalAccess, it is OK here 1247 //we simply ignore this method 1248 } catch (InvocationTargetException e) { 1249 //Exception InvocationTarget, it is OK here 1250 //we simply ignore this method 1251 } 1252 } 1253 return sb.toString(); 1254 } 1255 capturedViewExportFields(Object obj, Class<?> klass, String prefix)1256 private static String capturedViewExportFields(Object obj, Class<?> klass, String prefix) { 1257 if (obj == null) { 1258 return "null"; 1259 } 1260 1261 StringBuilder sb = new StringBuilder(); 1262 final Field[] fields = capturedViewGetPropertyFields(klass); 1263 1264 int count = fields.length; 1265 for (int i = 0; i < count; i++) { 1266 final Field field = fields[i]; 1267 try { 1268 Object fieldValue = field.get(obj); 1269 1270 sb.append(prefix); 1271 sb.append(field.getName()); 1272 sb.append("="); 1273 1274 if (fieldValue != null) { 1275 final String value = fieldValue.toString().replace("\n", "\\n"); 1276 sb.append(value); 1277 } else { 1278 sb.append("null"); 1279 } 1280 sb.append(' '); 1281 } catch (IllegalAccessException e) { 1282 //Exception IllegalAccess, it is OK here 1283 //we simply ignore this field 1284 } 1285 } 1286 return sb.toString(); 1287 } 1288 1289 /** 1290 * Dump view info for id based instrument test generation 1291 * (and possibly further data analysis). The results are dumped 1292 * to the log. 1293 * @param tag for log 1294 * @param view for dump 1295 */ dumpCapturedView(String tag, Object view)1296 public static void dumpCapturedView(String tag, Object view) { 1297 Class<?> klass = view.getClass(); 1298 StringBuilder sb = new StringBuilder(klass.getName() + ": "); 1299 sb.append(capturedViewExportFields(view, klass, "")); 1300 sb.append(capturedViewExportMethods(view, klass, "")); 1301 Log.d(tag, sb.toString()); 1302 } 1303 } 1304