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