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