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