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