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