RenderSessionImpl.java revision b863a416cfe16c71fa3165a5550380288906b9bf
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.LayoutLog;
22import com.android.ide.common.rendering.api.LayoutlibCallback;
23import com.android.ide.common.rendering.api.RenderResources;
24import com.android.ide.common.rendering.api.RenderSession;
25import com.android.ide.common.rendering.api.ResourceReference;
26import com.android.ide.common.rendering.api.ResourceValue;
27import com.android.ide.common.rendering.api.Result;
28import com.android.ide.common.rendering.api.SessionParams;
29import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
30import com.android.ide.common.rendering.api.ViewInfo;
31import com.android.ide.common.rendering.api.ViewType;
32import com.android.internal.view.menu.ActionMenuItemView;
33import com.android.internal.view.menu.BridgeMenuItemImpl;
34import com.android.internal.view.menu.IconMenuItemView;
35import com.android.internal.view.menu.ListMenuItemView;
36import com.android.internal.view.menu.MenuItemImpl;
37import com.android.internal.view.menu.MenuView;
38import com.android.layoutlib.bridge.Bridge;
39import com.android.layoutlib.bridge.android.BridgeContext;
40import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
41import com.android.layoutlib.bridge.android.RenderParamsFlags;
42import com.android.layoutlib.bridge.android.graphics.NopCanvas;
43import com.android.layoutlib.bridge.android.support.DesignLibUtil;
44import com.android.layoutlib.bridge.android.support.SupportPreferencesUtil;
45import com.android.layoutlib.bridge.impl.binding.FakeAdapter;
46import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter;
47import com.android.resources.ResourceType;
48import com.android.tools.layoutlib.java.System_Delegate;
49import com.android.util.Pair;
50import com.android.util.PropertiesMap;
51
52import android.annotation.NonNull;
53import android.annotation.Nullable;
54import android.app.Fragment_Delegate;
55import android.graphics.Bitmap;
56import android.graphics.Bitmap_Delegate;
57import android.graphics.Canvas;
58import android.os.Looper;
59import android.preference.Preference_Delegate;
60import android.view.AttachInfo_Accessor;
61import android.view.BridgeInflater;
62import android.view.Choreographer_Delegate;
63import android.view.View;
64import android.view.View.MeasureSpec;
65import android.view.ViewGroup;
66import android.view.ViewGroup.LayoutParams;
67import android.view.ViewGroup.MarginLayoutParams;
68import android.view.ViewParent;
69import android.widget.AbsListView;
70import android.widget.AbsSpinner;
71import android.widget.ActionMenuView;
72import android.widget.AdapterView;
73import android.widget.ExpandableListView;
74import android.widget.FrameLayout;
75import android.widget.LinearLayout;
76import android.widget.ListView;
77import android.widget.QuickContactBadge;
78import android.widget.TabHost;
79import android.widget.TabHost.TabSpec;
80import android.widget.TabWidget;
81
82import java.awt.AlphaComposite;
83import java.awt.Color;
84import java.awt.Graphics2D;
85import java.awt.image.BufferedImage;
86import java.util.ArrayList;
87import java.util.List;
88import java.util.Map;
89
90import static com.android.ide.common.rendering.api.Result.Status.ERROR_INFLATION;
91import static com.android.ide.common.rendering.api.Result.Status.ERROR_NOT_INFLATED;
92import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
93import static com.android.ide.common.rendering.api.Result.Status.SUCCESS;
94import static com.android.layoutlib.bridge.util.ReflectionUtils.isInstanceOf;
95
96/**
97 * Class implementing the render session.
98 * <p/>
99 * A session is a stateful representation of a layout file. It is initialized with data coming
100 * through the {@link Bridge} API to inflate the layout. Further actions and rendering can then
101 * be done on the layout.
102 */
103public class RenderSessionImpl extends RenderAction<SessionParams> {
104
105    private static final Canvas NOP_CANVAS = new NopCanvas();
106
107    // scene state
108    private RenderSession mScene;
109    private BridgeXmlBlockParser mBlockParser;
110    private BridgeInflater mInflater;
111    private ViewGroup mViewRoot;
112    private FrameLayout mContentRoot;
113    private Canvas mCanvas;
114    private int mMeasuredScreenWidth = -1;
115    private int mMeasuredScreenHeight = -1;
116    private boolean mIsAlphaChannelImage;
117    /** If >= 0, a frame will be executed */
118    private long mElapsedFrameTimeNanos = -1;
119    /** True if one frame has been already executed to start the animations */
120    private boolean mFirstFrameExecuted = false;
121
122    // information being returned through the API
123    private BufferedImage mImage;
124    private List<ViewInfo> mViewInfoList;
125    private List<ViewInfo> mSystemViewInfoList;
126    private Layout.Builder mLayoutBuilder;
127    private boolean mNewRenderSize;
128
129    private static final class PostInflateException extends Exception {
130        private static final long serialVersionUID = 1L;
131
132        private PostInflateException(String message) {
133            super(message);
134        }
135    }
136
137    /**
138     * Creates a layout scene with all the information coming from the layout bridge API.
139     * <p>
140     * This <b>must</b> be followed by a call to {@link RenderSessionImpl#init(long)},
141     * which act as a
142     * call to {@link RenderSessionImpl#acquire(long)}
143     *
144     * @see Bridge#createSession(SessionParams)
145     */
146    public RenderSessionImpl(SessionParams params) {
147        super(new SessionParams(params));
148    }
149
150    /**
151     * Initializes and acquires the scene, creating various Android objects such as context,
152     * inflater, and parser.
153     *
154     * @param timeout the time to wait if another rendering is happening.
155     *
156     * @return whether the scene was prepared
157     *
158     * @see #acquire(long)
159     * @see #release()
160     */
161    @Override
162    public Result init(long timeout) {
163        Result result = super.init(timeout);
164        if (!result.isSuccess()) {
165            return result;
166        }
167
168        SessionParams params = getParams();
169        BridgeContext context = getContext();
170
171        // use default of true in case it's not found to use alpha by default
172        mIsAlphaChannelImage = ResourceHelper.getBooleanThemeValue(params.getResources(),
173                "windowIsFloating", true, true);
174
175        mLayoutBuilder = new Layout.Builder(params, context);
176
177        // build the inflater and parser.
178        mInflater = new BridgeInflater(context, params.getLayoutlibCallback());
179        context.setBridgeInflater(mInflater);
180
181        mBlockParser = new BridgeXmlBlockParser(params.getLayoutDescription(), context, false);
182
183        return SUCCESS.createResult();
184    }
185
186    /**
187     * Measures the the current layout if needed (see {@link #invalidateRenderingSize}).
188     */
189    private void measureLayout(@NonNull SessionParams params) {
190        // only do the screen measure when needed.
191        if (mMeasuredScreenWidth != -1) {
192            return;
193        }
194
195        RenderingMode renderingMode = params.getRenderingMode();
196        HardwareConfig hardwareConfig = params.getHardwareConfig();
197
198        mNewRenderSize = true;
199        mMeasuredScreenWidth = hardwareConfig.getScreenWidth();
200        mMeasuredScreenHeight = hardwareConfig.getScreenHeight();
201
202        if (renderingMode != RenderingMode.NORMAL) {
203            int widthMeasureSpecMode = renderingMode.isHorizExpand() ?
204                    MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
205                    : MeasureSpec.EXACTLY;
206            int heightMeasureSpecMode = renderingMode.isVertExpand() ?
207                    MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
208                    : MeasureSpec.EXACTLY;
209
210            // We used to compare the measured size of the content to the screen size but
211            // this does not work anymore due to the 2 following issues:
212            // - If the content is in a decor (system bar, title/action bar), the root view
213            //   will not resize even with the UNSPECIFIED because of the embedded layout.
214            // - If there is no decor, but a dialog frame, then the dialog padding prevents
215            //   comparing the size of the content to the screen frame (as it would not
216            //   take into account the dialog padding).
217
218            // The solution is to first get the content size in a normal rendering, inside
219            // the decor or the dialog padding.
220            // Then measure only the content with UNSPECIFIED to see the size difference
221            // and apply this to the screen size.
222
223            View measuredView = mContentRoot.getChildAt(0);
224
225            // first measure the full layout, with EXACTLY to get the size of the
226            // content as it is inside the decor/dialog
227            @SuppressWarnings("deprecation")
228            Pair<Integer, Integer> exactMeasure = measureView(
229                    mViewRoot, measuredView,
230                    mMeasuredScreenWidth, MeasureSpec.EXACTLY,
231                    mMeasuredScreenHeight, MeasureSpec.EXACTLY);
232
233            // now measure the content only using UNSPECIFIED (where applicable, based on
234            // the rendering mode). This will give us the size the content needs.
235            @SuppressWarnings("deprecation")
236            Pair<Integer, Integer> result = measureView(
237                    mContentRoot, mContentRoot.getChildAt(0),
238                    mMeasuredScreenWidth, widthMeasureSpecMode,
239                    mMeasuredScreenHeight, heightMeasureSpecMode);
240
241            // If measuredView is not null, exactMeasure nor result will be null.
242            assert exactMeasure != null;
243            assert result != null;
244
245            // now look at the difference and add what is needed.
246            if (renderingMode.isHorizExpand()) {
247                int measuredWidth = exactMeasure.getFirst();
248                int neededWidth = result.getFirst();
249                if (neededWidth > measuredWidth) {
250                    mMeasuredScreenWidth += neededWidth - measuredWidth;
251                }
252                if (mMeasuredScreenWidth < measuredWidth) {
253                    // If the screen width is less than the exact measured width,
254                    // expand to match.
255                    mMeasuredScreenWidth = measuredWidth;
256                }
257            }
258
259            if (renderingMode.isVertExpand()) {
260                int measuredHeight = exactMeasure.getSecond();
261                int neededHeight = result.getSecond();
262                if (neededHeight > measuredHeight) {
263                    mMeasuredScreenHeight += neededHeight - measuredHeight;
264                }
265                if (mMeasuredScreenHeight < measuredHeight) {
266                    // If the screen height is less than the exact measured height,
267                    // expand to match.
268                    mMeasuredScreenHeight = measuredHeight;
269                }
270            }
271        }
272    }
273
274    /**
275     * Inflates the layout.
276     * <p>
277     * {@link #acquire(long)} must have been called before this.
278     *
279     * @throws IllegalStateException if the current context is different than the one owned by
280     *      the scene, or if {@link #init(long)} was not called.
281     */
282    public Result inflate() {
283        checkLock();
284
285        try {
286            mViewRoot = new Layout(mLayoutBuilder);
287            mLayoutBuilder = null;  // Done with the builder.
288            mContentRoot = ((Layout) mViewRoot).getContentRoot();
289            SessionParams params = getParams();
290            BridgeContext context = getContext();
291
292            if (Bridge.isLocaleRtl(params.getLocale())) {
293                if (!params.isRtlSupported()) {
294                    Bridge.getLog().warning(LayoutLog.TAG_RTL_NOT_ENABLED,
295                            "You are using a right-to-left " +
296                                    "(RTL) locale but RTL is not enabled", null);
297                } else if (params.getSimulatedPlatformVersion() < 17) {
298                    // This will render ok because we are using the latest layoutlib but at least
299                    // warn the user that this might fail in a real device.
300                    Bridge.getLog().warning(LayoutLog.TAG_RTL_NOT_SUPPORTED, "You are using a " +
301                            "right-to-left " +
302                            "(RTL) locale but RTL is not supported for API level < 17", null);
303                }
304            }
305
306            // Sets the project callback (custom view loader) to the fragment delegate so that
307            // it can instantiate the custom Fragment.
308            Fragment_Delegate.setLayoutlibCallback(params.getLayoutlibCallback());
309
310            String rootTag = params.getFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG);
311            boolean isPreference = "PreferenceScreen".equals(rootTag);
312            View view;
313            if (isPreference) {
314                // First try to use the support library inflater. If something fails, fallback
315                // to the system preference inflater.
316                view = SupportPreferencesUtil.inflatePreference(getContext(), mBlockParser,
317                        mContentRoot);
318                if (view == null) {
319                    view = Preference_Delegate.inflatePreference(getContext(), mBlockParser,
320                            mContentRoot);
321                }
322            } else {
323                view = mInflater.inflate(mBlockParser, mContentRoot);
324            }
325
326            // done with the parser, pop it.
327            context.popParser();
328
329            Fragment_Delegate.setLayoutlibCallback(null);
330
331            // set the AttachInfo on the root view.
332            AttachInfo_Accessor.setAttachInfo(mViewRoot);
333
334            // post-inflate process. For now this supports TabHost/TabWidget
335            postInflateProcess(view, params.getLayoutlibCallback(), isPreference ? view : null);
336            mInflater.onDoneInflation();
337
338            setActiveToolbar(view, context, params);
339
340            measureLayout(params);
341            measureView(mViewRoot, null /*measuredView*/,
342                    mMeasuredScreenWidth, MeasureSpec.EXACTLY,
343                    mMeasuredScreenHeight, MeasureSpec.EXACTLY);
344            mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight);
345            mSystemViewInfoList =
346                    visitAllChildren(mViewRoot, 0, 0, params.getExtendedViewInfoMode(),
347                    false);
348
349            return SUCCESS.createResult();
350        } catch (PostInflateException e) {
351            return ERROR_INFLATION.createResult(e.getMessage(), e);
352        } catch (Throwable e) {
353            // get the real cause of the exception.
354            Throwable t = e;
355            while (t.getCause() != null) {
356                t = t.getCause();
357            }
358
359            return ERROR_INFLATION.createResult(t.getMessage(), t);
360        }
361    }
362
363    /**
364     * Sets the time for which the next frame will be selected. The time is the elapsed time from
365     * the current system nanos time. You
366     */
367    public void setElapsedFrameTimeNanos(long nanos) {
368        mElapsedFrameTimeNanos = nanos;
369    }
370
371    /**
372     * Runs a layout pass for the given view root
373     */
374    private static void doLayout(@NonNull BridgeContext context, @NonNull ViewGroup viewRoot,
375            int width, int height) {
376        // measure again with the size we need
377        // This must always be done before the call to layout
378        measureView(viewRoot, null /*measuredView*/,
379                width, MeasureSpec.EXACTLY,
380                height, MeasureSpec.EXACTLY);
381
382        // now do the layout.
383        viewRoot.layout(0, 0, width, height);
384        handleScrolling(context, viewRoot);
385    }
386
387    /**
388     * Renders the given view hierarchy to the passed canvas and returns the result of the render
389     * operation.
390     * @param canvas an optional canvas to render the views to. If null, only the measure and
391     * layout steps will be executed.
392     */
393    private static Result renderAndBuildResult(@NonNull ViewGroup viewRoot, @Nullable Canvas canvas) {
394        if (canvas == null) {
395            return SUCCESS.createResult();
396        }
397
398        AttachInfo_Accessor.dispatchOnPreDraw(viewRoot);
399        viewRoot.draw(canvas);
400
401        return SUCCESS.createResult();
402    }
403
404    /**
405     * Renders the scene.
406     * <p>
407     * {@link #acquire(long)} must have been called before this.
408     *
409     * @param freshRender whether the render is a new one and should erase the existing bitmap (in
410     *      the case where bitmaps are reused). This is typically needed when not playing
411     *      animations.)
412     *
413     * @throws IllegalStateException if the current context is different than the one owned by
414     *      the scene, or if {@link #acquire(long)} was not called.
415     *
416     * @see SessionParams#getRenderingMode()
417     * @see RenderSession#render(long)
418     */
419    public Result render(boolean freshRender) {
420        return renderAndBuildResult(freshRender, false);
421    }
422
423    /**
424     * Measures the layout
425     * <p>
426     * {@link #acquire(long)} must have been called before this.
427     *
428     * @throws IllegalStateException if the current context is different than the one owned by
429     *      the scene, or if {@link #acquire(long)} was not called.
430     *
431     * @see SessionParams#getRenderingMode()
432     * @see RenderSession#render(long)
433     */
434    public Result measure() {
435        return renderAndBuildResult(false, true);
436    }
437
438    /**
439     * Renders the scene.
440     * <p>
441     * {@link #acquire(long)} must have been called before this.
442     *
443     * @param freshRender whether the render is a new one and should erase the existing bitmap (in
444     *      the case where bitmaps are reused). This is typically needed when not playing
445     *      animations.)
446     *
447     * @throws IllegalStateException if the current context is different than the one owned by
448     *      the scene, or if {@link #acquire(long)} was not called.
449     *
450     * @see SessionParams#getRenderingMode()
451     * @see RenderSession#render(long)
452     */
453    private Result renderAndBuildResult(boolean freshRender, boolean onlyMeasure) {
454        checkLock();
455
456        SessionParams params = getParams();
457
458        try {
459            if (mViewRoot == null) {
460                return ERROR_NOT_INFLATED.createResult();
461            }
462
463            measureLayout(params);
464
465            HardwareConfig hardwareConfig = params.getHardwareConfig();
466            Result renderResult = SUCCESS.createResult();
467            if (onlyMeasure) {
468                // delete the canvas and image to reset them on the next full rendering
469                mImage = null;
470                mCanvas = null;
471                doLayout(getContext(), mViewRoot, mMeasuredScreenWidth, mMeasuredScreenHeight);
472            } else {
473                // draw the views
474                // create the BufferedImage into which the layout will be rendered.
475                boolean newImage = false;
476
477                // When disableBitmapCaching is true, we do not reuse mImage and
478                // we create a new one in every render.
479                // This is useful when mImage is just a wrapper of Graphics2D so
480                // it doesn't get cached.
481                boolean disableBitmapCaching = Boolean.TRUE.equals(params.getFlag(
482                    RenderParamsFlags.FLAG_KEY_DISABLE_BITMAP_CACHING));
483                if (mNewRenderSize || mCanvas == null || disableBitmapCaching) {
484                    mNewRenderSize = false;
485                    if (params.getImageFactory() != null) {
486                        mImage = params.getImageFactory().getImage(
487                                mMeasuredScreenWidth,
488                                mMeasuredScreenHeight);
489                    } else {
490                        mImage = new BufferedImage(
491                                mMeasuredScreenWidth,
492                                mMeasuredScreenHeight,
493                                BufferedImage.TYPE_INT_ARGB);
494                        newImage = true;
495                    }
496
497                    if (params.isBgColorOverridden()) {
498                        // since we override the content, it's the same as if it was a new image.
499                        newImage = true;
500                        Graphics2D gc = mImage.createGraphics();
501                        gc.setColor(new Color(params.getOverrideBgColor(), true));
502                        gc.setComposite(AlphaComposite.Src);
503                        gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight);
504                        gc.dispose();
505                    }
506
507                    // create an Android bitmap around the BufferedImage
508                    Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage,
509                            true /*isMutable*/, hardwareConfig.getDensity());
510
511                    if (mCanvas == null) {
512                        // create a Canvas around the Android bitmap
513                        mCanvas = new Canvas(bitmap);
514                    } else {
515                        mCanvas.setBitmap(bitmap);
516                    }
517                    mCanvas.setDensity(hardwareConfig.getDensity().getDpiValue());
518                }
519
520                if (freshRender && !newImage) {
521                    Graphics2D gc = mImage.createGraphics();
522                    gc.setComposite(AlphaComposite.Src);
523
524                    gc.setColor(new Color(0x00000000, true));
525                    gc.fillRect(0, 0,
526                            mMeasuredScreenWidth, mMeasuredScreenHeight);
527
528                    // done
529                    gc.dispose();
530                }
531
532                doLayout(getContext(), mViewRoot, mMeasuredScreenWidth, mMeasuredScreenHeight);
533                if (mElapsedFrameTimeNanos >= 0) {
534                    long initialTime = System_Delegate.nanoTime();
535                    if (!mFirstFrameExecuted) {
536                        // We need to run an initial draw call to initialize the animations
537                        renderAndBuildResult(mViewRoot, NOP_CANVAS);
538
539                        // The first frame will initialize the animations
540                        Choreographer_Delegate.doFrame(initialTime);
541                        mFirstFrameExecuted = true;
542                    }
543                    // Second frame will move the animations
544                    Choreographer_Delegate.doFrame(initialTime + mElapsedFrameTimeNanos);
545                }
546                renderResult = renderAndBuildResult(mViewRoot, mCanvas);
547            }
548
549            mSystemViewInfoList =
550                    visitAllChildren(mViewRoot, 0, 0, params.getExtendedViewInfoMode(),
551                    false);
552
553            // success!
554            return renderResult;
555        } catch (Throwable e) {
556            // get the real cause of the exception.
557            Throwable t = e;
558            while (t.getCause() != null) {
559                t = t.getCause();
560            }
561
562            return ERROR_UNKNOWN.createResult(t.getMessage(), t);
563        }
564    }
565
566    /**
567     * Executes {@link View#measure(int, int)} on a given view with the given parameters (used
568     * to create measure specs with {@link MeasureSpec#makeMeasureSpec(int, int)}.
569     *
570     * if <var>measuredView</var> is non null, the method returns a {@link Pair} of (width, height)
571     * for the view (using {@link View#getMeasuredWidth()} and {@link View#getMeasuredHeight()}).
572     *
573     * @param viewToMeasure the view on which to execute measure().
574     * @param measuredView if non null, the view to query for its measured width/height.
575     * @param width the width to use in the MeasureSpec.
576     * @param widthMode the MeasureSpec mode to use for the width.
577     * @param height the height to use in the MeasureSpec.
578     * @param heightMode the MeasureSpec mode to use for the height.
579     * @return the measured width/height if measuredView is non-null, null otherwise.
580     */
581    @SuppressWarnings("deprecation")  // For the use of Pair
582    private static Pair<Integer, Integer> measureView(ViewGroup viewToMeasure, View measuredView,
583            int width, int widthMode, int height, int heightMode) {
584        int w_spec = MeasureSpec.makeMeasureSpec(width, widthMode);
585        int h_spec = MeasureSpec.makeMeasureSpec(height, heightMode);
586        viewToMeasure.measure(w_spec, h_spec);
587
588        if (measuredView != null) {
589            return Pair.of(measuredView.getMeasuredWidth(), measuredView.getMeasuredHeight());
590        }
591
592        return null;
593    }
594
595    /**
596     * Post process on a view hierarchy that was just inflated.
597     * <p/>
598     * At the moment this only supports TabHost: If {@link TabHost} is detected, look for the
599     * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically
600     * based on the content of the {@link FrameLayout}.
601     * @param view the root view to process.
602     * @param layoutlibCallback callback to the project.
603     * @param skip the view and it's children are not processed.
604     */
605    @SuppressWarnings("deprecation")  // For the use of Pair
606    private void postInflateProcess(View view, LayoutlibCallback layoutlibCallback, View skip)
607            throws PostInflateException {
608        if (view == skip) {
609            return;
610        }
611        if (view instanceof TabHost) {
612            setupTabHost((TabHost) view, layoutlibCallback);
613        } else if (view instanceof QuickContactBadge) {
614            QuickContactBadge badge = (QuickContactBadge) view;
615            badge.setImageToDefault();
616        } else if (view instanceof AdapterView<?>) {
617            // get the view ID.
618            int id = view.getId();
619
620            BridgeContext context = getContext();
621
622            // get a ResourceReference from the integer ID.
623            ResourceReference listRef = context.resolveId(id);
624
625            if (listRef != null) {
626                SessionParams params = getParams();
627                AdapterBinding binding = params.getAdapterBindings().get(listRef);
628
629                // if there was no adapter binding, trying to get it from the call back.
630                if (binding == null) {
631                    binding = layoutlibCallback.getAdapterBinding(
632                            listRef, context.getViewKey(view), view);
633                }
634
635                if (binding != null) {
636
637                    if (view instanceof AbsListView) {
638                        if ((binding.getFooterCount() > 0 || binding.getHeaderCount() > 0) &&
639                                view instanceof ListView) {
640                            ListView list = (ListView) view;
641
642                            boolean skipCallbackParser = false;
643
644                            int count = binding.getHeaderCount();
645                            for (int i = 0; i < count; i++) {
646                                Pair<View, Boolean> pair = context.inflateView(
647                                        binding.getHeaderAt(i),
648                                        list, false, skipCallbackParser);
649                                if (pair.getFirst() != null) {
650                                    list.addHeaderView(pair.getFirst());
651                                }
652
653                                skipCallbackParser |= pair.getSecond();
654                            }
655
656                            count = binding.getFooterCount();
657                            for (int i = 0; i < count; i++) {
658                                Pair<View, Boolean> pair = context.inflateView(
659                                        binding.getFooterAt(i),
660                                        list, false, skipCallbackParser);
661                                if (pair.getFirst() != null) {
662                                    list.addFooterView(pair.getFirst());
663                                }
664
665                                skipCallbackParser |= pair.getSecond();
666                            }
667                        }
668
669                        if (view instanceof ExpandableListView) {
670                            ((ExpandableListView) view).setAdapter(
671                                    new FakeExpandableAdapter(listRef, binding, layoutlibCallback));
672                        } else {
673                            ((AbsListView) view).setAdapter(
674                                    new FakeAdapter(listRef, binding, layoutlibCallback));
675                        }
676                    } else if (view instanceof AbsSpinner) {
677                        ((AbsSpinner) view).setAdapter(
678                                new FakeAdapter(listRef, binding, layoutlibCallback));
679                    }
680                }
681            }
682        } else if (view instanceof ViewGroup) {
683            mInflater.postInflateProcess(view);
684            ViewGroup group = (ViewGroup) view;
685            final int count = group.getChildCount();
686            for (int c = 0; c < count; c++) {
687                View child = group.getChildAt(c);
688                postInflateProcess(child, layoutlibCallback, skip);
689            }
690        }
691    }
692
693    /**
694     * If the root layout is a CoordinatorLayout with an AppBar:
695     * Set the title of the AppBar to the title of the activity context.
696     */
697    private void setActiveToolbar(View view, BridgeContext context, SessionParams params) {
698        View coordinatorLayout = findChildView(view, DesignLibUtil.CN_COORDINATOR_LAYOUT);
699        if (coordinatorLayout == null) {
700            return;
701        }
702        View appBar = findChildView(coordinatorLayout, DesignLibUtil.CN_APPBAR_LAYOUT);
703        if (appBar == null) {
704            return;
705        }
706        ViewGroup collapsingToolbar =
707                (ViewGroup) findChildView(appBar, DesignLibUtil.CN_COLLAPSING_TOOLBAR_LAYOUT);
708        if (collapsingToolbar == null) {
709            return;
710        }
711        if (!hasToolbar(collapsingToolbar)) {
712            return;
713        }
714        RenderResources res = context.getRenderResources();
715        String title = params.getAppLabel();
716        ResourceValue titleValue = res.findResValue(title, false);
717        if (titleValue != null && titleValue.getValue() != null) {
718            title = titleValue.getValue();
719        }
720        DesignLibUtil.setTitle(collapsingToolbar, title);
721    }
722
723    private View findChildView(View view, String className) {
724        if (!(view instanceof ViewGroup)) {
725            return null;
726        }
727        ViewGroup group = (ViewGroup) view;
728        for (int i = 0; i < group.getChildCount(); i++) {
729            if (isInstanceOf(group.getChildAt(i), className)) {
730                return group.getChildAt(i);
731            }
732        }
733        return null;
734    }
735
736    private boolean hasToolbar(View collapsingToolbar) {
737        if (!(collapsingToolbar instanceof ViewGroup)) {
738            return false;
739        }
740        ViewGroup group = (ViewGroup) collapsingToolbar;
741        for (int i = 0; i < group.getChildCount(); i++) {
742            if (isInstanceOf(group.getChildAt(i), DesignLibUtil.CN_TOOLBAR)) {
743                return true;
744            }
745        }
746        return false;
747    }
748
749    /**
750     * Set the scroll position on all the components with the "scrollX" and "scrollY" attribute. If
751     * the component supports nested scrolling attempt that first, then use the unconsumed scroll
752     * part to scroll the content in the component.
753     */
754    private static void handleScrolling(BridgeContext context, View view) {
755        int scrollPosX = context.getScrollXPos(view);
756        int scrollPosY = context.getScrollYPos(view);
757        if (scrollPosX != 0 || scrollPosY != 0) {
758            if (view.isNestedScrollingEnabled()) {
759                int[] consumed = new int[2];
760                int axis = scrollPosX != 0 ? View.SCROLL_AXIS_HORIZONTAL : 0;
761                axis |= scrollPosY != 0 ? View.SCROLL_AXIS_VERTICAL : 0;
762                if (view.startNestedScroll(axis)) {
763                    view.dispatchNestedPreScroll(scrollPosX, scrollPosY, consumed, null);
764                    view.dispatchNestedScroll(consumed[0], consumed[1], scrollPosX, scrollPosY,
765                            null);
766                    view.stopNestedScroll();
767                    scrollPosX -= consumed[0];
768                    scrollPosY -= consumed[1];
769                }
770            }
771            if (scrollPosX != 0 || scrollPosY != 0) {
772                view.scrollTo(scrollPosX, scrollPosY);
773            }
774        }
775
776        if (!(view instanceof ViewGroup)) {
777            return;
778        }
779        ViewGroup group = (ViewGroup) view;
780        for (int i = 0; i < group.getChildCount(); i++) {
781            View child = group.getChildAt(i);
782            handleScrolling(context, child);
783        }
784    }
785
786    /**
787     * Sets up a {@link TabHost} object.
788     * @param tabHost the TabHost to setup.
789     * @param layoutlibCallback The project callback object to access the project R class.
790     * @throws PostInflateException if TabHost is missing the required ids for TabHost
791     */
792    private void setupTabHost(TabHost tabHost, LayoutlibCallback layoutlibCallback)
793            throws PostInflateException {
794        // look for the TabWidget, and the FrameLayout. They have their own specific names
795        View v = tabHost.findViewById(android.R.id.tabs);
796
797        if (v == null) {
798            throw new PostInflateException(
799                    "TabHost requires a TabWidget with id \"android:id/tabs\".\n");
800        }
801
802        if (!(v instanceof TabWidget)) {
803            throw new PostInflateException(String.format(
804                    "TabHost requires a TabWidget with id \"android:id/tabs\".\n" +
805                    "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName()));
806        }
807
808        v = tabHost.findViewById(android.R.id.tabcontent);
809
810        if (v == null) {
811            // TODO: see if we can fake tabs even without the FrameLayout (same below when the frameLayout is empty)
812            //noinspection SpellCheckingInspection
813            throw new PostInflateException(
814                    "TabHost requires a FrameLayout with id \"android:id/tabcontent\".");
815        }
816
817        if (!(v instanceof FrameLayout)) {
818            //noinspection SpellCheckingInspection
819            throw new PostInflateException(String.format(
820                    "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" +
821                    "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName()));
822        }
823
824        FrameLayout content = (FrameLayout)v;
825
826        // now process the content of the frameLayout and dynamically create tabs for it.
827        final int count = content.getChildCount();
828
829        // this must be called before addTab() so that the TabHost searches its TabWidget
830        // and FrameLayout.
831        tabHost.setup();
832
833        if (count == 0) {
834            // Create a dummy child to get a single tab
835            TabSpec spec = tabHost.newTabSpec("tag")
836                    .setIndicator("Tab Label", tabHost.getResources()
837                            .getDrawable(android.R.drawable.ic_menu_info_details, null))
838                    .setContent(tag -> new LinearLayout(getContext()));
839            tabHost.addTab(spec);
840        } else {
841            // for each child of the frameLayout, add a new TabSpec
842            for (int i = 0 ; i < count ; i++) {
843                View child = content.getChildAt(i);
844                String tabSpec = String.format("tab_spec%d", i+1);
845                @SuppressWarnings("ConstantConditions")  // child cannot be null.
846                int id = child.getId();
847                @SuppressWarnings("deprecation")
848                Pair<ResourceType, String> resource = layoutlibCallback.resolveResourceId(id);
849                String name;
850                if (resource != null) {
851                    name = resource.getSecond();
852                } else {
853                    name = String.format("Tab %d", i+1); // default name if id is unresolved.
854                }
855                tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id));
856            }
857        }
858    }
859
860    /**
861     * Visits a {@link View} and its children and generate a {@link ViewInfo} containing the
862     * bounds of all the views.
863     *
864     * @param view the root View
865     * @param hOffset horizontal offset for the view bounds.
866     * @param vOffset vertical offset for the view bounds.
867     * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object.
868     * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the
869     *                       content frame.
870     *
871     * @return {@code ViewInfo} containing the bounds of the view and it children otherwise.
872     */
873    private ViewInfo visit(View view, int hOffset, int vOffset, boolean setExtendedInfo,
874            boolean isContentFrame) {
875        ViewInfo result = createViewInfo(view, hOffset, vOffset, setExtendedInfo, isContentFrame);
876
877        if (view instanceof ViewGroup) {
878            ViewGroup group = ((ViewGroup) view);
879            result.setChildren(visitAllChildren(group, isContentFrame ? 0 : hOffset,
880                    isContentFrame ? 0 : vOffset,
881                    setExtendedInfo, isContentFrame));
882        }
883        return result;
884    }
885
886    /**
887     * Visits all the children of a given ViewGroup and generates a list of {@link ViewInfo}
888     * containing the bounds of all the views. It also initializes the {@link #mViewInfoList} with
889     * the children of the {@code mContentRoot}.
890     *
891     * @param viewGroup the root View
892     * @param hOffset horizontal offset from the top for the content view frame.
893     * @param vOffset vertical offset from the top for the content view frame.
894     * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object.
895     * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the
896     *                       content frame. {@code false} if the {@code ViewInfo} to be created is
897     *                       part of the system decor.
898     */
899    private List<ViewInfo> visitAllChildren(ViewGroup viewGroup, int hOffset, int vOffset,
900            boolean setExtendedInfo, boolean isContentFrame) {
901        if (viewGroup == null) {
902            return null;
903        }
904
905        if (!isContentFrame) {
906            vOffset += viewGroup.getTop();
907            hOffset += viewGroup.getLeft();
908        }
909
910        int childCount = viewGroup.getChildCount();
911        if (viewGroup == mContentRoot) {
912            List<ViewInfo> childrenWithoutOffset = new ArrayList<>(childCount);
913            List<ViewInfo> childrenWithOffset = new ArrayList<>(childCount);
914            for (int i = 0; i < childCount; i++) {
915                ViewInfo[] childViewInfo =
916                        visitContentRoot(viewGroup.getChildAt(i), hOffset, vOffset,
917                        setExtendedInfo);
918                childrenWithoutOffset.add(childViewInfo[0]);
919                childrenWithOffset.add(childViewInfo[1]);
920            }
921            mViewInfoList = childrenWithOffset;
922            return childrenWithoutOffset;
923        } else {
924            List<ViewInfo> children = new ArrayList<>(childCount);
925            for (int i = 0; i < childCount; i++) {
926                children.add(visit(viewGroup.getChildAt(i), hOffset, vOffset, setExtendedInfo,
927                        isContentFrame));
928            }
929            return children;
930        }
931    }
932
933    /**
934     * Visits the children of {@link #mContentRoot} and generates {@link ViewInfo} containing the
935     * bounds of all the views. It returns two {@code ViewInfo} objects with the same children,
936     * one with the {@code offset} and other without the {@code offset}. The offset is needed to
937     * get the right bounds if the {@code ViewInfo} hierarchy is accessed from
938     * {@code mViewInfoList}. When the hierarchy is accessed via {@code mSystemViewInfoList}, the
939     * offset is not needed.
940     *
941     * @return an array of length two, with ViewInfo at index 0 is without offset and ViewInfo at
942     *         index 1 is with the offset.
943     */
944    @NonNull
945    private ViewInfo[] visitContentRoot(View view, int hOffset, int vOffset,
946            boolean setExtendedInfo) {
947        ViewInfo[] result = new ViewInfo[2];
948        if (view == null) {
949            return result;
950        }
951
952        result[0] = createViewInfo(view, 0, 0, setExtendedInfo, true);
953        result[1] = createViewInfo(view, hOffset, vOffset, setExtendedInfo, true);
954        if (view instanceof ViewGroup) {
955            List<ViewInfo> children =
956                    visitAllChildren((ViewGroup) view, 0, 0, setExtendedInfo, true);
957            result[0].setChildren(children);
958            result[1].setChildren(children);
959        }
960        return result;
961    }
962
963    /**
964     * Creates a {@link ViewInfo} for the view. The {@code ViewInfo} corresponding to the children
965     * of the {@code view} are not created. Consequently, the children of {@code ViewInfo} is not
966     * set.
967     * @param hOffset horizontal offset for the view bounds. Used only if view is part of the
968     * content frame.
969     * @param vOffset vertial an offset for the view bounds. Used only if view is part of the
970     * content frame.
971     */
972    private ViewInfo createViewInfo(View view, int hOffset, int vOffset, boolean setExtendedInfo,
973            boolean isContentFrame) {
974        if (view == null) {
975            return null;
976        }
977
978        ViewParent parent = view.getParent();
979        ViewInfo result;
980        if (isContentFrame) {
981            // Account for parent scroll values when calculating the bounding box
982            int scrollX = parent != null ? ((View)parent).getScrollX() : 0;
983            int scrollY = parent != null ? ((View)parent).getScrollY() : 0;
984
985            // The view is part of the layout added by the user. Hence,
986            // the ViewCookie may be obtained only through the Context.
987            result = new ViewInfo(view.getClass().getName(),
988                    getContext().getViewKey(view), -scrollX + view.getLeft() + hOffset,
989                    -scrollY + view.getTop() + vOffset, -scrollX + view.getRight() + hOffset,
990                    -scrollY + view.getBottom() + vOffset,
991                    view, view.getLayoutParams());
992        } else {
993            // We are part of the system decor.
994            SystemViewInfo r = new SystemViewInfo(view.getClass().getName(),
995                    getViewKey(view),
996                    view.getLeft(), view.getTop(), view.getRight(),
997                    view.getBottom(), view, view.getLayoutParams());
998            result = r;
999            // We currently mark three kinds of views:
1000            // 1. Menus in the Action Bar
1001            // 2. Menus in the Overflow popup.
1002            // 3. The overflow popup button.
1003            if (view instanceof ListMenuItemView) {
1004                // Mark 2.
1005                // All menus in the popup are of type ListMenuItemView.
1006                r.setViewType(ViewType.ACTION_BAR_OVERFLOW_MENU);
1007            } else {
1008                // Mark 3.
1009                ViewGroup.LayoutParams lp = view.getLayoutParams();
1010                if (lp instanceof ActionMenuView.LayoutParams &&
1011                        ((ActionMenuView.LayoutParams) lp).isOverflowButton) {
1012                    r.setViewType(ViewType.ACTION_BAR_OVERFLOW);
1013                } else {
1014                    // Mark 1.
1015                    // A view is a menu in the Action Bar is it is not the overflow button and of
1016                    // its parent is of type ActionMenuView. We can also check if the view is
1017                    // instanceof ActionMenuItemView but that will fail for menus using
1018                    // actionProviderClass.
1019                    while (parent != mViewRoot && parent instanceof ViewGroup) {
1020                        if (parent instanceof ActionMenuView) {
1021                            r.setViewType(ViewType.ACTION_BAR_MENU);
1022                            break;
1023                        }
1024                        parent = parent.getParent();
1025                    }
1026                }
1027            }
1028        }
1029
1030        if (setExtendedInfo) {
1031            MarginLayoutParams marginParams = null;
1032            LayoutParams params = view.getLayoutParams();
1033            if (params instanceof MarginLayoutParams) {
1034                marginParams = (MarginLayoutParams) params;
1035            }
1036            result.setExtendedInfo(view.getBaseline(),
1037                    marginParams != null ? marginParams.leftMargin : 0,
1038                    marginParams != null ? marginParams.topMargin : 0,
1039                    marginParams != null ? marginParams.rightMargin : 0,
1040                    marginParams != null ? marginParams.bottomMargin : 0);
1041        }
1042
1043        return result;
1044    }
1045
1046    /* (non-Javadoc)
1047     * The cookie for menu items are stored in menu item and not in the map from View stored in
1048     * BridgeContext.
1049     */
1050    @Nullable
1051    private Object getViewKey(View view) {
1052        BridgeContext context = getContext();
1053        if (!(view instanceof MenuView.ItemView)) {
1054            return context.getViewKey(view);
1055        }
1056        MenuItemImpl menuItem;
1057        if (view instanceof ActionMenuItemView) {
1058            menuItem = ((ActionMenuItemView) view).getItemData();
1059        } else if (view instanceof ListMenuItemView) {
1060            menuItem = ((ListMenuItemView) view).getItemData();
1061        } else if (view instanceof IconMenuItemView) {
1062            menuItem = ((IconMenuItemView) view).getItemData();
1063        } else {
1064            menuItem = null;
1065        }
1066        if (menuItem instanceof BridgeMenuItemImpl) {
1067            return ((BridgeMenuItemImpl) menuItem).getViewCookie();
1068        }
1069
1070        return null;
1071    }
1072
1073    public void invalidateRenderingSize() {
1074        mMeasuredScreenWidth = mMeasuredScreenHeight = -1;
1075    }
1076
1077    public BufferedImage getImage() {
1078        return mImage;
1079    }
1080
1081    public boolean isAlphaChannelImage() {
1082        return mIsAlphaChannelImage;
1083    }
1084
1085    public List<ViewInfo> getViewInfos() {
1086        return mViewInfoList;
1087    }
1088
1089    public List<ViewInfo> getSystemViewInfos() {
1090        return mSystemViewInfoList;
1091    }
1092
1093    public Map<Object, PropertiesMap> getDefaultProperties() {
1094        return getContext().getDefaultProperties();
1095    }
1096
1097    public void setScene(RenderSession session) {
1098        mScene = session;
1099    }
1100
1101    public RenderSession getSession() {
1102        return mScene;
1103    }
1104
1105    public void dispose() {
1106        boolean createdLooper = false;
1107        if (Looper.myLooper() == null) {
1108            // Detaching the root view from the window will try to stop any running animations.
1109            // The stop method checks that it can run in the looper so, if there is no current
1110            // looper, we create a temporary one to complete the shutdown.
1111            Bridge.prepareThread();
1112            createdLooper = true;
1113        }
1114        AttachInfo_Accessor.detachFromWindow(mViewRoot);
1115        if (mCanvas != null) {
1116            mCanvas.release();
1117            mCanvas = null;
1118        }
1119        if (mViewInfoList != null) {
1120            mViewInfoList.clear();
1121        }
1122        if (mSystemViewInfoList != null) {
1123            mSystemViewInfoList.clear();
1124        }
1125        mImage = null;
1126        mViewRoot = null;
1127        mContentRoot = null;
1128
1129        if (createdLooper) {
1130            Bridge.cleanupThread();
1131            Choreographer_Delegate.dispose();
1132        }
1133    }
1134}
1135