1 /* 2 * Copyright (C) 2022 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 com.android.app.viewcapture; 18 19 import static com.android.app.viewcapture.data.ExportedData.MagicNumber.MAGIC_NUMBER_H; 20 import static com.android.app.viewcapture.data.ExportedData.MagicNumber.MAGIC_NUMBER_L; 21 22 import android.content.ComponentCallbacks2; 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.content.res.Resources; 26 import android.media.permission.SafeCloseable; 27 import android.os.HandlerThread; 28 import android.os.Looper; 29 import android.os.SystemClock; 30 import android.os.Trace; 31 import android.text.TextUtils; 32 import android.util.SparseArray; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.view.ViewTreeObserver; 36 import android.view.Window; 37 38 import androidx.annotation.AnyThread; 39 import androidx.annotation.NonNull; 40 import androidx.annotation.Nullable; 41 import androidx.annotation.UiThread; 42 import androidx.annotation.VisibleForTesting; 43 import androidx.annotation.WorkerThread; 44 45 import com.android.app.viewcapture.data.ExportedData; 46 import com.android.app.viewcapture.data.FrameData; 47 import com.android.app.viewcapture.data.MotionWindowData; 48 import com.android.app.viewcapture.data.ViewNode; 49 import com.android.app.viewcapture.data.WindowData; 50 51 import java.io.DataOutputStream; 52 import java.io.IOException; 53 import java.io.OutputStream; 54 import java.util.ArrayList; 55 import java.util.List; 56 import java.util.Optional; 57 import java.util.concurrent.CompletableFuture; 58 import java.util.concurrent.ExecutionException; 59 import java.util.concurrent.Executor; 60 import java.util.concurrent.TimeUnit; 61 import java.util.function.Consumer; 62 import java.util.function.Predicate; 63 64 /** 65 * Utility class for capturing view data every frame 66 */ 67 public abstract class ViewCapture { 68 69 private static final String TAG = "ViewCapture"; 70 71 // These flags are copies of two private flags in the View class. 72 private static final int PFLAG_INVALIDATED = 0x80000000; 73 private static final int PFLAG_DIRTY_MASK = 0x00200000; 74 75 private static final long MAGIC_NUMBER_FOR_WINSCOPE = 76 ((long) MAGIC_NUMBER_H.getNumber() << 32) | MAGIC_NUMBER_L.getNumber(); 77 78 // Number of frames to keep in memory 79 private final int mMemorySize; 80 protected static final int DEFAULT_MEMORY_SIZE = 2000; 81 // Initial size of the reference pool. This is at least be 5 * total number of views in 82 // Launcher. This allows the first free frames avoid object allocation during view capture. 83 protected static final int DEFAULT_INIT_POOL_SIZE = 300; 84 85 public static final LooperExecutor MAIN_EXECUTOR = new LooperExecutor(Looper.getMainLooper()); 86 87 private final List<WindowListener> mListeners = new ArrayList<>(); 88 89 protected final Executor mBgExecutor; 90 91 // Pool used for capturing view tree on the UI thread. 92 private ViewRef mPool = new ViewRef(); 93 private boolean mIsEnabled = true; 94 ViewCapture(int memorySize, int initPoolSize, Executor bgExecutor)95 protected ViewCapture(int memorySize, int initPoolSize, Executor bgExecutor) { 96 mMemorySize = memorySize; 97 mBgExecutor = bgExecutor; 98 mBgExecutor.execute(() -> initPool(initPoolSize)); 99 } 100 createAndStartNewLooperExecutor(String name, int priority)101 public static LooperExecutor createAndStartNewLooperExecutor(String name, int priority) { 102 HandlerThread thread = new HandlerThread(name, priority); 103 thread.start(); 104 return new LooperExecutor(thread.getLooper()); 105 } 106 107 @UiThread addToPool(ViewRef start, ViewRef end)108 private void addToPool(ViewRef start, ViewRef end) { 109 end.next = mPool; 110 mPool = start; 111 } 112 113 @WorkerThread initPool(int initPoolSize)114 private void initPool(int initPoolSize) { 115 ViewRef start = new ViewRef(); 116 ViewRef current = start; 117 118 for (int i = 0; i < initPoolSize; i++) { 119 current.next = new ViewRef(); 120 current = current.next; 121 } 122 123 ViewRef finalCurrent = current; 124 MAIN_EXECUTOR.execute(() -> addToPool(start, finalCurrent)); 125 } 126 127 /** 128 * Attaches the ViewCapture to the provided window and returns a handle to detach the listener 129 */ 130 @NonNull startCapture(@onNull Window window)131 public SafeCloseable startCapture(@NonNull Window window) { 132 String title = window.getAttributes().getTitle().toString(); 133 String name = TextUtils.isEmpty(title) ? window.toString() : title; 134 return startCapture(window.getDecorView(), name); 135 } 136 137 /** 138 * Attaches the ViewCapture to the provided window and returns a handle to detach the listener. 139 * Verifies that ViewCapture is enabled before actually attaching an onDrawListener. 140 */ 141 @NonNull startCapture(@onNull View view, @NonNull String name)142 public SafeCloseable startCapture(@NonNull View view, @NonNull String name) { 143 WindowListener listener = new WindowListener(view, name); 144 if (mIsEnabled) MAIN_EXECUTOR.execute(listener::attachToRoot); 145 mListeners.add(listener); 146 view.getContext().registerComponentCallbacks(listener); 147 148 return () -> { 149 if (listener.mRoot != null && listener.mRoot.getContext() != null) { 150 listener.mRoot.getContext().unregisterComponentCallbacks(listener); 151 } 152 mListeners.remove(listener); 153 listener.detachFromRoot(); 154 }; 155 } 156 157 /** 158 * Launcher checks for leaks in many spots during its instrumented tests. The WindowListeners 159 * appear to have leaks because they store mRoot views. In reality, attached views close their 160 * respective window listeners when they are destroyed. 161 * <p> 162 * This method deletes detaches and deletes mRoot views from windowListeners. This makes the 163 * WindowListeners unusable for anything except dumping previously captured information. They 164 * are still technically enabled to allow for dumping. 165 */ 166 @VisibleForTesting 167 public void stopCapture(@NonNull View rootView) { 168 mListeners.forEach(it -> { 169 if (rootView == it.mRoot) { 170 it.mRoot.getViewTreeObserver().removeOnDrawListener(it); 171 it.mRoot = null; 172 } 173 }); 174 } 175 176 @UiThread 177 protected void enableOrDisableWindowListeners(boolean isEnabled) { 178 mIsEnabled = isEnabled; 179 mListeners.forEach(WindowListener::detachFromRoot); 180 if (mIsEnabled) mListeners.forEach(WindowListener::attachToRoot); 181 } 182 183 @AnyThread 184 protected void dumpTo(OutputStream os, Context context) 185 throws InterruptedException, ExecutionException, IOException { 186 if (mIsEnabled) { 187 DataOutputStream dataOutputStream = new DataOutputStream(os); 188 ExportedData ex = getExportedData(context); 189 dataOutputStream.writeInt(ex.getSerializedSize()); 190 ex.writeTo(dataOutputStream); 191 } 192 } 193 194 @VisibleForTesting 195 public ExportedData getExportedData(Context context) 196 throws InterruptedException, ExecutionException { 197 ArrayList<Class> classList = new ArrayList<>(); 198 return ExportedData.newBuilder() 199 .setMagicNumber(MAGIC_NUMBER_FOR_WINSCOPE) 200 .setPackage(context.getPackageName()) 201 .addAllWindowData(getWindowData(context, classList, l -> l.mIsActive).get()) 202 .addAllClassname(toStringList(classList)) 203 .setRealToElapsedTimeOffsetNanos(TimeUnit.MILLISECONDS 204 .toNanos(System.currentTimeMillis()) - SystemClock.elapsedRealtimeNanos()) 205 .build(); 206 } 207 208 private static List<String> toStringList(List<Class> classList) { 209 return classList.stream().map(Class::getName).toList(); 210 } 211 212 public CompletableFuture<Optional<MotionWindowData>> getDumpTask(View view) { 213 ArrayList<Class> classList = new ArrayList<>(); 214 return getWindowData(view.getContext().getApplicationContext(), classList, 215 l -> l.mRoot.equals(view)).thenApply(list -> list.stream().findFirst().map(w -> 216 MotionWindowData.newBuilder() 217 .addAllFrameData(w.getFrameDataList()) 218 .addAllClassname(toStringList(classList)) 219 .build())); 220 } 221 222 @AnyThread 223 private CompletableFuture<List<WindowData>> getWindowData(Context context, 224 ArrayList<Class> outClassList, Predicate<WindowListener> filter) { 225 ViewIdProvider idProvider = new ViewIdProvider(context.getResources()); 226 return CompletableFuture.supplyAsync(() -> 227 mListeners.stream().filter(filter).toList(), MAIN_EXECUTOR).thenApplyAsync(it -> 228 it.stream().map(l -> l.dumpToProto(idProvider, outClassList)).toList(), 229 mBgExecutor); 230 } 231 232 @WorkerThread 233 protected void onCapturedViewPropertiesBg(long elapsedRealtimeNanos, String windowName, 234 ViewPropertyRef startFlattenedViewTree) { 235 } 236 237 /** 238 * Once this window listener is attached to a window's root view, it traverses the entire 239 * view tree on the main thread every time onDraw is called. It then saves the state of the view 240 * tree traversed in a local list of nodes, so that this list of nodes can be processed on a 241 * background thread, and prepared for being dumped into a bugreport. 242 * <p> 243 * Since some of the work needs to be done on the main thread after every draw, this piece of 244 * code needs to be hyper optimized. That is why we are recycling ViewRef and ViewPropertyRef 245 * objects and storing the list of nodes as a flat LinkedList, rather than as a tree. This data 246 * structure allows recycling to happen in O(1) time via pointer assignment. Without this 247 * optimization, a lot of time is wasted creating ViewRef objects, or finding ViewRef objects to 248 * recycle. 249 * <p> 250 * Another optimization is to only traverse view nodes on the main thread that have potentially 251 * changed since the last frame was drawn. This can be determined via a combination of private 252 * flags inside the View class. 253 * <p> 254 * Another optimization is to not store or manipulate any string objects on the main thread. 255 * While this might seem trivial, using Strings in any form causes the ViewCapture to hog the 256 * main thread for up to an additional 6-7ms. It must be avoided at all costs. 257 * <p> 258 * Another optimization is to only store the class names of the Views in the view hierarchy one 259 * time. They are then referenced via a classNameIndex value stored in each ViewPropertyRef. 260 * <p> 261 * TODO: b/262585897: If further memory optimization is required, an effective one would be to 262 * only store the changes between frames, rather than the entire node tree for each frame. 263 * The go/web-hv UX already does this, and has reaped significant memory improves because of it. 264 * <p> 265 * TODO: b/262585897: Another memory optimization could be to store all integer, float, and 266 * boolean information via single integer values via the Chinese remainder theorem, or a similar 267 * algorithm, which enables multiple numerical values to be stored inside 1 number. Doing this 268 * would allow each ViewProperty / ViewRef to slim down its memory footprint significantly. 269 * <p> 270 * One important thing to remember is that bugs related to recycling will usually only appear 271 * after at least 2000 frames have been rendered. If that code is changed, the tester can 272 * use hard-coded logs to verify that recycling is happening, and test view capturing at least 273 * ~8000 frames or so to verify the recycling functionality is working properly. 274 * <p> 275 * Each WindowListener is memory aware and will both stop collecting view capture information, 276 * as well as delete their current stash of information upon a signal from the system that 277 * memory resources are scarce. The user will need to restart the app process before 278 * more ViewCapture information is captured. 279 */ 280 private class WindowListener implements ViewTreeObserver.OnDrawListener, ComponentCallbacks2 { 281 282 @Nullable 283 public View mRoot; 284 public final String name; 285 286 private final ViewRef mViewRef = new ViewRef(); 287 288 private int mFrameIndexBg = -1; 289 private boolean mIsFirstFrame = true; 290 private long[] mFrameTimesNanosBg = new long[mMemorySize]; 291 private ViewPropertyRef[] mNodesBg = new ViewPropertyRef[mMemorySize]; 292 293 private boolean mIsActive = true; 294 private final Consumer<ViewRef> mCaptureCallback = this::captureViewPropertiesBg; 295 296 WindowListener(View view, String name) { 297 mRoot = view; 298 this.name = name; 299 } 300 301 /** 302 * Every time onDraw is called, it does the minimal set of work required on the main thread, 303 * i.e. capturing potentially dirty / invalidated views, and then immediately offloads the 304 * rest of the processing work (extracting the captured view properties) to a background 305 * thread via mExecutor. 306 */ 307 @Override 308 public void onDraw() { 309 Trace.beginSection("vc#onDraw"); 310 captureViewTree(mRoot, mViewRef); 311 ViewRef captured = mViewRef.next; 312 if (captured != null) { 313 captured.callback = mCaptureCallback; 314 captured.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos(); 315 mBgExecutor.execute(captured); 316 } 317 mIsFirstFrame = false; 318 Trace.endSection(); 319 } 320 321 /** 322 * Captures the View property on the background thread, and transfer all the ViewRef objects 323 * back to the pool 324 */ 325 @WorkerThread 326 private void captureViewPropertiesBg(ViewRef viewRefStart) { 327 Trace.beginSection("vc#captureViewPropertiesBg"); 328 329 long elapsedRealtimeNanos = viewRefStart.elapsedRealtimeNanos; 330 mFrameIndexBg++; 331 if (mFrameIndexBg >= mMemorySize) { 332 mFrameIndexBg = 0; 333 } 334 mFrameTimesNanosBg[mFrameIndexBg] = elapsedRealtimeNanos; 335 336 ViewPropertyRef recycle = mNodesBg[mFrameIndexBg]; 337 338 ViewPropertyRef resultStart = null; 339 ViewPropertyRef resultEnd = null; 340 341 ViewRef viewRefEnd = viewRefStart; 342 while (viewRefEnd != null) { 343 ViewPropertyRef propertyRef = recycle; 344 if (propertyRef == null) { 345 propertyRef = new ViewPropertyRef(); 346 } else { 347 recycle = recycle.next; 348 propertyRef.next = null; 349 } 350 351 ViewPropertyRef copy = null; 352 if (viewRefEnd.childCount < 0) { 353 copy = findInLastFrame(viewRefEnd.view.hashCode()); 354 viewRefEnd.childCount = (copy != null) ? copy.childCount : 0; 355 } 356 viewRefEnd.transferTo(propertyRef); 357 358 if (resultStart == null) { 359 resultStart = propertyRef; 360 resultEnd = resultStart; 361 } else { 362 resultEnd.next = propertyRef; 363 resultEnd = resultEnd.next; 364 } 365 366 if (copy != null) { 367 int pending = copy.childCount; 368 while (pending > 0) { 369 copy = copy.next; 370 pending = pending - 1 + copy.childCount; 371 372 propertyRef = recycle; 373 if (propertyRef == null) { 374 propertyRef = new ViewPropertyRef(); 375 } else { 376 recycle = recycle.next; 377 propertyRef.next = null; 378 } 379 380 copy.transferTo(propertyRef); 381 382 resultEnd.next = propertyRef; 383 resultEnd = resultEnd.next; 384 } 385 } 386 387 if (viewRefEnd.next == null) { 388 // The compiler will complain about using a non-final variable from 389 // an outer class in a lambda if we pass in viewRefEnd directly. 390 final ViewRef finalViewRefEnd = viewRefEnd; 391 MAIN_EXECUTOR.execute(() -> addToPool(viewRefStart, finalViewRefEnd)); 392 break; 393 } 394 viewRefEnd = viewRefEnd.next; 395 } 396 mNodesBg[mFrameIndexBg] = resultStart; 397 398 onCapturedViewPropertiesBg(elapsedRealtimeNanos, name, resultStart); 399 400 Trace.endSection(); 401 } 402 403 private @Nullable ViewPropertyRef findInLastFrame(int hashCode) { 404 int lastFrameIndex = (mFrameIndexBg == 0) ? mMemorySize - 1 : mFrameIndexBg - 1; 405 ViewPropertyRef viewPropertyRef = mNodesBg[lastFrameIndex]; 406 while (viewPropertyRef != null && viewPropertyRef.hashCode != hashCode) { 407 viewPropertyRef = viewPropertyRef.next; 408 } 409 return viewPropertyRef; 410 } 411 412 void attachToRoot() { 413 if (mRoot == null) return; 414 mIsActive = true; 415 if (mRoot.isAttachedToWindow()) { 416 safelyEnableOnDrawListener(); 417 } else { 418 mRoot.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { 419 @Override 420 public void onViewAttachedToWindow(View v) { 421 if (mIsActive) { 422 safelyEnableOnDrawListener(); 423 } 424 mRoot.removeOnAttachStateChangeListener(this); 425 } 426 427 @Override 428 public void onViewDetachedFromWindow(View v) { 429 } 430 }); 431 } 432 } 433 434 void detachFromRoot() { 435 mIsActive = false; 436 if (mRoot != null) { 437 mRoot.getViewTreeObserver().removeOnDrawListener(this); 438 } 439 } 440 441 private void safelyEnableOnDrawListener() { 442 if (mRoot != null) { 443 mRoot.getViewTreeObserver().removeOnDrawListener(this); 444 mRoot.getViewTreeObserver().addOnDrawListener(this); 445 } 446 } 447 448 @WorkerThread 449 private WindowData dumpToProto(ViewIdProvider idProvider, ArrayList<Class> classList) { 450 WindowData.Builder builder = WindowData.newBuilder().setTitle(name); 451 int size = (mNodesBg[mMemorySize - 1] == null) ? mFrameIndexBg + 1 : mMemorySize; 452 for (int i = size - 1; i >= 0; i--) { 453 int index = (mMemorySize + mFrameIndexBg - i) % mMemorySize; 454 ViewNode.Builder nodeBuilder = ViewNode.newBuilder(); 455 mNodesBg[index].toProto(idProvider, classList, nodeBuilder); 456 FrameData.Builder frameDataBuilder = FrameData.newBuilder() 457 .setNode(nodeBuilder) 458 .setTimestamp(mFrameTimesNanosBg[index]); 459 builder.addFrameData(frameDataBuilder); 460 } 461 return builder.build(); 462 } 463 464 private ViewRef captureViewTree(View view, ViewRef start) { 465 ViewRef ref; 466 if (mPool != null) { 467 ref = mPool; 468 mPool = mPool.next; 469 ref.next = null; 470 } else { 471 ref = new ViewRef(); 472 } 473 ref.view = view; 474 start.next = ref; 475 if (view instanceof ViewGroup) { 476 ViewGroup parent = (ViewGroup) view; 477 // If a view has not changed since the last frame, we will copy 478 // its children from the last processed frame's data. 479 if ((view.mPrivateFlags & (PFLAG_INVALIDATED | PFLAG_DIRTY_MASK)) == 0 480 && !mIsFirstFrame) { 481 // A negative child count is the signal to copy this view from the last frame. 482 ref.childCount = -parent.getChildCount(); 483 return ref; 484 } 485 ViewRef result = ref; 486 int childCount = ref.childCount = parent.getChildCount(); 487 for (int i = 0; i < childCount; i++) { 488 result = captureViewTree(parent.getChildAt(i), result); 489 } 490 return result; 491 } else { 492 ref.childCount = 0; 493 return ref; 494 } 495 } 496 497 @Override 498 public void onTrimMemory(int level) { 499 if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { 500 mNodesBg = new ViewPropertyRef[0]; 501 mFrameTimesNanosBg = new long[0]; 502 if (mRoot != null && mRoot.getContext() != null) { 503 mRoot.getContext().unregisterComponentCallbacks(this); 504 } 505 detachFromRoot(); 506 mRoot = null; 507 } 508 } 509 510 @Override 511 public void onConfigurationChanged(Configuration configuration) { 512 // No Operation 513 } 514 515 @Override 516 public void onLowMemory() { 517 onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND); 518 } 519 } 520 521 protected static class ViewPropertyRef { 522 // We store reference in memory to avoid generating and storing too many strings 523 public Class clazz; 524 public int hashCode; 525 public int childCount = 0; 526 527 public int id; 528 public int left, top, right, bottom; 529 public int scrollX, scrollY; 530 531 public float translateX, translateY; 532 public float scaleX, scaleY; 533 public float alpha; 534 public float elevation; 535 536 public int visibility; 537 public boolean willNotDraw; 538 public boolean clipChildren; 539 540 public ViewPropertyRef next; 541 542 public void transferTo(ViewPropertyRef out) { 543 out.clazz = this.clazz; 544 out.hashCode = this.hashCode; 545 out.childCount = this.childCount; 546 out.id = this.id; 547 out.left = this.left; 548 out.top = this.top; 549 out.right = this.right; 550 out.bottom = this.bottom; 551 out.scrollX = this.scrollX; 552 out.scrollY = this.scrollY; 553 out.scaleX = this.scaleX; 554 out.scaleY = this.scaleY; 555 out.translateX = this.translateX; 556 out.translateY = this.translateY; 557 out.alpha = this.alpha; 558 out.visibility = this.visibility; 559 out.willNotDraw = this.willNotDraw; 560 out.clipChildren = this.clipChildren; 561 out.elevation = this.elevation; 562 } 563 564 /** 565 * Converts the data to the proto representation and returns the next property ref 566 * at the end of the iteration. 567 */ 568 public ViewPropertyRef toProto(ViewIdProvider idProvider, ArrayList<Class> classList, 569 ViewNode.Builder viewNode) { 570 int classnameIndex = classList.indexOf(clazz); 571 if (classnameIndex < 0) { 572 classnameIndex = classList.size(); 573 classList.add(clazz); 574 } 575 576 viewNode.setClassnameIndex(classnameIndex) 577 .setHashcode(hashCode) 578 .setId(idProvider.getName(id)) 579 .setLeft(left) 580 .setTop(top) 581 .setWidth(right - left) 582 .setHeight(bottom - top) 583 .setTranslationX(translateX) 584 .setTranslationY(translateY) 585 .setScrollX(scrollX) 586 .setScrollY(scrollY) 587 .setScaleX(scaleX) 588 .setScaleY(scaleY) 589 .setAlpha(alpha) 590 .setVisibility(visibility) 591 .setWillNotDraw(willNotDraw) 592 .setElevation(elevation) 593 .setClipChildren(clipChildren); 594 595 ViewPropertyRef result = next; 596 for (int i = 0; (i < childCount) && (result != null); i++) { 597 ViewNode.Builder childViewNode = ViewNode.newBuilder(); 598 result = result.toProto(idProvider, classList, childViewNode); 599 viewNode.addChildren(childViewNode); 600 } 601 return result; 602 } 603 } 604 605 606 private static class ViewRef implements Runnable { 607 public View view; 608 public int childCount = 0; 609 @Nullable 610 public ViewRef next; 611 612 public Consumer<ViewRef> callback = null; 613 public long elapsedRealtimeNanos = 0; 614 615 public void transferTo(ViewPropertyRef out) { 616 out.childCount = this.childCount; 617 618 View view = this.view; 619 this.view = null; 620 621 out.clazz = view.getClass(); 622 out.hashCode = view.hashCode(); 623 out.id = view.getId(); 624 out.left = view.getLeft(); 625 out.top = view.getTop(); 626 out.right = view.getRight(); 627 out.bottom = view.getBottom(); 628 out.scrollX = view.getScrollX(); 629 out.scrollY = view.getScrollY(); 630 631 out.translateX = view.getTranslationX(); 632 out.translateY = view.getTranslationY(); 633 out.scaleX = view.getScaleX(); 634 out.scaleY = view.getScaleY(); 635 out.alpha = view.getAlpha(); 636 out.elevation = view.getElevation(); 637 638 out.visibility = view.getVisibility(); 639 out.willNotDraw = view.willNotDraw(); 640 } 641 642 @Override 643 public void run() { 644 Consumer<ViewRef> oldCallback = callback; 645 callback = null; 646 if (oldCallback != null) { 647 oldCallback.accept(this); 648 } 649 } 650 } 651 652 protected static final class ViewIdProvider { 653 654 private final SparseArray<String> mNames = new SparseArray<>(); 655 private final Resources mRes; 656 657 ViewIdProvider(Resources res) { 658 mRes = res; 659 } 660 661 String getName(int id) { 662 String name = mNames.get(id); 663 if (name == null) { 664 if (id >= 0) { 665 try { 666 name = mRes.getResourceTypeName(id) + '/' + mRes.getResourceEntryName(id); 667 } catch (Resources.NotFoundException e) { 668 name = "id/" + "0x" + Integer.toHexString(id).toUpperCase(); 669 } 670 } else { 671 name = "NO_ID"; 672 } 673 mNames.put(id, name); 674 } 675 return name; 676 } 677 } 678 } 679