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