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