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 /** 259 * Allows a View to inject custom children into HierarchyViewer. For example, 260 * WebView uses this to add its internal layer tree as a child to itself 261 * @hide 262 */ 263 public interface HierarchyHandler { 264 /** 265 * Dumps custom children to hierarchy viewer. 266 * See ViewDebug.dumpViewWithProperties(Context, View, BufferedWriter, int) 267 * for the format 268 * 269 * An empty implementation should simply do nothing 270 * 271 * @param out The output writer 272 * @param level The indentation level 273 */ dumpViewHierarchyWithProperties(BufferedWriter out, int level)274 public void dumpViewHierarchyWithProperties(BufferedWriter out, int level); 275 276 /** 277 * Returns a View to enable grabbing screenshots from custom children 278 * returned in dumpViewHierarchyWithProperties. 279 * 280 * @param className The className of the view to find 281 * @param hashCode The hashCode of the view to find 282 * @return the View to capture from, or null if not found 283 */ findHierarchyView(String className, int hashCode)284 public View findHierarchyView(String className, int hashCode); 285 } 286 287 private static HashMap<Class<?>, Method[]> mCapturedViewMethodsForClasses = null; 288 private static HashMap<Class<?>, Field[]> mCapturedViewFieldsForClasses = null; 289 290 // Maximum delay in ms after which we stop trying to capture a View's drawing 291 private static final int CAPTURE_TIMEOUT = 4000; 292 293 private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE"; 294 private static final String REMOTE_COMMAND_DUMP = "DUMP"; 295 private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE"; 296 private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT"; 297 private static final String REMOTE_PROFILE = "PROFILE"; 298 private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS"; 299 private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST"; 300 301 private static HashMap<Class<?>, Field[]> sFieldsForClasses; 302 private static HashMap<Class<?>, Method[]> sMethodsForClasses; 303 private static HashMap<AccessibleObject, ExportedProperty> sAnnotations; 304 305 /** 306 * @deprecated This enum is now unused 307 */ 308 @Deprecated 309 public enum HierarchyTraceType { 310 INVALIDATE, 311 INVALIDATE_CHILD, 312 INVALIDATE_CHILD_IN_PARENT, 313 REQUEST_LAYOUT, 314 ON_LAYOUT, 315 ON_MEASURE, 316 DRAW, 317 BUILD_CACHE 318 } 319 320 /** 321 * @deprecated This enum is now unused 322 */ 323 @Deprecated 324 public enum RecyclerTraceType { 325 NEW_VIEW, 326 BIND_VIEW, 327 RECYCLE_FROM_ACTIVE_HEAP, 328 RECYCLE_FROM_SCRAP_HEAP, 329 MOVE_TO_SCRAP_HEAP, 330 MOVE_FROM_ACTIVE_TO_SCRAP_HEAP 331 } 332 333 /** 334 * Returns the number of instanciated Views. 335 * 336 * @return The number of Views instanciated in the current process. 337 * 338 * @hide 339 */ getViewInstanceCount()340 public static long getViewInstanceCount() { 341 return Debug.countInstancesOfClass(View.class); 342 } 343 344 /** 345 * Returns the number of instanciated ViewAncestors. 346 * 347 * @return The number of ViewAncestors instanciated in the current process. 348 * 349 * @hide 350 */ getViewRootImplCount()351 public static long getViewRootImplCount() { 352 return Debug.countInstancesOfClass(ViewRootImpl.class); 353 } 354 355 /** 356 * @deprecated This method is now unused and invoking it is a no-op 357 */ 358 @Deprecated 359 @SuppressWarnings({ "UnusedParameters", "deprecation" }) trace(View view, RecyclerTraceType type, int... parameters)360 public static void trace(View view, RecyclerTraceType type, int... parameters) { 361 } 362 363 /** 364 * @deprecated This method is now unused and invoking it is a no-op 365 */ 366 @Deprecated 367 @SuppressWarnings("UnusedParameters") startRecyclerTracing(String prefix, View view)368 public static void startRecyclerTracing(String prefix, View view) { 369 } 370 371 /** 372 * @deprecated This method is now unused and invoking it is a no-op 373 */ 374 @Deprecated 375 @SuppressWarnings("UnusedParameters") stopRecyclerTracing()376 public static void stopRecyclerTracing() { 377 } 378 379 /** 380 * @deprecated This method is now unused and invoking it is a no-op 381 */ 382 @Deprecated 383 @SuppressWarnings({ "UnusedParameters", "deprecation" }) trace(View view, HierarchyTraceType type)384 public static void trace(View view, HierarchyTraceType type) { 385 } 386 387 /** 388 * @deprecated This method is now unused and invoking it is a no-op 389 */ 390 @Deprecated 391 @SuppressWarnings("UnusedParameters") startHierarchyTracing(String prefix, View view)392 public static void startHierarchyTracing(String prefix, View view) { 393 } 394 395 /** 396 * @deprecated This method is now unused and invoking it is a no-op 397 */ 398 @Deprecated stopHierarchyTracing()399 public static void stopHierarchyTracing() { 400 } 401 dispatchCommand(View view, String command, String parameters, OutputStream clientStream)402 static void dispatchCommand(View view, String command, String parameters, 403 OutputStream clientStream) throws IOException { 404 405 // Paranoid but safe... 406 view = view.getRootView(); 407 408 if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) { 409 dump(view, clientStream); 410 } else if (REMOTE_COMMAND_CAPTURE_LAYERS.equalsIgnoreCase(command)) { 411 captureLayers(view, new DataOutputStream(clientStream)); 412 } else { 413 final String[] params = parameters.split(" "); 414 if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) { 415 capture(view, clientStream, params[0]); 416 } else if (REMOTE_COMMAND_OUTPUT_DISPLAYLIST.equalsIgnoreCase(command)) { 417 outputDisplayList(view, params[0]); 418 } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) { 419 invalidate(view, params[0]); 420 } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) { 421 requestLayout(view, params[0]); 422 } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) { 423 profile(view, clientStream, params[0]); 424 } 425 } 426 } 427 findView(View root, String parameter)428 private static View findView(View root, String parameter) { 429 // Look by type/hashcode 430 if (parameter.indexOf('@') != -1) { 431 final String[] ids = parameter.split("@"); 432 final String className = ids[0]; 433 final int hashCode = (int) Long.parseLong(ids[1], 16); 434 435 View view = root.getRootView(); 436 if (view instanceof ViewGroup) { 437 return findView((ViewGroup) view, className, hashCode); 438 } 439 } else { 440 // Look by id 441 final int id = root.getResources().getIdentifier(parameter, null, null); 442 return root.getRootView().findViewById(id); 443 } 444 445 return null; 446 } 447 invalidate(View root, String parameter)448 private static void invalidate(View root, String parameter) { 449 final View view = findView(root, parameter); 450 if (view != null) { 451 view.postInvalidate(); 452 } 453 } 454 requestLayout(View root, String parameter)455 private static void requestLayout(View root, String parameter) { 456 final View view = findView(root, parameter); 457 if (view != null) { 458 root.post(new Runnable() { 459 public void run() { 460 view.requestLayout(); 461 } 462 }); 463 } 464 } 465 profile(View root, OutputStream clientStream, String parameter)466 private static void profile(View root, OutputStream clientStream, String parameter) 467 throws IOException { 468 469 final View view = findView(root, parameter); 470 BufferedWriter out = null; 471 try { 472 out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024); 473 474 if (view != null) { 475 profileViewAndChildren(view, out); 476 } else { 477 out.write("-1 -1 -1"); 478 out.newLine(); 479 } 480 out.write("DONE."); 481 out.newLine(); 482 } catch (Exception e) { 483 android.util.Log.w("View", "Problem profiling the view:", e); 484 } finally { 485 if (out != null) { 486 out.close(); 487 } 488 } 489 } 490 profileViewAndChildren(final View view, BufferedWriter out)491 private static void profileViewAndChildren(final View view, BufferedWriter out) 492 throws IOException { 493 profileViewAndChildren(view, out, true); 494 } 495 profileViewAndChildren(final View view, BufferedWriter out, boolean root)496 private static void profileViewAndChildren(final View view, BufferedWriter out, boolean root) 497 throws IOException { 498 499 long durationMeasure = 500 (root || (view.mPrivateFlags & View.PFLAG_MEASURED_DIMENSION_SET) != 0) 501 ? profileViewOperation(view, new ViewOperation<Void>() { 502 public Void[] pre() { 503 forceLayout(view); 504 return null; 505 } 506 507 private void forceLayout(View view) { 508 view.forceLayout(); 509 if (view instanceof ViewGroup) { 510 ViewGroup group = (ViewGroup) view; 511 final int count = group.getChildCount(); 512 for (int i = 0; i < count; i++) { 513 forceLayout(group.getChildAt(i)); 514 } 515 } 516 } 517 518 public void run(Void... data) { 519 view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec); 520 } 521 522 public void post(Void... data) { 523 } 524 }) 525 : 0; 526 long durationLayout = 527 (root || (view.mPrivateFlags & View.PFLAG_LAYOUT_REQUIRED) != 0) 528 ? profileViewOperation(view, new ViewOperation<Void>() { 529 public Void[] pre() { 530 return null; 531 } 532 533 public void run(Void... data) { 534 view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom); 535 } 536 537 public void post(Void... data) { 538 } 539 }) : 0; 540 long durationDraw = 541 (root || !view.willNotDraw() || (view.mPrivateFlags & View.PFLAG_DRAWN) != 0) 542 ? profileViewOperation(view, new ViewOperation<Object>() { 543 public Object[] pre() { 544 final DisplayMetrics metrics = 545 (view != null && view.getResources() != null) ? 546 view.getResources().getDisplayMetrics() : null; 547 final Bitmap bitmap = metrics != null ? 548 Bitmap.createBitmap(metrics, metrics.widthPixels, 549 metrics.heightPixels, Bitmap.Config.RGB_565) : null; 550 final Canvas canvas = bitmap != null ? new Canvas(bitmap) : null; 551 return new Object[] { 552 bitmap, canvas 553 }; 554 } 555 556 public void run(Object... data) { 557 if (data[1] != null) { 558 view.draw((Canvas) data[1]); 559 } 560 } 561 562 public void post(Object... data) { 563 if (data[1] != null) { 564 ((Canvas) data[1]).setBitmap(null); 565 } 566 if (data[0] != null) { 567 ((Bitmap) data[0]).recycle(); 568 } 569 } 570 }) : 0; 571 out.write(String.valueOf(durationMeasure)); 572 out.write(' '); 573 out.write(String.valueOf(durationLayout)); 574 out.write(' '); 575 out.write(String.valueOf(durationDraw)); 576 out.newLine(); 577 if (view instanceof ViewGroup) { 578 ViewGroup group = (ViewGroup) view; 579 final int count = group.getChildCount(); 580 for (int i = 0; i < count; i++) { 581 profileViewAndChildren(group.getChildAt(i), out, false); 582 } 583 } 584 } 585 586 interface ViewOperation<T> { pre()587 T[] pre(); run(T... data)588 void run(T... data); post(T... data)589 void post(T... data); 590 } 591 profileViewOperation(View view, final ViewOperation<T> operation)592 private static <T> long profileViewOperation(View view, final ViewOperation<T> operation) { 593 final CountDownLatch latch = new CountDownLatch(1); 594 final long[] duration = new long[1]; 595 596 view.post(new Runnable() { 597 public void run() { 598 try { 599 T[] data = operation.pre(); 600 long start = Debug.threadCpuTimeNanos(); 601 //noinspection unchecked 602 operation.run(data); 603 duration[0] = Debug.threadCpuTimeNanos() - start; 604 //noinspection unchecked 605 operation.post(data); 606 } finally { 607 latch.countDown(); 608 } 609 } 610 }); 611 612 try { 613 if (!latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS)) { 614 Log.w("View", "Could not complete the profiling of the view " + view); 615 return -1; 616 } 617 } catch (InterruptedException e) { 618 Log.w("View", "Could not complete the profiling of the view " + view); 619 Thread.currentThread().interrupt(); 620 return -1; 621 } 622 623 return duration[0]; 624 } 625 captureLayers(View root, final DataOutputStream clientStream)626 private static void captureLayers(View root, final DataOutputStream clientStream) 627 throws IOException { 628 629 try { 630 Rect outRect = new Rect(); 631 try { 632 root.mAttachInfo.mSession.getDisplayFrame(root.mAttachInfo.mWindow, outRect); 633 } catch (RemoteException e) { 634 // Ignore 635 } 636 637 clientStream.writeInt(outRect.width()); 638 clientStream.writeInt(outRect.height()); 639 640 captureViewLayer(root, clientStream, true); 641 642 clientStream.write(2); 643 } finally { 644 clientStream.close(); 645 } 646 } 647 captureViewLayer(View view, DataOutputStream clientStream, boolean visible)648 private static void captureViewLayer(View view, DataOutputStream clientStream, boolean visible) 649 throws IOException { 650 651 final boolean localVisible = view.getVisibility() == View.VISIBLE && visible; 652 653 if ((view.mPrivateFlags & View.PFLAG_SKIP_DRAW) != View.PFLAG_SKIP_DRAW) { 654 final int id = view.getId(); 655 String name = view.getClass().getSimpleName(); 656 if (id != View.NO_ID) { 657 name = resolveId(view.getContext(), id).toString(); 658 } 659 660 clientStream.write(1); 661 clientStream.writeUTF(name); 662 clientStream.writeByte(localVisible ? 1 : 0); 663 664 int[] position = new int[2]; 665 // XXX: Should happen on the UI thread 666 view.getLocationInWindow(position); 667 668 clientStream.writeInt(position[0]); 669 clientStream.writeInt(position[1]); 670 clientStream.flush(); 671 672 Bitmap b = performViewCapture(view, true); 673 if (b != null) { 674 ByteArrayOutputStream arrayOut = new ByteArrayOutputStream(b.getWidth() * 675 b.getHeight() * 2); 676 b.compress(Bitmap.CompressFormat.PNG, 100, arrayOut); 677 clientStream.writeInt(arrayOut.size()); 678 arrayOut.writeTo(clientStream); 679 } 680 clientStream.flush(); 681 } 682 683 if (view instanceof ViewGroup) { 684 ViewGroup group = (ViewGroup) view; 685 int count = group.getChildCount(); 686 687 for (int i = 0; i < count; i++) { 688 captureViewLayer(group.getChildAt(i), clientStream, localVisible); 689 } 690 } 691 } 692 outputDisplayList(View root, String parameter)693 private static void outputDisplayList(View root, String parameter) throws IOException { 694 final View view = findView(root, parameter); 695 view.getViewRootImpl().outputDisplayList(view); 696 } 697 capture(View root, final OutputStream clientStream, String parameter)698 private static void capture(View root, final OutputStream clientStream, String parameter) 699 throws IOException { 700 701 final View captureView = findView(root, parameter); 702 Bitmap b = performViewCapture(captureView, false); 703 704 if (b == null) { 705 Log.w("View", "Failed to create capture bitmap!"); 706 // Send an empty one so that it doesn't get stuck waiting for 707 // something. 708 b = Bitmap.createBitmap(root.getResources().getDisplayMetrics(), 709 1, 1, Bitmap.Config.ARGB_8888); 710 } 711 712 BufferedOutputStream out = null; 713 try { 714 out = new BufferedOutputStream(clientStream, 32 * 1024); 715 b.compress(Bitmap.CompressFormat.PNG, 100, out); 716 out.flush(); 717 } finally { 718 if (out != null) { 719 out.close(); 720 } 721 b.recycle(); 722 } 723 } 724 performViewCapture(final View captureView, final boolean skpiChildren)725 private static Bitmap performViewCapture(final View captureView, final boolean skpiChildren) { 726 if (captureView != null) { 727 final CountDownLatch latch = new CountDownLatch(1); 728 final Bitmap[] cache = new Bitmap[1]; 729 730 captureView.post(new Runnable() { 731 public void run() { 732 try { 733 cache[0] = captureView.createSnapshot( 734 Bitmap.Config.ARGB_8888, 0, skpiChildren); 735 } catch (OutOfMemoryError e) { 736 Log.w("View", "Out of memory for bitmap"); 737 } finally { 738 latch.countDown(); 739 } 740 } 741 }); 742 743 try { 744 latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS); 745 return cache[0]; 746 } catch (InterruptedException e) { 747 Log.w("View", "Could not complete the capture of the view " + captureView); 748 Thread.currentThread().interrupt(); 749 } 750 } 751 752 return null; 753 } 754 dump(View root, OutputStream clientStream)755 private static void dump(View root, OutputStream clientStream) throws IOException { 756 BufferedWriter out = null; 757 try { 758 out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024); 759 View view = root.getRootView(); 760 if (view instanceof ViewGroup) { 761 ViewGroup group = (ViewGroup) view; 762 dumpViewHierarchyWithProperties(group.getContext(), group, out, 0); 763 } 764 out.write("DONE."); 765 out.newLine(); 766 } catch (Exception e) { 767 android.util.Log.w("View", "Problem dumping the view:", e); 768 } finally { 769 if (out != null) { 770 out.close(); 771 } 772 } 773 } 774 findView(ViewGroup group, String className, int hashCode)775 private static View findView(ViewGroup group, String className, int hashCode) { 776 if (isRequestedView(group, className, hashCode)) { 777 return group; 778 } 779 780 final int count = group.getChildCount(); 781 for (int i = 0; i < count; i++) { 782 final View view = group.getChildAt(i); 783 if (view instanceof ViewGroup) { 784 final View found = findView((ViewGroup) view, className, hashCode); 785 if (found != null) { 786 return found; 787 } 788 } else if (isRequestedView(view, className, hashCode)) { 789 return view; 790 } 791 if (view instanceof HierarchyHandler) { 792 final View found = ((HierarchyHandler)view) 793 .findHierarchyView(className, hashCode); 794 if (found != null) { 795 return found; 796 } 797 } 798 } 799 800 return null; 801 } 802 isRequestedView(View view, String className, int hashCode)803 private static boolean isRequestedView(View view, String className, int hashCode) { 804 return view.getClass().getName().equals(className) && view.hashCode() == hashCode; 805 } 806 dumpViewHierarchyWithProperties(Context context, ViewGroup group, BufferedWriter out, int level)807 private static void dumpViewHierarchyWithProperties(Context context, ViewGroup group, 808 BufferedWriter out, int level) { 809 if (!dumpViewWithProperties(context, group, out, level)) { 810 return; 811 } 812 813 final int count = group.getChildCount(); 814 for (int i = 0; i < count; i++) { 815 final View view = group.getChildAt(i); 816 if (view instanceof ViewGroup) { 817 dumpViewHierarchyWithProperties(context, (ViewGroup) view, out, level + 1); 818 } else { 819 dumpViewWithProperties(context, view, out, level + 1); 820 } 821 } 822 if (group instanceof HierarchyHandler) { 823 ((HierarchyHandler)group).dumpViewHierarchyWithProperties(out, level + 1); 824 } 825 } 826 dumpViewWithProperties(Context context, View view, BufferedWriter out, int level)827 private static boolean dumpViewWithProperties(Context context, View view, 828 BufferedWriter out, int level) { 829 830 try { 831 for (int i = 0; i < level; i++) { 832 out.write(' '); 833 } 834 out.write(view.getClass().getName()); 835 out.write('@'); 836 out.write(Integer.toHexString(view.hashCode())); 837 out.write(' '); 838 dumpViewProperties(context, view, out); 839 out.newLine(); 840 } catch (IOException e) { 841 Log.w("View", "Error while dumping hierarchy tree"); 842 return false; 843 } 844 return true; 845 } 846 getExportedPropertyFields(Class<?> klass)847 private static Field[] getExportedPropertyFields(Class<?> klass) { 848 if (sFieldsForClasses == null) { 849 sFieldsForClasses = new HashMap<Class<?>, Field[]>(); 850 } 851 if (sAnnotations == null) { 852 sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512); 853 } 854 855 final HashMap<Class<?>, Field[]> map = sFieldsForClasses; 856 857 Field[] fields = map.get(klass); 858 if (fields != null) { 859 return fields; 860 } 861 862 final ArrayList<Field> foundFields = new ArrayList<Field>(); 863 fields = klass.getDeclaredFields(); 864 865 int count = fields.length; 866 for (int i = 0; i < count; i++) { 867 final Field field = fields[i]; 868 if (field.isAnnotationPresent(ExportedProperty.class)) { 869 field.setAccessible(true); 870 foundFields.add(field); 871 sAnnotations.put(field, field.getAnnotation(ExportedProperty.class)); 872 } 873 } 874 875 fields = foundFields.toArray(new Field[foundFields.size()]); 876 map.put(klass, fields); 877 878 return fields; 879 } 880 getExportedPropertyMethods(Class<?> klass)881 private static Method[] getExportedPropertyMethods(Class<?> klass) { 882 if (sMethodsForClasses == null) { 883 sMethodsForClasses = new HashMap<Class<?>, Method[]>(100); 884 } 885 if (sAnnotations == null) { 886 sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512); 887 } 888 889 final HashMap<Class<?>, Method[]> map = sMethodsForClasses; 890 891 Method[] methods = map.get(klass); 892 if (methods != null) { 893 return methods; 894 } 895 896 final ArrayList<Method> foundMethods = new ArrayList<Method>(); 897 methods = klass.getDeclaredMethods(); 898 899 int count = methods.length; 900 for (int i = 0; i < count; i++) { 901 final Method method = methods[i]; 902 if (method.getParameterTypes().length == 0 && 903 method.isAnnotationPresent(ExportedProperty.class) && 904 method.getReturnType() != Void.class) { 905 method.setAccessible(true); 906 foundMethods.add(method); 907 sAnnotations.put(method, method.getAnnotation(ExportedProperty.class)); 908 } 909 } 910 911 methods = foundMethods.toArray(new Method[foundMethods.size()]); 912 map.put(klass, methods); 913 914 return methods; 915 } 916 dumpViewProperties(Context context, Object view, BufferedWriter out)917 private static void dumpViewProperties(Context context, Object view, 918 BufferedWriter out) throws IOException { 919 920 dumpViewProperties(context, view, out, ""); 921 } 922 dumpViewProperties(Context context, Object view, BufferedWriter out, String prefix)923 private static void dumpViewProperties(Context context, Object view, 924 BufferedWriter out, String prefix) throws IOException { 925 926 if (view == null) { 927 out.write(prefix + "=4,null "); 928 return; 929 } 930 931 Class<?> klass = view.getClass(); 932 do { 933 exportFields(context, view, out, klass, prefix); 934 exportMethods(context, view, out, klass, prefix); 935 klass = klass.getSuperclass(); 936 } while (klass != Object.class); 937 } 938 exportMethods(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)939 private static void exportMethods(Context context, Object view, BufferedWriter out, 940 Class<?> klass, String prefix) throws IOException { 941 942 final Method[] methods = getExportedPropertyMethods(klass); 943 944 int count = methods.length; 945 for (int i = 0; i < count; i++) { 946 final Method method = methods[i]; 947 //noinspection EmptyCatchBlock 948 try { 949 // TODO: This should happen on the UI thread 950 Object methodValue = method.invoke(view, (Object[]) null); 951 final Class<?> returnType = method.getReturnType(); 952 final ExportedProperty property = sAnnotations.get(method); 953 String categoryPrefix = 954 property.category().length() != 0 ? property.category() + ":" : ""; 955 956 if (returnType == int.class) { 957 958 if (property.resolveId() && context != null) { 959 final int id = (Integer) methodValue; 960 methodValue = resolveId(context, id); 961 } else { 962 final FlagToString[] flagsMapping = property.flagMapping(); 963 if (flagsMapping.length > 0) { 964 final int intValue = (Integer) methodValue; 965 final String valuePrefix = 966 categoryPrefix + prefix + method.getName() + '_'; 967 exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix); 968 } 969 970 final IntToString[] mapping = property.mapping(); 971 if (mapping.length > 0) { 972 final int intValue = (Integer) methodValue; 973 boolean mapped = false; 974 int mappingCount = mapping.length; 975 for (int j = 0; j < mappingCount; j++) { 976 final IntToString mapper = mapping[j]; 977 if (mapper.from() == intValue) { 978 methodValue = mapper.to(); 979 mapped = true; 980 break; 981 } 982 } 983 984 if (!mapped) { 985 methodValue = intValue; 986 } 987 } 988 } 989 } else if (returnType == int[].class) { 990 final int[] array = (int[]) methodValue; 991 final String valuePrefix = categoryPrefix + prefix + method.getName() + '_'; 992 final String suffix = "()"; 993 994 exportUnrolledArray(context, out, property, array, valuePrefix, suffix); 995 996 // Probably want to return here, same as for fields. 997 return; 998 } else if (!returnType.isPrimitive()) { 999 if (property.deepExport()) { 1000 dumpViewProperties(context, methodValue, out, prefix + property.prefix()); 1001 continue; 1002 } 1003 } 1004 1005 writeEntry(out, categoryPrefix + prefix, method.getName(), "()", methodValue); 1006 } catch (IllegalAccessException e) { 1007 } catch (InvocationTargetException e) { 1008 } 1009 } 1010 } 1011 exportFields(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix)1012 private static void exportFields(Context context, Object view, BufferedWriter out, 1013 Class<?> klass, String prefix) throws IOException { 1014 1015 final Field[] fields = getExportedPropertyFields(klass); 1016 1017 int count = fields.length; 1018 for (int i = 0; i < count; i++) { 1019 final Field field = fields[i]; 1020 1021 //noinspection EmptyCatchBlock 1022 try { 1023 Object fieldValue = null; 1024 final Class<?> type = field.getType(); 1025 final ExportedProperty property = sAnnotations.get(field); 1026 String categoryPrefix = 1027 property.category().length() != 0 ? property.category() + ":" : ""; 1028 1029 if (type == int.class) { 1030 1031 if (property.resolveId() && context != null) { 1032 final int id = field.getInt(view); 1033 fieldValue = resolveId(context, id); 1034 } else { 1035 final FlagToString[] flagsMapping = property.flagMapping(); 1036 if (flagsMapping.length > 0) { 1037 final int intValue = field.getInt(view); 1038 final String valuePrefix = 1039 categoryPrefix + prefix + field.getName() + '_'; 1040 exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix); 1041 } 1042 1043 final IntToString[] mapping = property.mapping(); 1044 if (mapping.length > 0) { 1045 final int intValue = field.getInt(view); 1046 int mappingCount = mapping.length; 1047 for (int j = 0; j < mappingCount; j++) { 1048 final IntToString mapped = mapping[j]; 1049 if (mapped.from() == intValue) { 1050 fieldValue = mapped.to(); 1051 break; 1052 } 1053 } 1054 1055 if (fieldValue == null) { 1056 fieldValue = intValue; 1057 } 1058 } 1059 } 1060 } else if (type == int[].class) { 1061 final int[] array = (int[]) field.get(view); 1062 final String valuePrefix = categoryPrefix + prefix + field.getName() + '_'; 1063 final String suffix = ""; 1064 1065 exportUnrolledArray(context, out, property, array, valuePrefix, suffix); 1066 1067 // We exit here! 1068 return; 1069 } else if (!type.isPrimitive()) { 1070 if (property.deepExport()) { 1071 dumpViewProperties(context, field.get(view), out, prefix + 1072 property.prefix()); 1073 continue; 1074 } 1075 } 1076 1077 if (fieldValue == null) { 1078 fieldValue = field.get(view); 1079 } 1080 1081 writeEntry(out, categoryPrefix + prefix, field.getName(), "", fieldValue); 1082 } catch (IllegalAccessException e) { 1083 } 1084 } 1085 } 1086 writeEntry(BufferedWriter out, String prefix, String name, String suffix, Object value)1087 private static void writeEntry(BufferedWriter out, String prefix, String name, 1088 String suffix, Object value) throws IOException { 1089 1090 out.write(prefix); 1091 out.write(name); 1092 out.write(suffix); 1093 out.write("="); 1094 writeValue(out, value); 1095 out.write(' '); 1096 } 1097 exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping, int intValue, String prefix)1098 private static void exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping, 1099 int intValue, String prefix) throws IOException { 1100 1101 final int count = mapping.length; 1102 for (int j = 0; j < count; j++) { 1103 final FlagToString flagMapping = mapping[j]; 1104 final boolean ifTrue = flagMapping.outputIf(); 1105 final int maskResult = intValue & flagMapping.mask(); 1106 final boolean test = maskResult == flagMapping.equals(); 1107 if ((test && ifTrue) || (!test && !ifTrue)) { 1108 final String name = flagMapping.name(); 1109 final String value = "0x" + Integer.toHexString(maskResult); 1110 writeEntry(out, prefix, name, "", value); 1111 } 1112 } 1113 } 1114 exportUnrolledArray(Context context, BufferedWriter out, ExportedProperty property, int[] array, String prefix, String suffix)1115 private static void exportUnrolledArray(Context context, BufferedWriter out, 1116 ExportedProperty property, int[] array, String prefix, String suffix) 1117 throws IOException { 1118 1119 final IntToString[] indexMapping = property.indexMapping(); 1120 final boolean hasIndexMapping = indexMapping.length > 0; 1121 1122 final IntToString[] mapping = property.mapping(); 1123 final boolean hasMapping = mapping.length > 0; 1124 1125 final boolean resolveId = property.resolveId() && context != null; 1126 final int valuesCount = array.length; 1127 1128 for (int j = 0; j < valuesCount; j++) { 1129 String name; 1130 String value = null; 1131 1132 final int intValue = array[j]; 1133 1134 name = String.valueOf(j); 1135 if (hasIndexMapping) { 1136 int mappingCount = indexMapping.length; 1137 for (int k = 0; k < mappingCount; k++) { 1138 final IntToString mapped = indexMapping[k]; 1139 if (mapped.from() == j) { 1140 name = mapped.to(); 1141 break; 1142 } 1143 } 1144 } 1145 1146 if (hasMapping) { 1147 int mappingCount = mapping.length; 1148 for (int k = 0; k < mappingCount; k++) { 1149 final IntToString mapped = mapping[k]; 1150 if (mapped.from() == intValue) { 1151 value = mapped.to(); 1152 break; 1153 } 1154 } 1155 } 1156 1157 if (resolveId) { 1158 if (value == null) value = (String) resolveId(context, intValue); 1159 } else { 1160 value = String.valueOf(intValue); 1161 } 1162 1163 writeEntry(out, prefix, name, suffix, value); 1164 } 1165 } 1166 resolveId(Context context, int id)1167 static Object resolveId(Context context, int id) { 1168 Object fieldValue; 1169 final Resources resources = context.getResources(); 1170 if (id >= 0) { 1171 try { 1172 fieldValue = resources.getResourceTypeName(id) + '/' + 1173 resources.getResourceEntryName(id); 1174 } catch (Resources.NotFoundException e) { 1175 fieldValue = "id/0x" + Integer.toHexString(id); 1176 } 1177 } else { 1178 fieldValue = "NO_ID"; 1179 } 1180 return fieldValue; 1181 } 1182 writeValue(BufferedWriter out, Object value)1183 private static void writeValue(BufferedWriter out, Object value) throws IOException { 1184 if (value != null) { 1185 String output = "[EXCEPTION]"; 1186 try { 1187 output = value.toString().replace("\n", "\\n"); 1188 } finally { 1189 out.write(String.valueOf(output.length())); 1190 out.write(","); 1191 out.write(output); 1192 } 1193 } else { 1194 out.write("4,null"); 1195 } 1196 } 1197 capturedViewGetPropertyFields(Class<?> klass)1198 private static Field[] capturedViewGetPropertyFields(Class<?> klass) { 1199 if (mCapturedViewFieldsForClasses == null) { 1200 mCapturedViewFieldsForClasses = new HashMap<Class<?>, Field[]>(); 1201 } 1202 final HashMap<Class<?>, Field[]> map = mCapturedViewFieldsForClasses; 1203 1204 Field[] fields = map.get(klass); 1205 if (fields != null) { 1206 return fields; 1207 } 1208 1209 final ArrayList<Field> foundFields = new ArrayList<Field>(); 1210 fields = klass.getFields(); 1211 1212 int count = fields.length; 1213 for (int i = 0; i < count; i++) { 1214 final Field field = fields[i]; 1215 if (field.isAnnotationPresent(CapturedViewProperty.class)) { 1216 field.setAccessible(true); 1217 foundFields.add(field); 1218 } 1219 } 1220 1221 fields = foundFields.toArray(new Field[foundFields.size()]); 1222 map.put(klass, fields); 1223 1224 return fields; 1225 } 1226 capturedViewGetPropertyMethods(Class<?> klass)1227 private static Method[] capturedViewGetPropertyMethods(Class<?> klass) { 1228 if (mCapturedViewMethodsForClasses == null) { 1229 mCapturedViewMethodsForClasses = new HashMap<Class<?>, Method[]>(); 1230 } 1231 final HashMap<Class<?>, Method[]> map = mCapturedViewMethodsForClasses; 1232 1233 Method[] methods = map.get(klass); 1234 if (methods != null) { 1235 return methods; 1236 } 1237 1238 final ArrayList<Method> foundMethods = new ArrayList<Method>(); 1239 methods = klass.getMethods(); 1240 1241 int count = methods.length; 1242 for (int i = 0; i < count; i++) { 1243 final Method method = methods[i]; 1244 if (method.getParameterTypes().length == 0 && 1245 method.isAnnotationPresent(CapturedViewProperty.class) && 1246 method.getReturnType() != Void.class) { 1247 method.setAccessible(true); 1248 foundMethods.add(method); 1249 } 1250 } 1251 1252 methods = foundMethods.toArray(new Method[foundMethods.size()]); 1253 map.put(klass, methods); 1254 1255 return methods; 1256 } 1257 capturedViewExportMethods(Object obj, Class<?> klass, String prefix)1258 private static String capturedViewExportMethods(Object obj, Class<?> klass, 1259 String prefix) { 1260 1261 if (obj == null) { 1262 return "null"; 1263 } 1264 1265 StringBuilder sb = new StringBuilder(); 1266 final Method[] methods = capturedViewGetPropertyMethods(klass); 1267 1268 int count = methods.length; 1269 for (int i = 0; i < count; i++) { 1270 final Method method = methods[i]; 1271 try { 1272 Object methodValue = method.invoke(obj, (Object[]) null); 1273 final Class<?> returnType = method.getReturnType(); 1274 1275 CapturedViewProperty property = method.getAnnotation(CapturedViewProperty.class); 1276 if (property.retrieveReturn()) { 1277 //we are interested in the second level data only 1278 sb.append(capturedViewExportMethods(methodValue, returnType, method.getName() + "#")); 1279 } else { 1280 sb.append(prefix); 1281 sb.append(method.getName()); 1282 sb.append("()="); 1283 1284 if (methodValue != null) { 1285 final String value = methodValue.toString().replace("\n", "\\n"); 1286 sb.append(value); 1287 } else { 1288 sb.append("null"); 1289 } 1290 sb.append("; "); 1291 } 1292 } catch (IllegalAccessException e) { 1293 //Exception IllegalAccess, it is OK here 1294 //we simply ignore this method 1295 } catch (InvocationTargetException e) { 1296 //Exception InvocationTarget, it is OK here 1297 //we simply ignore this method 1298 } 1299 } 1300 return sb.toString(); 1301 } 1302 capturedViewExportFields(Object obj, Class<?> klass, String prefix)1303 private static String capturedViewExportFields(Object obj, Class<?> klass, String prefix) { 1304 if (obj == null) { 1305 return "null"; 1306 } 1307 1308 StringBuilder sb = new StringBuilder(); 1309 final Field[] fields = capturedViewGetPropertyFields(klass); 1310 1311 int count = fields.length; 1312 for (int i = 0; i < count; i++) { 1313 final Field field = fields[i]; 1314 try { 1315 Object fieldValue = field.get(obj); 1316 1317 sb.append(prefix); 1318 sb.append(field.getName()); 1319 sb.append("="); 1320 1321 if (fieldValue != null) { 1322 final String value = fieldValue.toString().replace("\n", "\\n"); 1323 sb.append(value); 1324 } else { 1325 sb.append("null"); 1326 } 1327 sb.append(' '); 1328 } catch (IllegalAccessException e) { 1329 //Exception IllegalAccess, it is OK here 1330 //we simply ignore this field 1331 } 1332 } 1333 return sb.toString(); 1334 } 1335 1336 /** 1337 * Dump view info for id based instrument test generation 1338 * (and possibly further data analysis). The results are dumped 1339 * to the log. 1340 * @param tag for log 1341 * @param view for dump 1342 */ dumpCapturedView(String tag, Object view)1343 public static void dumpCapturedView(String tag, Object view) { 1344 Class<?> klass = view.getClass(); 1345 StringBuilder sb = new StringBuilder(klass.getName() + ": "); 1346 sb.append(capturedViewExportFields(view, klass, "")); 1347 sb.append(capturedViewExportMethods(view, klass, "")); 1348 Log.d(tag, sb.toString()); 1349 } 1350 } 1351