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