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