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