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