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