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