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