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