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