RenderSessionImpl.java revision 7550ec1d8e526c4ae8c0bb08b06e1b6e799eacec
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. Because we can't rely on layout transition listeners when
658                    // there is no Animator at all, instead we keep the animator but set its
659                    // duration to 0.
660                    // Note: Cannot user Animation.setDuration() directly. Have to set it
661                    // on the LayoutTransition.
662                    removeTransition.setDuration(LayoutTransition.DISAPPEARING, 0);
663
664                    if (previousParent != newParentView) {
665                        // different parent, set a Layout transition on the new parent.
666                        newParentView.setLayoutTransition(new LayoutTransition());
667                    }
668
669                    // no fade-in. Because we can't rely on layout transition listeners when
670                    // there is no Animator at all, instead we keep the animator but set its
671                    // duration to 0.
672                    // Note: Cannot user Animation.setDuration() directly. Have to set it
673                    // on the LayoutTransition.
674                    newParentView.getLayoutTransition().setDuration(LayoutTransition.APPEARING, 0);
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 wait for the child's
724                // parent reference to be null'ed out so that we can add it to the new parent.
725                // It is technically removed right before the DISAPPEARING animation is done (if
726                // the animation of this type is not null, otherwise it's after which is impossible
727                // to handle).
728                // Because there is no move animation, if the new parent is the same as the old
729                // parent, we need to wait until the CHANGE_DISAPPEARING animation is done before
730                // adding the child or the child will appear in its new location before the
731                // other children have made room for it.
732                // If the parents are different, then we can add the child to its new parent right
733                // after the DISAPPEARING animation is done.
734
735                final int waitForType = newParent == previousParent ?
736                        LayoutTransition.CHANGE_DISAPPEARING : LayoutTransition.DISAPPEARING;
737
738                // add a listener to the transition to be notified of the actual removal.
739                transition.addTransitionListener(new TransitionListener() {
740
741                    public void startTransition(LayoutTransition transition, ViewGroup container,
742                            View view, int transitionType) {
743                        // don't care.
744                    }
745
746                    public void endTransition(LayoutTransition transition, ViewGroup container,
747                            View view, int transitionType) {
748                        if (transitionType == waitForType) {
749                            // add it to the parentView in the correct location
750                            if (params != null) {
751                                newParent.addView(view, index, params);
752                            } else {
753                                newParent.addView(view, index);
754                            }
755
756                        }
757                    }
758                });
759
760                // remove the view from the current parent.
761                previousParent.removeView(view);
762
763                // and return since adding the view to the new parent is done in the listener.
764                return SUCCESS.createResult();
765            } else {
766                // standard code with no animation. pretty simple.
767                previousParent.removeView(view);
768
769                // add it to the parentView in the correct location
770                if (params != null) {
771                    newParent.addView(view, index, params);
772                } else {
773                    newParent.addView(view, index);
774                }
775
776                return SUCCESS.createResult();
777            }
778        } catch (UnsupportedOperationException e) {
779            // looks like this is a view class that doesn't support children manipulation!
780            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
781        }
782    }
783
784    /**
785     * Removes a child from its current parent.
786     * <p>
787     * {@link #acquire(long)} must have been called before this.
788     *
789     * @throws IllegalStateException if the current context is different than the one owned by
790     *      the scene, or if {@link #acquire(long)} was not called.
791     *
792     * @see LayoutScene#removeChild(Object, IAnimationListener)
793     */
794    public Result removeChild(final View childView, IAnimationListener listener) {
795        checkLock();
796
797        invalidateRenderingSize();
798
799        final ViewGroup parent = (ViewGroup) childView.getParent();
800
801        if (listener != null) {
802            new AnimationThread(this, "moveChild", listener) {
803
804                @Override
805                public Result preAnimation() {
806                    parent.setLayoutTransition(new LayoutTransition());
807                    return removeView(parent, childView);
808                }
809
810                @Override
811                public void postAnimation() {
812                    parent.setLayoutTransition(null);
813                }
814            }.start();
815
816            // always return success since the real status will come through the listener.
817            return SUCCESS.createResult();
818        }
819
820        Result result = removeView(parent, childView);
821        if (result.isSuccess() == false) {
822            return result;
823        }
824
825        return render();
826    }
827
828    /**
829     * Removes a given view from its current parent.
830     *
831     * @param view the view to remove from its parent
832     *
833     * @return a Result with {@link Status#SUCCESS} or
834     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
835     *     adding views.
836     */
837    private Result removeView(ViewGroup parent, View view) {
838        try {
839            parent.removeView(view);
840            return SUCCESS.createResult();
841        } catch (UnsupportedOperationException e) {
842            // looks like this is a view class that doesn't support children manipulation!
843            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
844        }
845    }
846
847    /**
848     * Returns the log associated with the session.
849     * @return the log or null if there are none.
850     */
851    public LayoutLog getLog() {
852        if (mParams != null) {
853            return mParams.getLog();
854        }
855
856        return null;
857    }
858
859    /**
860     * Checks that the lock is owned by the current thread and that the current context is the one
861     * from this scene.
862     *
863     * @throws IllegalStateException if the current context is different than the one owned by
864     *      the scene, or if {@link #acquire(long)} was not called.
865     */
866    private void checkLock() {
867        ReentrantLock lock = Bridge.getLock();
868        if (lock.isHeldByCurrentThread() == false) {
869            throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
870        }
871        if (sCurrentContext != mContext) {
872            throw new IllegalStateException("Thread acquired a scene but is rendering a different one");
873        }
874    }
875
876
877    /**
878     * Compute style information from the given list of style for the project and framework.
879     * @param themeName the name of the current theme.  In order to differentiate project and
880     * platform themes sharing the same name, all project themes must be prepended with
881     * a '*' character.
882     * @param isProjectTheme Is this a project theme
883     * @param inProjectStyleMap the project style map
884     * @param inFrameworkStyleMap the framework style map
885     * @param outInheritanceMap the map of style inheritance. This is filled by the method
886     * @return the {@link StyleResourceValue} matching <var>themeName</var>
887     */
888    private StyleResourceValue computeStyleMaps(
889            String themeName, boolean isProjectTheme, Map<String,
890            ResourceValue> inProjectStyleMap, Map<String, ResourceValue> inFrameworkStyleMap,
891            Map<StyleResourceValue, StyleResourceValue> outInheritanceMap) {
892
893        if (inProjectStyleMap != null && inFrameworkStyleMap != null) {
894            // first, get the theme
895            ResourceValue theme = null;
896
897            // project theme names have been prepended with a *
898            if (isProjectTheme) {
899                theme = inProjectStyleMap.get(themeName);
900            } else {
901                theme = inFrameworkStyleMap.get(themeName);
902            }
903
904            if (theme instanceof StyleResourceValue) {
905                // compute the inheritance map for both the project and framework styles
906                computeStyleInheritance(inProjectStyleMap.values(), inProjectStyleMap,
907                        inFrameworkStyleMap, outInheritanceMap);
908
909                // Compute the style inheritance for the framework styles/themes.
910                // Since, for those, the style parent values do not contain 'android:'
911                // we want to force looking in the framework style only to avoid using
912                // similarly named styles from the project.
913                // To do this, we pass null in lieu of the project style map.
914                computeStyleInheritance(inFrameworkStyleMap.values(), null /*inProjectStyleMap */,
915                        inFrameworkStyleMap, outInheritanceMap);
916
917                return (StyleResourceValue)theme;
918            }
919        }
920
921        return null;
922    }
923
924    /**
925     * Compute the parent style for all the styles in a given list.
926     * @param styles the styles for which we compute the parent.
927     * @param inProjectStyleMap the map of project styles.
928     * @param inFrameworkStyleMap the map of framework styles.
929     * @param outInheritanceMap the map of style inheritance. This is filled by the method.
930     */
931    private void computeStyleInheritance(Collection<ResourceValue> styles,
932            Map<String, ResourceValue> inProjectStyleMap,
933            Map<String, ResourceValue> inFrameworkStyleMap,
934            Map<StyleResourceValue, StyleResourceValue> outInheritanceMap) {
935        for (ResourceValue value : styles) {
936            if (value instanceof StyleResourceValue) {
937                StyleResourceValue style = (StyleResourceValue)value;
938                StyleResourceValue parentStyle = null;
939
940                // first look for a specified parent.
941                String parentName = style.getParentStyle();
942
943                // no specified parent? try to infer it from the name of the style.
944                if (parentName == null) {
945                    parentName = getParentName(value.getName());
946                }
947
948                if (parentName != null) {
949                    parentStyle = getStyle(parentName, inProjectStyleMap, inFrameworkStyleMap);
950
951                    if (parentStyle != null) {
952                        outInheritanceMap.put(style, parentStyle);
953                    }
954                }
955            }
956        }
957    }
958
959    /**
960     * Searches for and returns the {@link StyleResourceValue} from a given name.
961     * <p/>The format of the name can be:
962     * <ul>
963     * <li>[android:]&lt;name&gt;</li>
964     * <li>[android:]style/&lt;name&gt;</li>
965     * <li>@[android:]style/&lt;name&gt;</li>
966     * </ul>
967     * @param parentName the name of the style.
968     * @param inProjectStyleMap the project style map. Can be <code>null</code>
969     * @param inFrameworkStyleMap the framework style map.
970     * @return The matching {@link StyleResourceValue} object or <code>null</code> if not found.
971     */
972    private StyleResourceValue getStyle(String parentName,
973            Map<String, ResourceValue> inProjectStyleMap,
974            Map<String, ResourceValue> inFrameworkStyleMap) {
975        boolean frameworkOnly = false;
976
977        String name = parentName;
978
979        // remove the useless @ if it's there
980        if (name.startsWith(BridgeConstants.PREFIX_RESOURCE_REF)) {
981            name = name.substring(BridgeConstants.PREFIX_RESOURCE_REF.length());
982        }
983
984        // check for framework identifier.
985        if (name.startsWith(BridgeConstants.PREFIX_ANDROID)) {
986            frameworkOnly = true;
987            name = name.substring(BridgeConstants.PREFIX_ANDROID.length());
988        }
989
990        // at this point we could have the format <type>/<name>. we want only the name as long as
991        // the type is style.
992        if (name.startsWith(BridgeConstants.REFERENCE_STYLE)) {
993            name = name.substring(BridgeConstants.REFERENCE_STYLE.length());
994        } else if (name.indexOf('/') != -1) {
995            return null;
996        }
997
998        ResourceValue parent = null;
999
1000        // if allowed, search in the project resources.
1001        if (frameworkOnly == false && inProjectStyleMap != null) {
1002            parent = inProjectStyleMap.get(name);
1003        }
1004
1005        // if not found, then look in the framework resources.
1006        if (parent == null) {
1007            parent = inFrameworkStyleMap.get(name);
1008        }
1009
1010        // make sure the result is the proper class type and return it.
1011        if (parent instanceof StyleResourceValue) {
1012            return (StyleResourceValue)parent;
1013        }
1014
1015        assert false;
1016        mParams.getLog().error(null,
1017                String.format("Unable to resolve parent style name: %s", parentName));
1018
1019        return null;
1020    }
1021
1022    /**
1023     * Computes the name of the parent style, or <code>null</code> if the style is a root style.
1024     */
1025    private String getParentName(String styleName) {
1026        int index = styleName.lastIndexOf('.');
1027        if (index != -1) {
1028            return styleName.substring(0, index);
1029        }
1030
1031        return null;
1032    }
1033
1034    /**
1035     * Returns the top screen offset. This depends on whether the current theme defines the user
1036     * of the title and status bars.
1037     * @param frameworkResources The framework resources
1038     * @param currentTheme The current theme
1039     * @param context The context
1040     * @return the pixel height offset
1041     */
1042    private int getScreenOffset(Map<String, Map<String, ResourceValue>> frameworkResources,
1043            StyleResourceValue currentTheme, BridgeContext context) {
1044        int offset = 0;
1045
1046        // get the title bar flag from the current theme.
1047        ResourceValue value = context.findItemInStyle(currentTheme, "windowNoTitle");
1048
1049        // because it may reference something else, we resolve it.
1050        value = context.resolveResValue(value);
1051
1052        // if there's a value and it's true (default is false)
1053        if (value == null || value.getValue() == null ||
1054                XmlUtils.convertValueToBoolean(value.getValue(), false /* defValue */) == false) {
1055            // default size of the window title bar
1056            int defaultOffset = DEFAULT_TITLE_BAR_HEIGHT;
1057
1058            // get value from the theme.
1059            value = context.findItemInStyle(currentTheme, "windowTitleSize");
1060
1061            // resolve it
1062            value = context.resolveResValue(value);
1063
1064            if (value != null) {
1065                // get the numerical value, if available
1066                TypedValue typedValue = ResourceHelper.getValue(value.getValue());
1067                if (typedValue != null) {
1068                    // compute the pixel value based on the display metrics
1069                    defaultOffset = (int)typedValue.getDimension(context.getResources().mMetrics);
1070                }
1071            }
1072
1073            offset += defaultOffset;
1074        }
1075
1076        // get the fullscreen flag from the current theme.
1077        value = context.findItemInStyle(currentTheme, "windowFullscreen");
1078
1079        // because it may reference something else, we resolve it.
1080        value = context.resolveResValue(value);
1081
1082        if (value == null || value.getValue() == null ||
1083                XmlUtils.convertValueToBoolean(value.getValue(), false /* defValue */) == false) {
1084
1085            // default value
1086            int defaultOffset = DEFAULT_STATUS_BAR_HEIGHT;
1087
1088            // get the real value, first the list of Dimensions from the framework map
1089            Map<String, ResourceValue> dimens = frameworkResources.get(BridgeConstants.RES_DIMEN);
1090
1091            // now get the value
1092            value = dimens.get("status_bar_height");
1093            if (value != null) {
1094                TypedValue typedValue = ResourceHelper.getValue(value.getValue());
1095                if (typedValue != null) {
1096                    // compute the pixel value based on the display metrics
1097                    defaultOffset = (int)typedValue.getDimension(context.getResources().mMetrics);
1098                }
1099            }
1100
1101            // add the computed offset.
1102            offset += defaultOffset;
1103        }
1104
1105        return offset;
1106
1107    }
1108
1109    /**
1110     * Post process on a view hierachy that was just inflated.
1111     * <p/>At the moment this only support TabHost: If {@link TabHost} is detected, look for the
1112     * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically
1113     * based on the content of the {@link FrameLayout}.
1114     * @param view the root view to process.
1115     * @param projectCallback callback to the project.
1116     */
1117    private void postInflateProcess(View view, IProjectCallback projectCallback)
1118            throws PostInflateException {
1119        if (view instanceof TabHost) {
1120            setupTabHost((TabHost)view, projectCallback);
1121        } else if (view instanceof ViewGroup) {
1122            ViewGroup group = (ViewGroup)view;
1123            final int count = group.getChildCount();
1124            for (int c = 0 ; c < count ; c++) {
1125                View child = group.getChildAt(c);
1126                postInflateProcess(child, projectCallback);
1127            }
1128        }
1129    }
1130
1131    /**
1132     * Sets up a {@link TabHost} object.
1133     * @param tabHost the TabHost to setup.
1134     * @param projectCallback The project callback object to access the project R class.
1135     * @throws PostInflateException
1136     */
1137    private void setupTabHost(TabHost tabHost, IProjectCallback projectCallback)
1138            throws PostInflateException {
1139        // look for the TabWidget, and the FrameLayout. They have their own specific names
1140        View v = tabHost.findViewById(android.R.id.tabs);
1141
1142        if (v == null) {
1143            throw new PostInflateException(
1144                    "TabHost requires a TabWidget with id \"android:id/tabs\".\n");
1145        }
1146
1147        if ((v instanceof TabWidget) == false) {
1148            throw new PostInflateException(String.format(
1149                    "TabHost requires a TabWidget with id \"android:id/tabs\".\n" +
1150                    "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName()));
1151        }
1152
1153        v = tabHost.findViewById(android.R.id.tabcontent);
1154
1155        if (v == null) {
1156            // TODO: see if we can fake tabs even without the FrameLayout (same below when the framelayout is empty)
1157            throw new PostInflateException(
1158                    "TabHost requires a FrameLayout with id \"android:id/tabcontent\".");
1159        }
1160
1161        if ((v instanceof FrameLayout) == false) {
1162            throw new PostInflateException(String.format(
1163                    "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" +
1164                    "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName()));
1165        }
1166
1167        FrameLayout content = (FrameLayout)v;
1168
1169        // now process the content of the framelayout and dynamically create tabs for it.
1170        final int count = content.getChildCount();
1171
1172        if (count == 0) {
1173            throw new PostInflateException(
1174                    "The FrameLayout for the TabHost has no content. Rendering failed.\n");
1175        }
1176
1177        // this must be called before addTab() so that the TabHost searches its TabWidget
1178        // and FrameLayout.
1179        tabHost.setup();
1180
1181        // for each child of the framelayout, add a new TabSpec
1182        for (int i = 0 ; i < count ; i++) {
1183            View child = content.getChildAt(i);
1184            String tabSpec = String.format("tab_spec%d", i+1);
1185            int id = child.getId();
1186            String[] resource = projectCallback.resolveResourceValue(id);
1187            String name;
1188            if (resource != null) {
1189                name = resource[0]; // 0 is resource name, 1 is resource type.
1190            } else {
1191                name = String.format("Tab %d", i+1); // default name if id is unresolved.
1192            }
1193            tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id));
1194        }
1195    }
1196
1197
1198    /**
1199     * Visits a View and its children and generate a {@link ViewInfo} containing the
1200     * bounds of all the views.
1201     * @param view the root View
1202     * @param context the context.
1203     */
1204    private ViewInfo visit(View view, BridgeContext context) {
1205        if (view == null) {
1206            return null;
1207        }
1208
1209        ViewInfo result = new ViewInfo(view.getClass().getName(),
1210                context.getViewKey(view),
1211                view.getLeft(), view.getTop(), view.getRight(), view.getBottom(),
1212                view, view.getLayoutParams());
1213
1214        if (view instanceof ViewGroup) {
1215            ViewGroup group = ((ViewGroup) view);
1216            List<ViewInfo> children = new ArrayList<ViewInfo>();
1217            for (int i = 0; i < group.getChildCount(); i++) {
1218                children.add(visit(group.getChildAt(i), context));
1219            }
1220            result.setChildren(children);
1221        }
1222
1223        return result;
1224    }
1225
1226    private void invalidateRenderingSize() {
1227        mMeasuredScreenWidth = mMeasuredScreenHeight = -1;
1228    }
1229
1230    public BufferedImage getImage() {
1231        return mImage;
1232    }
1233
1234    public ViewInfo getViewInfo() {
1235        return mViewInfo;
1236    }
1237
1238    public Map<String, String> getDefaultProperties(Object viewObject) {
1239        return mContext.getDefaultPropMap(viewObject);
1240    }
1241
1242    public void setScene(RenderSession session) {
1243        mScene = session;
1244    }
1245
1246    public RenderSession getSession() {
1247        return mScene;
1248    }
1249}
1250