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