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