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