RenderSessionImpl.java revision d7cc1244d7f0901688545c8535fa531fbb68669a
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            mViewRoot.dispatchAttachedToWindow(info, 0);
348
349            // get the background drawable
350            if (mWindowBackground != null) {
351                Drawable d = ResourceHelper.getDrawable(mWindowBackground,
352                        mContext, true /* isFramework */);
353                mViewRoot.setBackgroundDrawable(d);
354            }
355
356            return SUCCESS.createResult();
357        } catch (PostInflateException e) {
358            return ERROR_INFLATION.createResult(e.getMessage(), e);
359        } catch (Throwable e) {
360            // get the real cause of the exception.
361            Throwable t = e;
362            while (t.getCause() != null) {
363                t = t.getCause();
364            }
365
366            // log it
367            mParams.getLog().error("Scene inflate failed", t);
368
369            return ERROR_INFLATION.createResult(t.getMessage(), t);
370        }
371    }
372
373    /**
374     * Renders the scene.
375     * <p>
376     * {@link #acquire(long)} must have been called before this.
377     *
378     * @throws IllegalStateException if the current context is different than the one owned by
379     *      the scene, or if {@link #acquire(long)} was not called.
380     *
381     * @see SceneParams#getRenderingMode()
382     * @see LayoutScene#render(long)
383     */
384    public Result render() {
385        checkLock();
386
387        try {
388            if (mViewRoot == null) {
389                return ERROR_NOT_INFLATED.createResult();
390            }
391            // measure the views
392            int w_spec, h_spec;
393
394            RenderingMode renderingMode = mParams.getRenderingMode();
395
396            // only do the screen measure when needed.
397            boolean newRenderSize = false;
398            if (mMeasuredScreenWidth == -1) {
399                newRenderSize = true;
400                mMeasuredScreenWidth = mParams.getScreenWidth();
401                mMeasuredScreenHeight = mParams.getScreenHeight();
402
403                if (renderingMode != RenderingMode.NORMAL) {
404                    // measure the full size needed by the layout.
405                    w_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenWidth,
406                            renderingMode.isHorizExpand() ?
407                                    MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
408                                    : MeasureSpec.EXACTLY);
409                    h_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenHeight - mScreenOffset,
410                            renderingMode.isVertExpand() ?
411                                    MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
412                                    : MeasureSpec.EXACTLY);
413                    mViewRoot.measure(w_spec, h_spec);
414
415                    if (renderingMode.isHorizExpand()) {
416                        int neededWidth = mViewRoot.getChildAt(0).getMeasuredWidth();
417                        if (neededWidth > mMeasuredScreenWidth) {
418                            mMeasuredScreenWidth = neededWidth;
419                        }
420                    }
421
422                    if (renderingMode.isVertExpand()) {
423                        int neededHeight = mViewRoot.getChildAt(0).getMeasuredHeight();
424                        if (neededHeight > mMeasuredScreenHeight - mScreenOffset) {
425                            mMeasuredScreenHeight = neededHeight + mScreenOffset;
426                        }
427                    }
428                }
429            }
430
431            // remeasure with the size we need
432            // This must always be done before the call to layout
433            w_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenWidth, MeasureSpec.EXACTLY);
434            h_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenHeight - mScreenOffset,
435                    MeasureSpec.EXACTLY);
436            mViewRoot.measure(w_spec, h_spec);
437
438            // now do the layout.
439            mViewRoot.layout(0, mScreenOffset, mMeasuredScreenWidth, mMeasuredScreenHeight);
440
441            // draw the views
442            // create the BufferedImage into which the layout will be rendered.
443            if (newRenderSize || mCanvas == null) {
444                if (mParams.getImageFactory() != null) {
445                    mImage = mParams.getImageFactory().getImage(mMeasuredScreenWidth,
446                            mMeasuredScreenHeight - mScreenOffset);
447                } else {
448                    mImage = new BufferedImage(mMeasuredScreenWidth,
449                            mMeasuredScreenHeight - mScreenOffset, BufferedImage.TYPE_INT_ARGB);
450                }
451
452                if (mParams.isBgColorOverridden()) {
453                    Graphics2D gc = mImage.createGraphics();
454                    gc.setColor(new Color(mParams.getOverrideBgColor(), true));
455                    gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight - mScreenOffset);
456                    gc.dispose();
457                }
458
459                // create an Android bitmap around the BufferedImage
460                Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage,
461                        true /*isMutable*/,
462                        ResourceDensity.getEnum(mParams.getDensity()));
463
464                // create a Canvas around the Android bitmap
465                mCanvas = new Canvas(bitmap);
466                mCanvas.setDensity(mParams.getDensity());
467            }
468
469            mViewRoot.draw(mCanvas);
470
471            mViewInfo = visit(((ViewGroup)mViewRoot).getChildAt(0), mContext);
472
473            // success!
474            return SUCCESS.createResult();
475        } catch (Throwable e) {
476            // get the real cause of the exception.
477            Throwable t = e;
478            while (t.getCause() != null) {
479                t = t.getCause();
480            }
481
482            // log it
483            mParams.getLog().error("Scene Render failed", t);
484
485            return ERROR_UNKNOWN.createResult(t.getMessage(), t);
486        }
487    }
488
489    /**
490     * Animate an object
491     * <p>
492     * {@link #acquire(long)} must have been called before this.
493     *
494     * @throws IllegalStateException if the current context is different than the one owned by
495     *      the scene, or if {@link #acquire(long)} was not called.
496     *
497     * @see LayoutScene#animate(Object, String, boolean, IAnimationListener)
498     */
499    public Result animate(Object targetObject, String animationName,
500            boolean isFrameworkAnimation, IAnimationListener listener) {
501        checkLock();
502
503        // find the animation file.
504        ResourceValue animationResource = null;
505        int animationId = 0;
506        if (isFrameworkAnimation) {
507            animationResource = mContext.getFrameworkResource("anim", animationName);
508            if (animationResource != null) {
509                animationId = Bridge.getResourceValue("anim", animationName);
510            }
511        } else {
512            animationResource = mContext.getProjectResource("anim", animationName);
513            if (animationResource != null) {
514                animationId = mContext.getProjectCallback().getResourceValue("anim", animationName);
515            }
516        }
517
518        if (animationResource != null) {
519            try {
520                Animator anim = AnimatorInflater.loadAnimator(mContext, animationId);
521                if (anim != null) {
522                    anim.setTarget(targetObject);
523
524                    new PlayAnimationThread(anim, this, animationName, listener).start();
525
526                    return SUCCESS.createResult();
527                }
528            } catch (Exception e) {
529                // get the real cause of the exception.
530                Throwable t = e;
531                while (t.getCause() != null) {
532                    t = t.getCause();
533                }
534
535                return ERROR_UNKNOWN.createResult(t.getMessage(), t);
536            }
537        }
538
539        return ERROR_ANIM_NOT_FOUND.createResult();
540    }
541
542    /**
543     * Insert a new child into an existing parent.
544     * <p>
545     * {@link #acquire(long)} must have been called before this.
546     *
547     * @throws IllegalStateException if the current context is different than the one owned by
548     *      the scene, or if {@link #acquire(long)} was not called.
549     *
550     * @see LayoutScene#insertChild(Object, ILayoutPullParser, int, IAnimationListener)
551     */
552    public Result insertChild(final ViewGroup parentView, ILayoutPullParser childXml,
553            final int index, IAnimationListener listener) {
554        checkLock();
555
556        // create a block parser for the XML
557        BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(childXml, mContext,
558                false /* platformResourceFlag */);
559
560        // inflate the child without adding it to the root since we want to control where it'll
561        // get added. We do pass the parentView however to ensure that the layoutParams will
562        // be created correctly.
563        final View child = mInflater.inflate(blockParser, parentView, false /*attachToRoot*/);
564
565        invalidateRenderingSize();
566
567        if (listener != null) {
568            new AnimationThread(this, "insertChild", listener) {
569
570                @Override
571                public Result preAnimation() {
572                    parentView.setLayoutTransition(new LayoutTransition());
573                    return addView(parentView, child, index);
574                }
575
576                @Override
577                public void postAnimation() {
578                    parentView.setLayoutTransition(null);
579                }
580            }.start();
581
582            // always return success since the real status will come through the listener.
583            return SUCCESS.createResult(child);
584        }
585
586        // add it to the parentView in the correct location
587        Result result = addView(parentView, child, index);
588        if (result.isSuccess() == false) {
589            return result;
590        }
591
592        result = render();
593        if (result.isSuccess()) {
594            result = result.getCopyWithData(child);
595        }
596
597        return result;
598    }
599
600    /**
601     * Adds a given view to a given parent at a given index.
602     *
603     * @param parent the parent to receive the view
604     * @param view the view to add to the parent
605     * @param index the index where to do the add.
606     *
607     * @return a Result with {@link Status#SUCCESS} or
608     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
609     *     adding views.
610     */
611    private Result addView(ViewGroup parent, View view, int index) {
612        try {
613            parent.addView(view, index);
614            return SUCCESS.createResult();
615        } catch (UnsupportedOperationException e) {
616            // looks like this is a view class that doesn't support children manipulation!
617            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
618        }
619    }
620
621    /**
622     * Moves a view to a new parent at a given location
623     * <p>
624     * {@link #acquire(long)} must have been called before this.
625     *
626     * @throws IllegalStateException if the current context is different than the one owned by
627     *      the scene, or if {@link #acquire(long)} was not called.
628     *
629     * @see LayoutScene#moveChild(Object, Object, int, Map, IAnimationListener)
630     */
631    public Result moveChild(final ViewGroup newParentView, final View childView, final int index,
632            Map<String, String> layoutParamsMap, IAnimationListener listener) {
633        checkLock();
634
635        invalidateRenderingSize();
636
637        LayoutParams layoutParams = null;
638        if (layoutParamsMap != null) {
639            // need to create a new LayoutParams object for the new parent.
640            layoutParams = newParentView.generateLayoutParams(
641                    new BridgeLayoutParamsMapAttributes(layoutParamsMap));
642        }
643
644        // get the current parent of the view that needs to be moved.
645        final ViewGroup previousParent = (ViewGroup) childView.getParent();
646
647        if (listener != null) {
648            final LayoutParams params = layoutParams;
649            new AnimationThread(this, "moveChild", listener) {
650
651                @Override
652                public Result preAnimation() {
653                    // set up the transition for the previous parent.
654                    LayoutTransition removeTransition = new LayoutTransition();
655                    previousParent.setLayoutTransition(removeTransition);
656
657                    // no fade-out
658                    // FIXME: set a non-null do-nothing Animator.
659                    // setting a null animator doesn't work because the child gets its parent
660                    // set to null too late.
661                    //removeTransition.setAnimator(LayoutTransition.DISAPPEARING, null);
662
663                    // now for the new parent, if different
664                    if (previousParent != newParentView) {
665                        LayoutTransition addTransition = new LayoutTransition();
666
667                        // no fade-in
668                        // FIXME: set a non-null do-nothing Animator.
669                        // setting a null animator doesn't work because the child gets its parent
670                        // set to null too late.
671                        //addTransition.setAnimator(LayoutTransition.APPEARING, null);
672
673                        newParentView.setLayoutTransition(addTransition);
674                    }
675
676                    return moveView(previousParent, newParentView, childView, index, params);
677                }
678
679                @Override
680                public void postAnimation() {
681                    previousParent.setLayoutTransition(null);
682                    newParentView.setLayoutTransition(null);
683                }
684            }.start();
685
686            // always return success since the real status will come through the listener.
687            return SUCCESS.createResult(layoutParams);
688        }
689
690        Result result = moveView(previousParent, newParentView, childView, index, layoutParams);
691        if (result.isSuccess() == false) {
692            return result;
693        }
694
695        result = render();
696        if (layoutParams != null && result.isSuccess()) {
697            result = result.getCopyWithData(layoutParams);
698        }
699
700        return result;
701    }
702
703    /**
704     * Moves a View from its current parent to a new given parent at a new given location, with
705     * an optional new {@link LayoutParams} instance
706     *
707     * @param previousParent the previous parent, still owning the child at the time of the call.
708     * @param newParent the new parent
709     * @param view the view to move
710     * @param index the new location in the new parent
711     * @param params an option (can be null) {@link LayoutParams} instance.
712     *
713     * @return a Result with {@link Status#SUCCESS} or
714     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
715     *     adding views.
716     */
717    private Result moveView(ViewGroup previousParent, final ViewGroup newParent, View view,
718            final int index, final LayoutParams params) {
719        try {
720            // check if there is a transition on the previousParent.
721            LayoutTransition transition = previousParent.getLayoutTransition();
722            if (transition != null) {
723                // in this case there is an animation. This means we have to listener for the
724                // disappearing animation to be done before we can add the view to the new parent.
725                // TODO: check that if the disappearing animation is null, the removal is done during the removeView call which would simplify the code.
726
727                // add a listener to the transition to be notified of the actual removal.
728                transition.addTransitionListener(new TransitionListener() {
729
730                    public void startTransition(LayoutTransition transition, ViewGroup container,
731                            View view, int transitionType) {
732                        // don't care.
733                    }
734
735                    public void endTransition(LayoutTransition transition, ViewGroup container,
736                            View view, int transitionType) {
737                        if (transitionType == LayoutTransition.DISAPPEARING) {
738                            // add it to the parentView in the correct location
739                            if (params != null) {
740                                newParent.addView(view, index, params);
741                            } else {
742                                newParent.addView(view, index);
743                            }
744
745                        }
746                    }
747                });
748
749                // remove the view from the current parent.
750                previousParent.removeView(view);
751
752                // and return since adding the view to the new parent is done in the listener.
753                return SUCCESS.createResult();
754            } else {
755                // standard code with no animation. pretty simple.
756                previousParent.removeView(view);
757
758                // add it to the parentView in the correct location
759                if (params != null) {
760                    newParent.addView(view, index, params);
761                } else {
762                    newParent.addView(view, index);
763                }
764
765                return SUCCESS.createResult();
766            }
767        } catch (UnsupportedOperationException e) {
768            // looks like this is a view class that doesn't support children manipulation!
769            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
770        }
771    }
772
773    /**
774     * Removes a child from its current parent.
775     * <p>
776     * {@link #acquire(long)} must have been called before this.
777     *
778     * @throws IllegalStateException if the current context is different than the one owned by
779     *      the scene, or if {@link #acquire(long)} was not called.
780     *
781     * @see LayoutScene#removeChild(Object, IAnimationListener)
782     */
783    public Result removeChild(final View childView, IAnimationListener listener) {
784        checkLock();
785
786        invalidateRenderingSize();
787
788        final ViewGroup parent = (ViewGroup) childView.getParent();
789
790        if (listener != null) {
791            new AnimationThread(this, "moveChild", listener) {
792
793                @Override
794                public Result preAnimation() {
795                    parent.setLayoutTransition(new LayoutTransition());
796                    return removeView(parent, childView);
797                }
798
799                @Override
800                public void postAnimation() {
801                    parent.setLayoutTransition(null);
802                }
803            }.start();
804
805            // always return success since the real status will come through the listener.
806            return SUCCESS.createResult();
807        }
808
809        Result result = removeView(parent, childView);
810        if (result.isSuccess() == false) {
811            return result;
812        }
813
814        return render();
815    }
816
817    /**
818     * Removes a given view from its current parent.
819     *
820     * @param view the view to remove from its parent
821     *
822     * @return a Result with {@link Status#SUCCESS} or
823     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
824     *     adding views.
825     */
826    private Result removeView(ViewGroup parent, View view) {
827        try {
828            parent.removeView(view);
829            return SUCCESS.createResult();
830        } catch (UnsupportedOperationException e) {
831            // looks like this is a view class that doesn't support children manipulation!
832            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
833        }
834    }
835
836    /**
837     * Returns the log associated with the session.
838     * @return the log or null if there are none.
839     */
840    public LayoutLog getLog() {
841        if (mParams != null) {
842            return mParams.getLog();
843        }
844
845        return null;
846    }
847
848    /**
849     * Checks that the lock is owned by the current thread and that the current context is the one
850     * from this scene.
851     *
852     * @throws IllegalStateException if the current context is different than the one owned by
853     *      the scene, or if {@link #acquire(long)} was not called.
854     */
855    private void checkLock() {
856        ReentrantLock lock = Bridge.getLock();
857        if (lock.isHeldByCurrentThread() == false) {
858            throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
859        }
860        if (sCurrentContext != mContext) {
861            throw new IllegalStateException("Thread acquired a scene but is rendering a different one");
862        }
863    }
864
865
866    /**
867     * Compute style information from the given list of style for the project and framework.
868     * @param themeName the name of the current theme.  In order to differentiate project and
869     * platform themes sharing the same name, all project themes must be prepended with
870     * a '*' character.
871     * @param isProjectTheme Is this a project theme
872     * @param inProjectStyleMap the project style map
873     * @param inFrameworkStyleMap the framework style map
874     * @param outInheritanceMap the map of style inheritance. This is filled by the method
875     * @return the {@link StyleResourceValue} matching <var>themeName</var>
876     */
877    private StyleResourceValue computeStyleMaps(
878            String themeName, boolean isProjectTheme, Map<String,
879            ResourceValue> inProjectStyleMap, Map<String, ResourceValue> inFrameworkStyleMap,
880            Map<StyleResourceValue, StyleResourceValue> outInheritanceMap) {
881
882        if (inProjectStyleMap != null && inFrameworkStyleMap != null) {
883            // first, get the theme
884            ResourceValue theme = null;
885
886            // project theme names have been prepended with a *
887            if (isProjectTheme) {
888                theme = inProjectStyleMap.get(themeName);
889            } else {
890                theme = inFrameworkStyleMap.get(themeName);
891            }
892
893            if (theme instanceof StyleResourceValue) {
894                // compute the inheritance map for both the project and framework styles
895                computeStyleInheritance(inProjectStyleMap.values(), inProjectStyleMap,
896                        inFrameworkStyleMap, outInheritanceMap);
897
898                // Compute the style inheritance for the framework styles/themes.
899                // Since, for those, the style parent values do not contain 'android:'
900                // we want to force looking in the framework style only to avoid using
901                // similarly named styles from the project.
902                // To do this, we pass null in lieu of the project style map.
903                computeStyleInheritance(inFrameworkStyleMap.values(), null /*inProjectStyleMap */,
904                        inFrameworkStyleMap, outInheritanceMap);
905
906                return (StyleResourceValue)theme;
907            }
908        }
909
910        return null;
911    }
912
913    /**
914     * Compute the parent style for all the styles in a given list.
915     * @param styles the styles for which we compute the parent.
916     * @param inProjectStyleMap the map of project styles.
917     * @param inFrameworkStyleMap the map of framework styles.
918     * @param outInheritanceMap the map of style inheritance. This is filled by the method.
919     */
920    private void computeStyleInheritance(Collection<ResourceValue> styles,
921            Map<String, ResourceValue> inProjectStyleMap,
922            Map<String, ResourceValue> inFrameworkStyleMap,
923            Map<StyleResourceValue, StyleResourceValue> outInheritanceMap) {
924        for (ResourceValue value : styles) {
925            if (value instanceof StyleResourceValue) {
926                StyleResourceValue style = (StyleResourceValue)value;
927                StyleResourceValue parentStyle = null;
928
929                // first look for a specified parent.
930                String parentName = style.getParentStyle();
931
932                // no specified parent? try to infer it from the name of the style.
933                if (parentName == null) {
934                    parentName = getParentName(value.getName());
935                }
936
937                if (parentName != null) {
938                    parentStyle = getStyle(parentName, inProjectStyleMap, inFrameworkStyleMap);
939
940                    if (parentStyle != null) {
941                        outInheritanceMap.put(style, parentStyle);
942                    }
943                }
944            }
945        }
946    }
947
948    /**
949     * Searches for and returns the {@link StyleResourceValue} from a given name.
950     * <p/>The format of the name can be:
951     * <ul>
952     * <li>[android:]&lt;name&gt;</li>
953     * <li>[android:]style/&lt;name&gt;</li>
954     * <li>@[android:]style/&lt;name&gt;</li>
955     * </ul>
956     * @param parentName the name of the style.
957     * @param inProjectStyleMap the project style map. Can be <code>null</code>
958     * @param inFrameworkStyleMap the framework style map.
959     * @return The matching {@link StyleResourceValue} object or <code>null</code> if not found.
960     */
961    private StyleResourceValue getStyle(String parentName,
962            Map<String, ResourceValue> inProjectStyleMap,
963            Map<String, ResourceValue> inFrameworkStyleMap) {
964        boolean frameworkOnly = false;
965
966        String name = parentName;
967
968        // remove the useless @ if it's there
969        if (name.startsWith(BridgeConstants.PREFIX_RESOURCE_REF)) {
970            name = name.substring(BridgeConstants.PREFIX_RESOURCE_REF.length());
971        }
972
973        // check for framework identifier.
974        if (name.startsWith(BridgeConstants.PREFIX_ANDROID)) {
975            frameworkOnly = true;
976            name = name.substring(BridgeConstants.PREFIX_ANDROID.length());
977        }
978
979        // at this point we could have the format <type>/<name>. we want only the name as long as
980        // the type is style.
981        if (name.startsWith(BridgeConstants.REFERENCE_STYLE)) {
982            name = name.substring(BridgeConstants.REFERENCE_STYLE.length());
983        } else if (name.indexOf('/') != -1) {
984            return null;
985        }
986
987        ResourceValue parent = null;
988
989        // if allowed, search in the project resources.
990        if (frameworkOnly == false && inProjectStyleMap != null) {
991            parent = inProjectStyleMap.get(name);
992        }
993
994        // if not found, then look in the framework resources.
995        if (parent == null) {
996            parent = inFrameworkStyleMap.get(name);
997        }
998
999        // make sure the result is the proper class type and return it.
1000        if (parent instanceof StyleResourceValue) {
1001            return (StyleResourceValue)parent;
1002        }
1003
1004        assert false;
1005        mParams.getLog().error(null,
1006                String.format("Unable to resolve parent style name: %s", parentName));
1007
1008        return null;
1009    }
1010
1011    /**
1012     * Computes the name of the parent style, or <code>null</code> if the style is a root style.
1013     */
1014    private String getParentName(String styleName) {
1015        int index = styleName.lastIndexOf('.');
1016        if (index != -1) {
1017            return styleName.substring(0, index);
1018        }
1019
1020        return null;
1021    }
1022
1023    /**
1024     * Returns the top screen offset. This depends on whether the current theme defines the user
1025     * of the title and status bars.
1026     * @param frameworkResources The framework resources
1027     * @param currentTheme The current theme
1028     * @param context The context
1029     * @return the pixel height offset
1030     */
1031    private int getScreenOffset(Map<String, Map<String, ResourceValue>> frameworkResources,
1032            StyleResourceValue currentTheme, BridgeContext context) {
1033        int offset = 0;
1034
1035        // get the title bar flag from the current theme.
1036        ResourceValue value = context.findItemInStyle(currentTheme, "windowNoTitle");
1037
1038        // because it may reference something else, we resolve it.
1039        value = context.resolveResValue(value);
1040
1041        // if there's a value and it's true (default is false)
1042        if (value == null || value.getValue() == null ||
1043                XmlUtils.convertValueToBoolean(value.getValue(), false /* defValue */) == false) {
1044            // default size of the window title bar
1045            int defaultOffset = DEFAULT_TITLE_BAR_HEIGHT;
1046
1047            // get value from the theme.
1048            value = context.findItemInStyle(currentTheme, "windowTitleSize");
1049
1050            // resolve it
1051            value = context.resolveResValue(value);
1052
1053            if (value != null) {
1054                // get the numerical value, if available
1055                TypedValue typedValue = ResourceHelper.getValue(value.getValue());
1056                if (typedValue != null) {
1057                    // compute the pixel value based on the display metrics
1058                    defaultOffset = (int)typedValue.getDimension(context.getResources().mMetrics);
1059                }
1060            }
1061
1062            offset += defaultOffset;
1063        }
1064
1065        // get the fullscreen flag from the current theme.
1066        value = context.findItemInStyle(currentTheme, "windowFullscreen");
1067
1068        // because it may reference something else, we resolve it.
1069        value = context.resolveResValue(value);
1070
1071        if (value == null || value.getValue() == null ||
1072                XmlUtils.convertValueToBoolean(value.getValue(), false /* defValue */) == false) {
1073
1074            // default value
1075            int defaultOffset = DEFAULT_STATUS_BAR_HEIGHT;
1076
1077            // get the real value, first the list of Dimensions from the framework map
1078            Map<String, ResourceValue> dimens = frameworkResources.get(BridgeConstants.RES_DIMEN);
1079
1080            // now get the value
1081            value = dimens.get("status_bar_height");
1082            if (value != null) {
1083                TypedValue typedValue = ResourceHelper.getValue(value.getValue());
1084                if (typedValue != null) {
1085                    // compute the pixel value based on the display metrics
1086                    defaultOffset = (int)typedValue.getDimension(context.getResources().mMetrics);
1087                }
1088            }
1089
1090            // add the computed offset.
1091            offset += defaultOffset;
1092        }
1093
1094        return offset;
1095
1096    }
1097
1098    /**
1099     * Post process on a view hierachy that was just inflated.
1100     * <p/>At the moment this only support TabHost: If {@link TabHost} is detected, look for the
1101     * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically
1102     * based on the content of the {@link FrameLayout}.
1103     * @param view the root view to process.
1104     * @param projectCallback callback to the project.
1105     */
1106    private void postInflateProcess(View view, IProjectCallback projectCallback)
1107            throws PostInflateException {
1108        if (view instanceof TabHost) {
1109            setupTabHost((TabHost)view, projectCallback);
1110        } else if (view instanceof ViewGroup) {
1111            ViewGroup group = (ViewGroup)view;
1112            final int count = group.getChildCount();
1113            for (int c = 0 ; c < count ; c++) {
1114                View child = group.getChildAt(c);
1115                postInflateProcess(child, projectCallback);
1116            }
1117        }
1118    }
1119
1120    /**
1121     * Sets up a {@link TabHost} object.
1122     * @param tabHost the TabHost to setup.
1123     * @param projectCallback The project callback object to access the project R class.
1124     * @throws PostInflateException
1125     */
1126    private void setupTabHost(TabHost tabHost, IProjectCallback projectCallback)
1127            throws PostInflateException {
1128        // look for the TabWidget, and the FrameLayout. They have their own specific names
1129        View v = tabHost.findViewById(android.R.id.tabs);
1130
1131        if (v == null) {
1132            throw new PostInflateException(
1133                    "TabHost requires a TabWidget with id \"android:id/tabs\".\n");
1134        }
1135
1136        if ((v instanceof TabWidget) == false) {
1137            throw new PostInflateException(String.format(
1138                    "TabHost requires a TabWidget with id \"android:id/tabs\".\n" +
1139                    "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName()));
1140        }
1141
1142        v = tabHost.findViewById(android.R.id.tabcontent);
1143
1144        if (v == null) {
1145            // TODO: see if we can fake tabs even without the FrameLayout (same below when the framelayout is empty)
1146            throw new PostInflateException(
1147                    "TabHost requires a FrameLayout with id \"android:id/tabcontent\".");
1148        }
1149
1150        if ((v instanceof FrameLayout) == false) {
1151            throw new PostInflateException(String.format(
1152                    "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" +
1153                    "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName()));
1154        }
1155
1156        FrameLayout content = (FrameLayout)v;
1157
1158        // now process the content of the framelayout and dynamically create tabs for it.
1159        final int count = content.getChildCount();
1160
1161        if (count == 0) {
1162            throw new PostInflateException(
1163                    "The FrameLayout for the TabHost has no content. Rendering failed.\n");
1164        }
1165
1166        // this must be called before addTab() so that the TabHost searches its TabWidget
1167        // and FrameLayout.
1168        tabHost.setup();
1169
1170        // for each child of the framelayout, add a new TabSpec
1171        for (int i = 0 ; i < count ; i++) {
1172            View child = content.getChildAt(i);
1173            String tabSpec = String.format("tab_spec%d", i+1);
1174            int id = child.getId();
1175            String[] resource = projectCallback.resolveResourceValue(id);
1176            String name;
1177            if (resource != null) {
1178                name = resource[0]; // 0 is resource name, 1 is resource type.
1179            } else {
1180                name = String.format("Tab %d", i+1); // default name if id is unresolved.
1181            }
1182            tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id));
1183        }
1184    }
1185
1186
1187    /**
1188     * Visits a View and its children and generate a {@link ViewInfo} containing the
1189     * bounds of all the views.
1190     * @param view the root View
1191     * @param context the context.
1192     */
1193    private ViewInfo visit(View view, BridgeContext context) {
1194        if (view == null) {
1195            return null;
1196        }
1197
1198        ViewInfo result = new ViewInfo(view.getClass().getName(),
1199                context.getViewKey(view),
1200                view.getLeft(), view.getTop(), view.getRight(), view.getBottom(),
1201                view, view.getLayoutParams());
1202
1203        if (view instanceof ViewGroup) {
1204            ViewGroup group = ((ViewGroup) view);
1205            List<ViewInfo> children = new ArrayList<ViewInfo>();
1206            for (int i = 0; i < group.getChildCount(); i++) {
1207                children.add(visit(group.getChildAt(i), context));
1208            }
1209            result.setChildren(children);
1210        }
1211
1212        return result;
1213    }
1214
1215    private void invalidateRenderingSize() {
1216        mMeasuredScreenWidth = mMeasuredScreenHeight = -1;
1217    }
1218
1219    public BufferedImage getImage() {
1220        return mImage;
1221    }
1222
1223    public ViewInfo getViewInfo() {
1224        return mViewInfo;
1225    }
1226
1227    public Map<String, String> getDefaultProperties(Object viewObject) {
1228        return mContext.getDefaultPropMap(viewObject);
1229    }
1230
1231    public void setScene(RenderSession session) {
1232        mScene = session;
1233    }
1234
1235    public RenderSession getSession() {
1236        return mScene;
1237    }
1238}
1239