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