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