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