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