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