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