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