RenderSessionImpl.java revision 6c76b093911de7222d03e03aa94e13136808d45a
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.layoutlib.bridge.impl;
18
19import com.android.ide.common.rendering.api.AdapterBinding;
20import com.android.ide.common.rendering.api.HardwareConfig;
21import com.android.ide.common.rendering.api.IAnimationListener;
22import com.android.ide.common.rendering.api.ILayoutPullParser;
23import com.android.ide.common.rendering.api.LayoutLog;
24import com.android.ide.common.rendering.api.LayoutlibCallback;
25import com.android.ide.common.rendering.api.RenderResources;
26import com.android.ide.common.rendering.api.RenderSession;
27import com.android.ide.common.rendering.api.ResourceReference;
28import com.android.ide.common.rendering.api.ResourceValue;
29import com.android.ide.common.rendering.api.Result;
30import com.android.ide.common.rendering.api.Result.Status;
31import com.android.ide.common.rendering.api.SessionParams;
32import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
33import com.android.ide.common.rendering.api.ViewInfo;
34import com.android.ide.common.rendering.api.ViewType;
35import com.android.internal.view.menu.ActionMenuItemView;
36import com.android.internal.view.menu.BridgeMenuItemImpl;
37import com.android.internal.view.menu.IconMenuItemView;
38import com.android.internal.view.menu.ListMenuItemView;
39import com.android.internal.view.menu.MenuItemImpl;
40import com.android.internal.view.menu.MenuView;
41import com.android.layoutlib.bridge.Bridge;
42import com.android.layoutlib.bridge.android.BridgeContext;
43import com.android.layoutlib.bridge.android.BridgeLayoutParamsMapAttributes;
44import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
45import com.android.layoutlib.bridge.android.RenderParamsFlags;
46import com.android.layoutlib.bridge.android.graphics.NopCanvas;
47import com.android.layoutlib.bridge.android.support.DesignLibUtil;
48import com.android.layoutlib.bridge.android.support.SupportPreferencesUtil;
49import com.android.layoutlib.bridge.impl.binding.FakeAdapter;
50import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter;
51import com.android.resources.ResourceType;
52import com.android.tools.layoutlib.java.System_Delegate;
53import com.android.util.Pair;
54import com.android.util.PropertiesMap;
55
56import android.animation.AnimationThread;
57import android.animation.Animator;
58import android.animation.AnimatorInflater;
59import android.animation.LayoutTransition;
60import android.animation.LayoutTransition.TransitionListener;
61import android.annotation.NonNull;
62import android.annotation.Nullable;
63import android.app.Fragment_Delegate;
64import android.graphics.Bitmap;
65import android.graphics.Bitmap_Delegate;
66import android.graphics.Canvas;
67import android.os.Looper;
68import android.preference.Preference_Delegate;
69import android.view.AttachInfo_Accessor;
70import android.view.BridgeInflater;
71import android.view.Choreographer_Delegate;
72import android.view.View;
73import android.view.View.MeasureSpec;
74import android.view.ViewGroup;
75import android.view.ViewGroup.LayoutParams;
76import android.view.ViewGroup.MarginLayoutParams;
77import android.view.ViewParent;
78import android.widget.AbsListView;
79import android.widget.AbsSpinner;
80import android.widget.ActionMenuView;
81import android.widget.AdapterView;
82import android.widget.ExpandableListView;
83import android.widget.FrameLayout;
84import android.widget.LinearLayout;
85import android.widget.ListView;
86import android.widget.QuickContactBadge;
87import android.widget.TabHost;
88import android.widget.TabHost.TabSpec;
89import android.widget.TabWidget;
90
91import java.awt.AlphaComposite;
92import java.awt.Color;
93import java.awt.Graphics2D;
94import java.awt.image.BufferedImage;
95import java.util.ArrayList;
96import java.util.List;
97import java.util.Map;
98
99import static com.android.ide.common.rendering.api.Result.Status.ERROR_ANIM_NOT_FOUND;
100import static com.android.ide.common.rendering.api.Result.Status.ERROR_INFLATION;
101import static com.android.ide.common.rendering.api.Result.Status.ERROR_NOT_INFLATED;
102import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
103import static com.android.ide.common.rendering.api.Result.Status.ERROR_VIEWGROUP_NO_CHILDREN;
104import static com.android.ide.common.rendering.api.Result.Status.SUCCESS;
105import static com.android.layoutlib.bridge.util.ReflectionUtils.isInstanceOf;
106
107/**
108 * Class implementing the render session.
109 * <p/>
110 * A session is a stateful representation of a layout file. It is initialized with data coming
111 * through the {@link Bridge} API to inflate the layout. Further actions and rendering can then
112 * be done on the layout.
113 */
114public class RenderSessionImpl extends RenderAction<SessionParams> {
115
116    private static final Canvas NOP_CANVAS = new NopCanvas();
117
118    // scene state
119    private RenderSession mScene;
120    private BridgeXmlBlockParser mBlockParser;
121    private BridgeInflater mInflater;
122    private ViewGroup mViewRoot;
123    private FrameLayout mContentRoot;
124    private Canvas mCanvas;
125    private int mMeasuredScreenWidth = -1;
126    private int mMeasuredScreenHeight = -1;
127    private boolean mIsAlphaChannelImage;
128    /** If >= 0, a frame will be executed */
129    private long mElapsedFrameTimeNanos = -1;
130    /** True if one frame has been already executed to start the animations */
131    private boolean mFirstFrameExecuted = false;
132
133    // information being returned through the API
134    private BufferedImage mImage;
135    private List<ViewInfo> mViewInfoList;
136    private List<ViewInfo> mSystemViewInfoList;
137    private Layout.Builder mLayoutBuilder;
138    private boolean mNewRenderSize;
139
140    private static final class PostInflateException extends Exception {
141        private static final long serialVersionUID = 1L;
142
143        private PostInflateException(String message) {
144            super(message);
145        }
146    }
147
148    /**
149     * Creates a layout scene with all the information coming from the layout bridge API.
150     * <p>
151     * This <b>must</b> be followed by a call to {@link RenderSessionImpl#init(long)},
152     * which act as a
153     * call to {@link RenderSessionImpl#acquire(long)}
154     *
155     * @see Bridge#createSession(SessionParams)
156     */
157    public RenderSessionImpl(SessionParams params) {
158        super(new SessionParams(params));
159    }
160
161    /**
162     * Initializes and acquires the scene, creating various Android objects such as context,
163     * inflater, and parser.
164     *
165     * @param timeout the time to wait if another rendering is happening.
166     *
167     * @return whether the scene was prepared
168     *
169     * @see #acquire(long)
170     * @see #release()
171     */
172    @Override
173    public Result init(long timeout) {
174        Result result = super.init(timeout);
175        if (!result.isSuccess()) {
176            return result;
177        }
178
179        SessionParams params = getParams();
180        BridgeContext context = getContext();
181
182        // use default of true in case it's not found to use alpha by default
183        mIsAlphaChannelImage = ResourceHelper.getBooleanThemeValue(params.getResources(),
184                "windowIsFloating", true, true);
185
186        mLayoutBuilder = new Layout.Builder(params, context);
187
188        // build the inflater and parser.
189        mInflater = new BridgeInflater(context, params.getLayoutlibCallback());
190        context.setBridgeInflater(mInflater);
191
192        mBlockParser = new BridgeXmlBlockParser(params.getLayoutDescription(), context, false);
193
194        return SUCCESS.createResult();
195    }
196
197    /**
198     * Measures the the current layout if needed (see {@link #invalidateRenderingSize}).
199     */
200    private void measureLayout(@NonNull SessionParams params) {
201        // only do the screen measure when needed.
202        if (mMeasuredScreenWidth != -1) {
203            return;
204        }
205
206        RenderingMode renderingMode = params.getRenderingMode();
207        HardwareConfig hardwareConfig = params.getHardwareConfig();
208
209        mNewRenderSize = true;
210        mMeasuredScreenWidth = hardwareConfig.getScreenWidth();
211        mMeasuredScreenHeight = hardwareConfig.getScreenHeight();
212
213        if (renderingMode != RenderingMode.NORMAL) {
214            int widthMeasureSpecMode = renderingMode.isHorizExpand() ?
215                    MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
216                    : MeasureSpec.EXACTLY;
217            int heightMeasureSpecMode = renderingMode.isVertExpand() ?
218                    MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
219                    : MeasureSpec.EXACTLY;
220
221            // We used to compare the measured size of the content to the screen size but
222            // this does not work anymore due to the 2 following issues:
223            // - If the content is in a decor (system bar, title/action bar), the root view
224            //   will not resize even with the UNSPECIFIED because of the embedded layout.
225            // - If there is no decor, but a dialog frame, then the dialog padding prevents
226            //   comparing the size of the content to the screen frame (as it would not
227            //   take into account the dialog padding).
228
229            // The solution is to first get the content size in a normal rendering, inside
230            // the decor or the dialog padding.
231            // Then measure only the content with UNSPECIFIED to see the size difference
232            // and apply this to the screen size.
233
234            View measuredView = mContentRoot.getChildAt(0);
235
236            // first measure the full layout, with EXACTLY to get the size of the
237            // content as it is inside the decor/dialog
238            @SuppressWarnings("deprecation")
239            Pair<Integer, Integer> exactMeasure = measureView(
240                    mViewRoot, measuredView,
241                    mMeasuredScreenWidth, MeasureSpec.EXACTLY,
242                    mMeasuredScreenHeight, MeasureSpec.EXACTLY);
243
244            // now measure the content only using UNSPECIFIED (where applicable, based on
245            // the rendering mode). This will give us the size the content needs.
246            @SuppressWarnings("deprecation")
247            Pair<Integer, Integer> result = measureView(
248                    mContentRoot, mContentRoot.getChildAt(0),
249                    mMeasuredScreenWidth, widthMeasureSpecMode,
250                    mMeasuredScreenHeight, heightMeasureSpecMode);
251
252            // If measuredView is not null, exactMeasure nor result will be null.
253            assert exactMeasure != null;
254            assert result != null;
255
256            // now look at the difference and add what is needed.
257            if (renderingMode.isHorizExpand()) {
258                int measuredWidth = exactMeasure.getFirst();
259                int neededWidth = result.getFirst();
260                if (neededWidth > measuredWidth) {
261                    mMeasuredScreenWidth += neededWidth - measuredWidth;
262                }
263                if (mMeasuredScreenWidth < measuredWidth) {
264                    // If the screen width is less than the exact measured width,
265                    // expand to match.
266                    mMeasuredScreenWidth = measuredWidth;
267                }
268            }
269
270            if (renderingMode.isVertExpand()) {
271                int measuredHeight = exactMeasure.getSecond();
272                int neededHeight = result.getSecond();
273                if (neededHeight > measuredHeight) {
274                    mMeasuredScreenHeight += neededHeight - measuredHeight;
275                }
276                if (mMeasuredScreenHeight < measuredHeight) {
277                    // If the screen height is less than the exact measured height,
278                    // expand to match.
279                    mMeasuredScreenHeight = measuredHeight;
280                }
281            }
282        }
283    }
284
285    /**
286     * Inflates the layout.
287     * <p>
288     * {@link #acquire(long)} must have been called before this.
289     *
290     * @throws IllegalStateException if the current context is different than the one owned by
291     *      the scene, or if {@link #init(long)} was not called.
292     */
293    public Result inflate() {
294        checkLock();
295
296        try {
297            mViewRoot = new Layout(mLayoutBuilder);
298            mLayoutBuilder = null;  // Done with the builder.
299            mContentRoot = ((Layout) mViewRoot).getContentRoot();
300            SessionParams params = getParams();
301            BridgeContext context = getContext();
302
303            if (Bridge.isLocaleRtl(params.getLocale())) {
304                if (!params.isRtlSupported()) {
305                    Bridge.getLog().warning(LayoutLog.TAG_RTL_NOT_ENABLED,
306                            "You are using a right-to-left " +
307                                    "(RTL) locale but RTL is not enabled", null);
308                } else if (params.getSimulatedPlatformVersion() < 17) {
309                    // This will render ok because we are using the latest layoutlib but at least
310                    // warn the user that this might fail in a real device.
311                    Bridge.getLog().warning(LayoutLog.TAG_RTL_NOT_SUPPORTED, "You are using a " +
312                            "right-to-left " +
313                            "(RTL) locale but RTL is not supported for API level < 17", null);
314                }
315            }
316
317            // Sets the project callback (custom view loader) to the fragment delegate so that
318            // it can instantiate the custom Fragment.
319            Fragment_Delegate.setLayoutlibCallback(params.getLayoutlibCallback());
320
321            String rootTag = params.getFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG);
322            boolean isPreference = "PreferenceScreen".equals(rootTag);
323            View view;
324            if (isPreference) {
325                // First try to use the support library inflater. If something fails, fallback
326                // to the system preference inflater.
327                view = SupportPreferencesUtil.inflatePreference(getContext(), mBlockParser,
328                        mContentRoot);
329                if (view == null) {
330                    view = Preference_Delegate.inflatePreference(getContext(), mBlockParser,
331                            mContentRoot);
332                }
333            } else {
334                view = mInflater.inflate(mBlockParser, mContentRoot);
335            }
336
337            // done with the parser, pop it.
338            context.popParser();
339
340            Fragment_Delegate.setLayoutlibCallback(null);
341
342            // set the AttachInfo on the root view.
343            AttachInfo_Accessor.setAttachInfo(mViewRoot);
344
345            // post-inflate process. For now this supports TabHost/TabWidget
346            postInflateProcess(view, params.getLayoutlibCallback(), isPreference ? view : null);
347            mInflater.onDoneInflation();
348
349            setActiveToolbar(view, context, params);
350
351            measureLayout(params);
352            measureView(mViewRoot, null /*measuredView*/,
353                    mMeasuredScreenWidth, MeasureSpec.EXACTLY,
354                    mMeasuredScreenHeight, MeasureSpec.EXACTLY);
355            mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight);
356            mSystemViewInfoList =
357                    visitAllChildren(mViewRoot, 0, 0, params.getExtendedViewInfoMode(),
358                    false);
359
360            return SUCCESS.createResult();
361        } catch (PostInflateException e) {
362            return ERROR_INFLATION.createResult(e.getMessage(), e);
363        } catch (Throwable e) {
364            // get the real cause of the exception.
365            Throwable t = e;
366            while (t.getCause() != null) {
367                t = t.getCause();
368            }
369
370            return ERROR_INFLATION.createResult(t.getMessage(), t);
371        }
372    }
373
374    /**
375     * Sets the time for which the next frame will be selected. The time is the elapsed time from
376     * the current system nanos time. You
377     */
378    public void setElapsedFrameTimeNanos(long nanos) {
379        mElapsedFrameTimeNanos = nanos;
380    }
381
382    /**
383     * Runs a layout pass for the given view root
384     */
385    private static void doLayout(@NonNull BridgeContext context, @NonNull ViewGroup viewRoot,
386            int width, int height) {
387        // measure again with the size we need
388        // This must always be done before the call to layout
389        measureView(viewRoot, null /*measuredView*/,
390                width, MeasureSpec.EXACTLY,
391                height, MeasureSpec.EXACTLY);
392
393        // now do the layout.
394        viewRoot.layout(0, 0, width, height);
395        handleScrolling(context, viewRoot);
396    }
397
398    /**
399     * Renders the given view hierarchy to the passed canvas and returns the result of the render
400     * operation.
401     * @param canvas an optional canvas to render the views to. If null, only the measure and
402     * layout steps will be executed.
403     */
404    private static Result renderAndBuildResult(@NonNull ViewGroup viewRoot, @Nullable Canvas canvas) {
405        if (canvas == null) {
406            return SUCCESS.createResult();
407        }
408
409        AttachInfo_Accessor.dispatchOnPreDraw(viewRoot);
410        viewRoot.draw(canvas);
411
412        return SUCCESS.createResult();
413    }
414
415    /**
416     * Renders the scene.
417     * <p>
418     * {@link #acquire(long)} must have been called before this.
419     *
420     * @param freshRender whether the render is a new one and should erase the existing bitmap (in
421     *      the case where bitmaps are reused). This is typically needed when not playing
422     *      animations.)
423     *
424     * @throws IllegalStateException if the current context is different than the one owned by
425     *      the scene, or if {@link #acquire(long)} was not called.
426     *
427     * @see SessionParams#getRenderingMode()
428     * @see RenderSession#render(long)
429     */
430    public Result render(boolean freshRender) {
431        return renderAndBuildResult(freshRender, false);
432    }
433
434    /**
435     * Measures the layout
436     * <p>
437     * {@link #acquire(long)} must have been called before this.
438     *
439     * @throws IllegalStateException if the current context is different than the one owned by
440     *      the scene, or if {@link #acquire(long)} was not called.
441     *
442     * @see SessionParams#getRenderingMode()
443     * @see RenderSession#render(long)
444     */
445    public Result measure() {
446        return renderAndBuildResult(false, true);
447    }
448
449    /**
450     * Renders the scene.
451     * <p>
452     * {@link #acquire(long)} must have been called before this.
453     *
454     * @param freshRender whether the render is a new one and should erase the existing bitmap (in
455     *      the case where bitmaps are reused). This is typically needed when not playing
456     *      animations.)
457     *
458     * @throws IllegalStateException if the current context is different than the one owned by
459     *      the scene, or if {@link #acquire(long)} was not called.
460     *
461     * @see SessionParams#getRenderingMode()
462     * @see RenderSession#render(long)
463     */
464    private Result renderAndBuildResult(boolean freshRender, boolean onlyMeasure) {
465        checkLock();
466
467        SessionParams params = getParams();
468
469        try {
470            if (mViewRoot == null) {
471                return ERROR_NOT_INFLATED.createResult();
472            }
473
474            measureLayout(params);
475
476            HardwareConfig hardwareConfig = params.getHardwareConfig();
477            Result renderResult = SUCCESS.createResult();
478            if (onlyMeasure) {
479                // delete the canvas and image to reset them on the next full rendering
480                mImage = null;
481                mCanvas = null;
482                doLayout(getContext(), mViewRoot, mMeasuredScreenWidth, mMeasuredScreenHeight);
483            } else {
484                // draw the views
485                // create the BufferedImage into which the layout will be rendered.
486                boolean newImage = false;
487
488                // When disableBitmapCaching is true, we do not reuse mImage and
489                // we create a new one in every render.
490                // This is useful when mImage is just a wrapper of Graphics2D so
491                // it doesn't get cached.
492                boolean disableBitmapCaching = Boolean.TRUE.equals(params.getFlag(
493                    RenderParamsFlags.FLAG_KEY_DISABLE_BITMAP_CACHING));
494                if (mNewRenderSize || mCanvas == null || disableBitmapCaching) {
495                    mNewRenderSize = false;
496                    if (params.getImageFactory() != null) {
497                        mImage = params.getImageFactory().getImage(
498                                mMeasuredScreenWidth,
499                                mMeasuredScreenHeight);
500                    } else {
501                        mImage = new BufferedImage(
502                                mMeasuredScreenWidth,
503                                mMeasuredScreenHeight,
504                                BufferedImage.TYPE_INT_ARGB);
505                        newImage = true;
506                    }
507
508                    if (params.isBgColorOverridden()) {
509                        // since we override the content, it's the same as if it was a new image.
510                        newImage = true;
511                        Graphics2D gc = mImage.createGraphics();
512                        gc.setColor(new Color(params.getOverrideBgColor(), true));
513                        gc.setComposite(AlphaComposite.Src);
514                        gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight);
515                        gc.dispose();
516                    }
517
518                    // create an Android bitmap around the BufferedImage
519                    Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage,
520                            true /*isMutable*/, hardwareConfig.getDensity());
521
522                    if (mCanvas == null) {
523                        // create a Canvas around the Android bitmap
524                        mCanvas = new Canvas(bitmap);
525                    } else {
526                        mCanvas.setBitmap(bitmap);
527                    }
528                    mCanvas.setDensity(hardwareConfig.getDensity().getDpiValue());
529                }
530
531                if (freshRender && !newImage) {
532                    Graphics2D gc = mImage.createGraphics();
533                    gc.setComposite(AlphaComposite.Src);
534
535                    gc.setColor(new Color(0x00000000, true));
536                    gc.fillRect(0, 0,
537                            mMeasuredScreenWidth, mMeasuredScreenHeight);
538
539                    // done
540                    gc.dispose();
541                }
542
543                doLayout(getContext(), mViewRoot, mMeasuredScreenWidth, mMeasuredScreenHeight);
544                if (mElapsedFrameTimeNanos >= 0) {
545                    long initialTime = System_Delegate.nanoTime();
546                    if (!mFirstFrameExecuted) {
547                        // We need to run an initial draw call to initialize the animations
548                        renderAndBuildResult(mViewRoot, NOP_CANVAS);
549
550                        // The first frame will initialize the animations
551                        Choreographer_Delegate.doFrame(initialTime);
552                        mFirstFrameExecuted = true;
553                    }
554                    // Second frame will move the animations
555                    Choreographer_Delegate.doFrame(initialTime + mElapsedFrameTimeNanos);
556                }
557                renderResult = renderAndBuildResult(mViewRoot, mCanvas);
558            }
559
560            mSystemViewInfoList =
561                    visitAllChildren(mViewRoot, 0, 0, params.getExtendedViewInfoMode(),
562                    false);
563
564            // success!
565            return renderResult;
566        } catch (Throwable e) {
567            // get the real cause of the exception.
568            Throwable t = e;
569            while (t.getCause() != null) {
570                t = t.getCause();
571            }
572
573            return ERROR_UNKNOWN.createResult(t.getMessage(), t);
574        }
575    }
576
577    /**
578     * Executes {@link View#measure(int, int)} on a given view with the given parameters (used
579     * to create measure specs with {@link MeasureSpec#makeMeasureSpec(int, int)}.
580     *
581     * if <var>measuredView</var> is non null, the method returns a {@link Pair} of (width, height)
582     * for the view (using {@link View#getMeasuredWidth()} and {@link View#getMeasuredHeight()}).
583     *
584     * @param viewToMeasure the view on which to execute measure().
585     * @param measuredView if non null, the view to query for its measured width/height.
586     * @param width the width to use in the MeasureSpec.
587     * @param widthMode the MeasureSpec mode to use for the width.
588     * @param height the height to use in the MeasureSpec.
589     * @param heightMode the MeasureSpec mode to use for the height.
590     * @return the measured width/height if measuredView is non-null, null otherwise.
591     */
592    @SuppressWarnings("deprecation")  // For the use of Pair
593    private static Pair<Integer, Integer> measureView(ViewGroup viewToMeasure, View measuredView,
594            int width, int widthMode, int height, int heightMode) {
595        int w_spec = MeasureSpec.makeMeasureSpec(width, widthMode);
596        int h_spec = MeasureSpec.makeMeasureSpec(height, heightMode);
597        viewToMeasure.measure(w_spec, h_spec);
598
599        if (measuredView != null) {
600            return Pair.of(measuredView.getMeasuredWidth(), measuredView.getMeasuredHeight());
601        }
602
603        return null;
604    }
605
606    /**
607     * Animate an object
608     * <p>
609     * {@link #acquire(long)} must have been called before this.
610     *
611     * @throws IllegalStateException if the current context is different than the one owned by
612     *      the scene, or if {@link #acquire(long)} was not called.
613     *
614     * @see RenderSession#animate(Object, String, boolean, IAnimationListener)
615     */
616    public Result animate(Object targetObject, String animationName,
617            boolean isFrameworkAnimation, IAnimationListener listener) {
618        checkLock();
619
620        BridgeContext context = getContext();
621
622        // find the animation file.
623        ResourceValue animationResource;
624        int animationId = 0;
625        if (isFrameworkAnimation) {
626            animationResource = context.getRenderResources().getFrameworkResource(
627                    ResourceType.ANIMATOR, animationName);
628            if (animationResource != null) {
629                animationId = Bridge.getResourceId(ResourceType.ANIMATOR, animationName);
630            }
631        } else {
632            animationResource = context.getRenderResources().getProjectResource(
633                    ResourceType.ANIMATOR, animationName);
634            if (animationResource != null) {
635                animationId = context.getLayoutlibCallback().getResourceId(
636                        ResourceType.ANIMATOR, animationName);
637            }
638        }
639
640        if (animationResource != null) {
641            try {
642                Animator anim = AnimatorInflater.loadAnimator(context, animationId);
643                if (anim != null) {
644                    anim.setTarget(targetObject);
645
646                    new PlayAnimationThread(anim, this, animationName, listener).start();
647
648                    return SUCCESS.createResult();
649                }
650            } catch (Exception e) {
651                // get the real cause of the exception.
652                Throwable t = e;
653                while (t.getCause() != null) {
654                    t = t.getCause();
655                }
656
657                return ERROR_UNKNOWN.createResult(t.getMessage(), t);
658            }
659        }
660
661        return ERROR_ANIM_NOT_FOUND.createResult();
662    }
663
664    /**
665     * Insert a new child into an existing parent.
666     * <p>
667     * {@link #acquire(long)} must have been called before this.
668     *
669     * @throws IllegalStateException if the current context is different than the one owned by
670     *      the scene, or if {@link #acquire(long)} was not called.
671     *
672     * @see RenderSession#insertChild(Object, ILayoutPullParser, int, IAnimationListener)
673     */
674    public Result insertChild(final ViewGroup parentView, ILayoutPullParser childXml,
675            final int index, IAnimationListener listener) {
676        checkLock();
677
678        BridgeContext context = getContext();
679
680        // create a block parser for the XML
681        BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(
682                childXml, context, false /* platformResourceFlag */);
683
684        // inflate the child without adding it to the root since we want to control where it'll
685        // get added. We do pass the parentView however to ensure that the layoutParams will
686        // be created correctly.
687        final View child = mInflater.inflate(blockParser, parentView, false /*attachToRoot*/);
688        blockParser.ensurePopped();
689
690        invalidateRenderingSize();
691
692        if (listener != null) {
693            new AnimationThread(this, "insertChild", listener) {
694
695                @Override
696                public Result preAnimation() {
697                    parentView.setLayoutTransition(new LayoutTransition());
698                    return addView(parentView, child, index);
699                }
700
701                @Override
702                public void postAnimation() {
703                    parentView.setLayoutTransition(null);
704                }
705            }.start();
706
707            // always return success since the real status will come through the listener.
708            return SUCCESS.createResult(child);
709        }
710
711        // add it to the parentView in the correct location
712        Result result = addView(parentView, child, index);
713        if (!result.isSuccess()) {
714            return result;
715        }
716
717        result = render(false /*freshRender*/);
718        if (result.isSuccess()) {
719            result = result.getCopyWithData(child);
720        }
721
722        return result;
723    }
724
725    /**
726     * Adds a given view to a given parent at a given index.
727     *
728     * @param parent the parent to receive the view
729     * @param view the view to add to the parent
730     * @param index the index where to do the add.
731     *
732     * @return a Result with {@link Status#SUCCESS} or
733     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
734     *     adding views.
735     */
736    private Result addView(ViewGroup parent, View view, int index) {
737        try {
738            parent.addView(view, index);
739            return SUCCESS.createResult();
740        } catch (UnsupportedOperationException e) {
741            // looks like this is a view class that doesn't support children manipulation!
742            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
743        }
744    }
745
746    /**
747     * Moves a view to a new parent at a given location
748     * <p>
749     * {@link #acquire(long)} must have been called before this.
750     *
751     * @throws IllegalStateException if the current context is different than the one owned by
752     *      the scene, or if {@link #acquire(long)} was not called.
753     *
754     * @see RenderSession#moveChild(Object, Object, int, Map, IAnimationListener)
755     */
756    public Result moveChild(final ViewGroup newParentView, final View childView, final int index,
757            Map<String, String> layoutParamsMap, final IAnimationListener listener) {
758        checkLock();
759
760        invalidateRenderingSize();
761
762        LayoutParams layoutParams = null;
763        if (layoutParamsMap != null) {
764            // need to create a new LayoutParams object for the new parent.
765            layoutParams = newParentView.generateLayoutParams(
766                    new BridgeLayoutParamsMapAttributes(layoutParamsMap));
767        }
768
769        // get the current parent of the view that needs to be moved.
770        final ViewGroup previousParent = (ViewGroup) childView.getParent();
771
772        if (listener != null) {
773            final LayoutParams params = layoutParams;
774
775            // there is no support for animating views across layouts, so in case the new and old
776            // parent views are different we fake the animation through a no animation thread.
777            if (previousParent != newParentView) {
778                new Thread("not animated moveChild") {
779                    @Override
780                    public void run() {
781                        Result result = moveView(previousParent, newParentView, childView, index,
782                                params);
783                        if (!result.isSuccess()) {
784                            listener.done(result);
785                        }
786
787                        // ready to do the work, acquire the scene.
788                        result = acquire(250);
789                        if (!result.isSuccess()) {
790                            listener.done(result);
791                            return;
792                        }
793
794                        try {
795                            result = render(false /*freshRender*/);
796                            if (result.isSuccess()) {
797                                listener.onNewFrame(RenderSessionImpl.this.getSession());
798                            }
799                        } finally {
800                            release();
801                        }
802
803                        listener.done(result);
804                    }
805                }.start();
806            } else {
807                new AnimationThread(this, "moveChild", listener) {
808
809                    @Override
810                    public Result preAnimation() {
811                        // set up the transition for the parent.
812                        LayoutTransition transition = new LayoutTransition();
813                        previousParent.setLayoutTransition(transition);
814
815                        // tweak the animation durations and start delays (to match the duration of
816                        // animation playing just before).
817                        // Note: Cannot user Animation.setDuration() directly. Have to set it
818                        // on the LayoutTransition.
819                        transition.setDuration(LayoutTransition.DISAPPEARING, 100);
820                        // CHANGE_DISAPPEARING plays after DISAPPEARING
821                        transition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 100);
822
823                        transition.setDuration(LayoutTransition.CHANGE_DISAPPEARING, 100);
824
825                        transition.setDuration(LayoutTransition.CHANGE_APPEARING, 100);
826                        // CHANGE_APPEARING plays after CHANGE_APPEARING
827                        transition.setStartDelay(LayoutTransition.APPEARING, 100);
828
829                        transition.setDuration(LayoutTransition.APPEARING, 100);
830
831                        return moveView(previousParent, newParentView, childView, index, params);
832                    }
833
834                    @Override
835                    public void postAnimation() {
836                        previousParent.setLayoutTransition(null);
837                        newParentView.setLayoutTransition(null);
838                    }
839                }.start();
840            }
841
842            // always return success since the real status will come through the listener.
843            return SUCCESS.createResult(layoutParams);
844        }
845
846        Result result = moveView(previousParent, newParentView, childView, index, layoutParams);
847        if (!result.isSuccess()) {
848            return result;
849        }
850
851        result = render(false /*freshRender*/);
852        if (layoutParams != null && result.isSuccess()) {
853            result = result.getCopyWithData(layoutParams);
854        }
855
856        return result;
857    }
858
859    /**
860     * Moves a View from its current parent to a new given parent at a new given location, with
861     * an optional new {@link LayoutParams} instance
862     *
863     * @param previousParent the previous parent, still owning the child at the time of the call.
864     * @param newParent the new parent
865     * @param movedView the view to move
866     * @param index the new location in the new parent
867     * @param params an option (can be null) {@link LayoutParams} instance.
868     *
869     * @return a Result with {@link Status#SUCCESS} or
870     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
871     *     adding views.
872     */
873    private Result moveView(ViewGroup previousParent, final ViewGroup newParent,
874            final View movedView, final int index, final LayoutParams params) {
875        try {
876            // check if there is a transition on the previousParent.
877            LayoutTransition previousTransition = previousParent.getLayoutTransition();
878            if (previousTransition != null) {
879                // in this case there is an animation. This means we have to wait for the child's
880                // parent reference to be null'ed out so that we can add it to the new parent.
881                // It is technically removed right before the DISAPPEARING animation is done (if
882                // the animation of this type is not null, otherwise it's after which is impossible
883                // to handle).
884                // Because there is no move animation, if the new parent is the same as the old
885                // parent, we need to wait until the CHANGE_DISAPPEARING animation is done before
886                // adding the child or the child will appear in its new location before the
887                // other children have made room for it.
888
889                // add a listener to the transition to be notified of the actual removal.
890                previousTransition.addTransitionListener(new TransitionListener() {
891                    private int mChangeDisappearingCount = 0;
892
893                    @Override
894                    public void startTransition(LayoutTransition transition, ViewGroup container,
895                            View view, int transitionType) {
896                        if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) {
897                            mChangeDisappearingCount++;
898                        }
899                    }
900
901                    @Override
902                    public void endTransition(LayoutTransition transition, ViewGroup container,
903                            View view, int transitionType) {
904                        if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) {
905                            mChangeDisappearingCount--;
906                        }
907
908                        if (transitionType == LayoutTransition.CHANGE_DISAPPEARING &&
909                                mChangeDisappearingCount == 0) {
910                            // add it to the parentView in the correct location
911                            if (params != null) {
912                                newParent.addView(movedView, index, params);
913                            } else {
914                                newParent.addView(movedView, index);
915                            }
916                        }
917                    }
918                });
919
920                // remove the view from the current parent.
921                previousParent.removeView(movedView);
922
923                // and return since adding the view to the new parent is done in the listener.
924                return SUCCESS.createResult();
925            } else {
926                // standard code with no animation. pretty simple.
927                previousParent.removeView(movedView);
928
929                // add it to the parentView in the correct location
930                if (params != null) {
931                    newParent.addView(movedView, index, params);
932                } else {
933                    newParent.addView(movedView, index);
934                }
935
936                return SUCCESS.createResult();
937            }
938        } catch (UnsupportedOperationException e) {
939            // looks like this is a view class that doesn't support children manipulation!
940            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
941        }
942    }
943
944    /**
945     * Removes a child from its current parent.
946     * <p>
947     * {@link #acquire(long)} must have been called before this.
948     *
949     * @throws IllegalStateException if the current context is different than the one owned by
950     *      the scene, or if {@link #acquire(long)} was not called.
951     *
952     * @see RenderSession#removeChild(Object, IAnimationListener)
953     */
954    public Result removeChild(final View childView, IAnimationListener listener) {
955        checkLock();
956
957        invalidateRenderingSize();
958
959        final ViewGroup parent = (ViewGroup) childView.getParent();
960
961        if (listener != null) {
962            new AnimationThread(this, "moveChild", listener) {
963
964                @Override
965                public Result preAnimation() {
966                    parent.setLayoutTransition(new LayoutTransition());
967                    return removeView(parent, childView);
968                }
969
970                @Override
971                public void postAnimation() {
972                    parent.setLayoutTransition(null);
973                }
974            }.start();
975
976            // always return success since the real status will come through the listener.
977            return SUCCESS.createResult();
978        }
979
980        Result result = removeView(parent, childView);
981        if (!result.isSuccess()) {
982            return result;
983        }
984
985        return render(false /*freshRender*/);
986    }
987
988    /**
989     * Removes a given view from its current parent.
990     *
991     * @param view the view to remove from its parent
992     *
993     * @return a Result with {@link Status#SUCCESS} or
994     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
995     *     adding views.
996     */
997    private Result removeView(ViewGroup parent, View view) {
998        try {
999            parent.removeView(view);
1000            return SUCCESS.createResult();
1001        } catch (UnsupportedOperationException e) {
1002            // looks like this is a view class that doesn't support children manipulation!
1003            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
1004        }
1005    }
1006
1007    /**
1008     * Post process on a view hierarchy that was just inflated.
1009     * <p/>
1010     * At the moment this only supports TabHost: If {@link TabHost} is detected, look for the
1011     * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically
1012     * based on the content of the {@link FrameLayout}.
1013     * @param view the root view to process.
1014     * @param layoutlibCallback callback to the project.
1015     * @param skip the view and it's children are not processed.
1016     */
1017    @SuppressWarnings("deprecation")  // For the use of Pair
1018    private void postInflateProcess(View view, LayoutlibCallback layoutlibCallback, View skip)
1019            throws PostInflateException {
1020        if (view == skip) {
1021            return;
1022        }
1023        if (view instanceof TabHost) {
1024            setupTabHost((TabHost) view, layoutlibCallback);
1025        } else if (view instanceof QuickContactBadge) {
1026            QuickContactBadge badge = (QuickContactBadge) view;
1027            badge.setImageToDefault();
1028        } else if (view instanceof AdapterView<?>) {
1029            // get the view ID.
1030            int id = view.getId();
1031
1032            BridgeContext context = getContext();
1033
1034            // get a ResourceReference from the integer ID.
1035            ResourceReference listRef = context.resolveId(id);
1036
1037            if (listRef != null) {
1038                SessionParams params = getParams();
1039                AdapterBinding binding = params.getAdapterBindings().get(listRef);
1040
1041                // if there was no adapter binding, trying to get it from the call back.
1042                if (binding == null) {
1043                    binding = layoutlibCallback.getAdapterBinding(
1044                            listRef, context.getViewKey(view), view);
1045                }
1046
1047                if (binding != null) {
1048
1049                    if (view instanceof AbsListView) {
1050                        if ((binding.getFooterCount() > 0 || binding.getHeaderCount() > 0) &&
1051                                view instanceof ListView) {
1052                            ListView list = (ListView) view;
1053
1054                            boolean skipCallbackParser = false;
1055
1056                            int count = binding.getHeaderCount();
1057                            for (int i = 0; i < count; i++) {
1058                                Pair<View, Boolean> pair = context.inflateView(
1059                                        binding.getHeaderAt(i),
1060                                        list, false, skipCallbackParser);
1061                                if (pair.getFirst() != null) {
1062                                    list.addHeaderView(pair.getFirst());
1063                                }
1064
1065                                skipCallbackParser |= pair.getSecond();
1066                            }
1067
1068                            count = binding.getFooterCount();
1069                            for (int i = 0; i < count; i++) {
1070                                Pair<View, Boolean> pair = context.inflateView(
1071                                        binding.getFooterAt(i),
1072                                        list, false, skipCallbackParser);
1073                                if (pair.getFirst() != null) {
1074                                    list.addFooterView(pair.getFirst());
1075                                }
1076
1077                                skipCallbackParser |= pair.getSecond();
1078                            }
1079                        }
1080
1081                        if (view instanceof ExpandableListView) {
1082                            ((ExpandableListView) view).setAdapter(
1083                                    new FakeExpandableAdapter(listRef, binding, layoutlibCallback));
1084                        } else {
1085                            ((AbsListView) view).setAdapter(
1086                                    new FakeAdapter(listRef, binding, layoutlibCallback));
1087                        }
1088                    } else if (view instanceof AbsSpinner) {
1089                        ((AbsSpinner) view).setAdapter(
1090                                new FakeAdapter(listRef, binding, layoutlibCallback));
1091                    }
1092                }
1093            }
1094        } else if (view instanceof ViewGroup) {
1095            mInflater.postInflateProcess(view);
1096            ViewGroup group = (ViewGroup) view;
1097            final int count = group.getChildCount();
1098            for (int c = 0; c < count; c++) {
1099                View child = group.getChildAt(c);
1100                postInflateProcess(child, layoutlibCallback, skip);
1101            }
1102        }
1103    }
1104
1105    /**
1106     * If the root layout is a CoordinatorLayout with an AppBar:
1107     * Set the title of the AppBar to the title of the activity context.
1108     */
1109    private void setActiveToolbar(View view, BridgeContext context, SessionParams params) {
1110        View coordinatorLayout = findChildView(view, DesignLibUtil.CN_COORDINATOR_LAYOUT);
1111        if (coordinatorLayout == null) {
1112            return;
1113        }
1114        View appBar = findChildView(coordinatorLayout, DesignLibUtil.CN_APPBAR_LAYOUT);
1115        if (appBar == null) {
1116            return;
1117        }
1118        ViewGroup collapsingToolbar =
1119                (ViewGroup) findChildView(appBar, DesignLibUtil.CN_COLLAPSING_TOOLBAR_LAYOUT);
1120        if (collapsingToolbar == null) {
1121            return;
1122        }
1123        if (!hasToolbar(collapsingToolbar)) {
1124            return;
1125        }
1126        RenderResources res = context.getRenderResources();
1127        String title = params.getAppLabel();
1128        ResourceValue titleValue = res.findResValue(title, false);
1129        if (titleValue != null && titleValue.getValue() != null) {
1130            title = titleValue.getValue();
1131        }
1132        DesignLibUtil.setTitle(collapsingToolbar, title);
1133    }
1134
1135    private View findChildView(View view, String className) {
1136        if (!(view instanceof ViewGroup)) {
1137            return null;
1138        }
1139        ViewGroup group = (ViewGroup) view;
1140        for (int i = 0; i < group.getChildCount(); i++) {
1141            if (isInstanceOf(group.getChildAt(i), className)) {
1142                return group.getChildAt(i);
1143            }
1144        }
1145        return null;
1146    }
1147
1148    private boolean hasToolbar(View collapsingToolbar) {
1149        if (!(collapsingToolbar instanceof ViewGroup)) {
1150            return false;
1151        }
1152        ViewGroup group = (ViewGroup) collapsingToolbar;
1153        for (int i = 0; i < group.getChildCount(); i++) {
1154            if (isInstanceOf(group.getChildAt(i), DesignLibUtil.CN_TOOLBAR)) {
1155                return true;
1156            }
1157        }
1158        return false;
1159    }
1160
1161    /**
1162     * Set the scroll position on all the components with the "scrollX" and "scrollY" attribute. If
1163     * the component supports nested scrolling attempt that first, then use the unconsumed scroll
1164     * part to scroll the content in the component.
1165     */
1166    private static void handleScrolling(BridgeContext context, View view) {
1167        int scrollPosX = context.getScrollXPos(view);
1168        int scrollPosY = context.getScrollYPos(view);
1169        if (scrollPosX != 0 || scrollPosY != 0) {
1170            if (view.isNestedScrollingEnabled()) {
1171                int[] consumed = new int[2];
1172                int axis = scrollPosX != 0 ? View.SCROLL_AXIS_HORIZONTAL : 0;
1173                axis |= scrollPosY != 0 ? View.SCROLL_AXIS_VERTICAL : 0;
1174                if (view.startNestedScroll(axis)) {
1175                    view.dispatchNestedPreScroll(scrollPosX, scrollPosY, consumed, null);
1176                    view.dispatchNestedScroll(consumed[0], consumed[1], scrollPosX, scrollPosY,
1177                            null);
1178                    view.stopNestedScroll();
1179                    scrollPosX -= consumed[0];
1180                    scrollPosY -= consumed[1];
1181                }
1182            }
1183            if (scrollPosX != 0 || scrollPosY != 0) {
1184                view.scrollTo(scrollPosX, scrollPosY);
1185            }
1186        }
1187
1188        if (!(view instanceof ViewGroup)) {
1189            return;
1190        }
1191        ViewGroup group = (ViewGroup) view;
1192        for (int i = 0; i < group.getChildCount(); i++) {
1193            View child = group.getChildAt(i);
1194            handleScrolling(context, child);
1195        }
1196    }
1197
1198    /**
1199     * Sets up a {@link TabHost} object.
1200     * @param tabHost the TabHost to setup.
1201     * @param layoutlibCallback The project callback object to access the project R class.
1202     * @throws PostInflateException if TabHost is missing the required ids for TabHost
1203     */
1204    private void setupTabHost(TabHost tabHost, LayoutlibCallback layoutlibCallback)
1205            throws PostInflateException {
1206        // look for the TabWidget, and the FrameLayout. They have their own specific names
1207        View v = tabHost.findViewById(android.R.id.tabs);
1208
1209        if (v == null) {
1210            throw new PostInflateException(
1211                    "TabHost requires a TabWidget with id \"android:id/tabs\".\n");
1212        }
1213
1214        if (!(v instanceof TabWidget)) {
1215            throw new PostInflateException(String.format(
1216                    "TabHost requires a TabWidget with id \"android:id/tabs\".\n" +
1217                    "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName()));
1218        }
1219
1220        v = tabHost.findViewById(android.R.id.tabcontent);
1221
1222        if (v == null) {
1223            // TODO: see if we can fake tabs even without the FrameLayout (same below when the frameLayout is empty)
1224            //noinspection SpellCheckingInspection
1225            throw new PostInflateException(
1226                    "TabHost requires a FrameLayout with id \"android:id/tabcontent\".");
1227        }
1228
1229        if (!(v instanceof FrameLayout)) {
1230            //noinspection SpellCheckingInspection
1231            throw new PostInflateException(String.format(
1232                    "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" +
1233                    "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName()));
1234        }
1235
1236        FrameLayout content = (FrameLayout)v;
1237
1238        // now process the content of the frameLayout and dynamically create tabs for it.
1239        final int count = content.getChildCount();
1240
1241        // this must be called before addTab() so that the TabHost searches its TabWidget
1242        // and FrameLayout.
1243        tabHost.setup();
1244
1245        if (count == 0) {
1246            // Create a dummy child to get a single tab
1247            TabSpec spec = tabHost.newTabSpec("tag")
1248                    .setIndicator("Tab Label", tabHost.getResources()
1249                            .getDrawable(android.R.drawable.ic_menu_info_details, null))
1250                    .setContent(tag -> new LinearLayout(getContext()));
1251            tabHost.addTab(spec);
1252        } else {
1253            // for each child of the frameLayout, add a new TabSpec
1254            for (int i = 0 ; i < count ; i++) {
1255                View child = content.getChildAt(i);
1256                String tabSpec = String.format("tab_spec%d", i+1);
1257                @SuppressWarnings("ConstantConditions")  // child cannot be null.
1258                int id = child.getId();
1259                @SuppressWarnings("deprecation")
1260                Pair<ResourceType, String> resource = layoutlibCallback.resolveResourceId(id);
1261                String name;
1262                if (resource != null) {
1263                    name = resource.getSecond();
1264                } else {
1265                    name = String.format("Tab %d", i+1); // default name if id is unresolved.
1266                }
1267                tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id));
1268            }
1269        }
1270    }
1271
1272    /**
1273     * Visits a {@link View} and its children and generate a {@link ViewInfo} containing the
1274     * bounds of all the views.
1275     *
1276     * @param view the root View
1277     * @param hOffset horizontal offset for the view bounds.
1278     * @param vOffset vertical offset for the view bounds.
1279     * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object.
1280     * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the
1281     *                       content frame.
1282     *
1283     * @return {@code ViewInfo} containing the bounds of the view and it children otherwise.
1284     */
1285    private ViewInfo visit(View view, int hOffset, int vOffset, boolean setExtendedInfo,
1286            boolean isContentFrame) {
1287        ViewInfo result = createViewInfo(view, hOffset, vOffset, setExtendedInfo, isContentFrame);
1288
1289        if (view instanceof ViewGroup) {
1290            ViewGroup group = ((ViewGroup) view);
1291            result.setChildren(visitAllChildren(group, isContentFrame ? 0 : hOffset,
1292                    isContentFrame ? 0 : vOffset,
1293                    setExtendedInfo, isContentFrame));
1294        }
1295        return result;
1296    }
1297
1298    /**
1299     * Visits all the children of a given ViewGroup and generates a list of {@link ViewInfo}
1300     * containing the bounds of all the views. It also initializes the {@link #mViewInfoList} with
1301     * the children of the {@code mContentRoot}.
1302     *
1303     * @param viewGroup the root View
1304     * @param hOffset horizontal offset from the top for the content view frame.
1305     * @param vOffset vertical offset from the top for the content view frame.
1306     * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object.
1307     * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the
1308     *                       content frame. {@code false} if the {@code ViewInfo} to be created is
1309     *                       part of the system decor.
1310     */
1311    private List<ViewInfo> visitAllChildren(ViewGroup viewGroup, int hOffset, int vOffset,
1312            boolean setExtendedInfo, boolean isContentFrame) {
1313        if (viewGroup == null) {
1314            return null;
1315        }
1316
1317        if (!isContentFrame) {
1318            vOffset += viewGroup.getTop();
1319            hOffset += viewGroup.getLeft();
1320        }
1321
1322        int childCount = viewGroup.getChildCount();
1323        if (viewGroup == mContentRoot) {
1324            List<ViewInfo> childrenWithoutOffset = new ArrayList<>(childCount);
1325            List<ViewInfo> childrenWithOffset = new ArrayList<>(childCount);
1326            for (int i = 0; i < childCount; i++) {
1327                ViewInfo[] childViewInfo =
1328                        visitContentRoot(viewGroup.getChildAt(i), hOffset, vOffset,
1329                        setExtendedInfo);
1330                childrenWithoutOffset.add(childViewInfo[0]);
1331                childrenWithOffset.add(childViewInfo[1]);
1332            }
1333            mViewInfoList = childrenWithOffset;
1334            return childrenWithoutOffset;
1335        } else {
1336            List<ViewInfo> children = new ArrayList<>(childCount);
1337            for (int i = 0; i < childCount; i++) {
1338                children.add(visit(viewGroup.getChildAt(i), hOffset, vOffset, setExtendedInfo,
1339                        isContentFrame));
1340            }
1341            return children;
1342        }
1343    }
1344
1345    /**
1346     * Visits the children of {@link #mContentRoot} and generates {@link ViewInfo} containing the
1347     * bounds of all the views. It returns two {@code ViewInfo} objects with the same children,
1348     * one with the {@code offset} and other without the {@code offset}. The offset is needed to
1349     * get the right bounds if the {@code ViewInfo} hierarchy is accessed from
1350     * {@code mViewInfoList}. When the hierarchy is accessed via {@code mSystemViewInfoList}, the
1351     * offset is not needed.
1352     *
1353     * @return an array of length two, with ViewInfo at index 0 is without offset and ViewInfo at
1354     *         index 1 is with the offset.
1355     */
1356    @NonNull
1357    private ViewInfo[] visitContentRoot(View view, int hOffset, int vOffset,
1358            boolean setExtendedInfo) {
1359        ViewInfo[] result = new ViewInfo[2];
1360        if (view == null) {
1361            return result;
1362        }
1363
1364        result[0] = createViewInfo(view, 0, 0, setExtendedInfo, true);
1365        result[1] = createViewInfo(view, hOffset, vOffset, setExtendedInfo, true);
1366        if (view instanceof ViewGroup) {
1367            List<ViewInfo> children =
1368                    visitAllChildren((ViewGroup) view, 0, 0, setExtendedInfo, true);
1369            result[0].setChildren(children);
1370            result[1].setChildren(children);
1371        }
1372        return result;
1373    }
1374
1375    /**
1376     * Creates a {@link ViewInfo} for the view. The {@code ViewInfo} corresponding to the children
1377     * of the {@code view} are not created. Consequently, the children of {@code ViewInfo} is not
1378     * set.
1379     * @param hOffset horizontal offset for the view bounds. Used only if view is part of the
1380     * content frame.
1381     * @param vOffset vertial an offset for the view bounds. Used only if view is part of the
1382     * content frame.
1383     */
1384    private ViewInfo createViewInfo(View view, int hOffset, int vOffset, boolean setExtendedInfo,
1385            boolean isContentFrame) {
1386        if (view == null) {
1387            return null;
1388        }
1389
1390        ViewParent parent = view.getParent();
1391        ViewInfo result;
1392        if (isContentFrame) {
1393            // Account for parent scroll values when calculating the bounding box
1394            int scrollX = parent != null ? ((View)parent).getScrollX() : 0;
1395            int scrollY = parent != null ? ((View)parent).getScrollY() : 0;
1396
1397            // The view is part of the layout added by the user. Hence,
1398            // the ViewCookie may be obtained only through the Context.
1399            result = new ViewInfo(view.getClass().getName(),
1400                    getContext().getViewKey(view), -scrollX + view.getLeft() + hOffset,
1401                    -scrollY + view.getTop() + vOffset, -scrollX + view.getRight() + hOffset,
1402                    -scrollY + view.getBottom() + vOffset,
1403                    view, view.getLayoutParams());
1404        } else {
1405            // We are part of the system decor.
1406            SystemViewInfo r = new SystemViewInfo(view.getClass().getName(),
1407                    getViewKey(view),
1408                    view.getLeft(), view.getTop(), view.getRight(),
1409                    view.getBottom(), view, view.getLayoutParams());
1410            result = r;
1411            // We currently mark three kinds of views:
1412            // 1. Menus in the Action Bar
1413            // 2. Menus in the Overflow popup.
1414            // 3. The overflow popup button.
1415            if (view instanceof ListMenuItemView) {
1416                // Mark 2.
1417                // All menus in the popup are of type ListMenuItemView.
1418                r.setViewType(ViewType.ACTION_BAR_OVERFLOW_MENU);
1419            } else {
1420                // Mark 3.
1421                ViewGroup.LayoutParams lp = view.getLayoutParams();
1422                if (lp instanceof ActionMenuView.LayoutParams &&
1423                        ((ActionMenuView.LayoutParams) lp).isOverflowButton) {
1424                    r.setViewType(ViewType.ACTION_BAR_OVERFLOW);
1425                } else {
1426                    // Mark 1.
1427                    // A view is a menu in the Action Bar is it is not the overflow button and of
1428                    // its parent is of type ActionMenuView. We can also check if the view is
1429                    // instanceof ActionMenuItemView but that will fail for menus using
1430                    // actionProviderClass.
1431                    while (parent != mViewRoot && parent instanceof ViewGroup) {
1432                        if (parent instanceof ActionMenuView) {
1433                            r.setViewType(ViewType.ACTION_BAR_MENU);
1434                            break;
1435                        }
1436                        parent = parent.getParent();
1437                    }
1438                }
1439            }
1440        }
1441
1442        if (setExtendedInfo) {
1443            MarginLayoutParams marginParams = null;
1444            LayoutParams params = view.getLayoutParams();
1445            if (params instanceof MarginLayoutParams) {
1446                marginParams = (MarginLayoutParams) params;
1447            }
1448            result.setExtendedInfo(view.getBaseline(),
1449                    marginParams != null ? marginParams.leftMargin : 0,
1450                    marginParams != null ? marginParams.topMargin : 0,
1451                    marginParams != null ? marginParams.rightMargin : 0,
1452                    marginParams != null ? marginParams.bottomMargin : 0);
1453        }
1454
1455        return result;
1456    }
1457
1458    /* (non-Javadoc)
1459     * The cookie for menu items are stored in menu item and not in the map from View stored in
1460     * BridgeContext.
1461     */
1462    @Nullable
1463    private Object getViewKey(View view) {
1464        BridgeContext context = getContext();
1465        if (!(view instanceof MenuView.ItemView)) {
1466            return context.getViewKey(view);
1467        }
1468        MenuItemImpl menuItem;
1469        if (view instanceof ActionMenuItemView) {
1470            menuItem = ((ActionMenuItemView) view).getItemData();
1471        } else if (view instanceof ListMenuItemView) {
1472            menuItem = ((ListMenuItemView) view).getItemData();
1473        } else if (view instanceof IconMenuItemView) {
1474            menuItem = ((IconMenuItemView) view).getItemData();
1475        } else {
1476            menuItem = null;
1477        }
1478        if (menuItem instanceof BridgeMenuItemImpl) {
1479            return ((BridgeMenuItemImpl) menuItem).getViewCookie();
1480        }
1481
1482        return null;
1483    }
1484
1485    public void invalidateRenderingSize() {
1486        mMeasuredScreenWidth = mMeasuredScreenHeight = -1;
1487    }
1488
1489    public BufferedImage getImage() {
1490        return mImage;
1491    }
1492
1493    public boolean isAlphaChannelImage() {
1494        return mIsAlphaChannelImage;
1495    }
1496
1497    public List<ViewInfo> getViewInfos() {
1498        return mViewInfoList;
1499    }
1500
1501    public List<ViewInfo> getSystemViewInfos() {
1502        return mSystemViewInfoList;
1503    }
1504
1505    public Map<Object, PropertiesMap> getDefaultProperties() {
1506        return getContext().getDefaultProperties();
1507    }
1508
1509    public void setScene(RenderSession session) {
1510        mScene = session;
1511    }
1512
1513    public RenderSession getSession() {
1514        return mScene;
1515    }
1516
1517    public void dispose() {
1518        boolean createdLooper = false;
1519        if (Looper.myLooper() == null) {
1520            // Detaching the root view from the window will try to stop any running animations.
1521            // The stop method checks that it can run in the looper so, if there is no current
1522            // looper, we create a temporary one to complete the shutdown.
1523            Bridge.prepareThread();
1524            createdLooper = true;
1525        }
1526        AttachInfo_Accessor.detachFromWindow(mViewRoot);
1527        if (mCanvas != null) {
1528            mCanvas.release();
1529            mCanvas = null;
1530        }
1531        if (mViewInfoList != null) {
1532            mViewInfoList.clear();
1533        }
1534        if (mSystemViewInfoList != null) {
1535            mSystemViewInfoList.clear();
1536        }
1537        mImage = null;
1538        mViewRoot = null;
1539        mContentRoot = null;
1540
1541        if (createdLooper) {
1542            Bridge.cleanupThread();
1543            Choreographer_Delegate.dispose();
1544        }
1545    }
1546}
1547