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