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