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