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