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