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