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