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