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