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