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