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