RenderSessionImpl.java revision 51a7e5447de94791c464cda5cc6ebbf616d73c80
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            return ERROR_INFLATION.createResult(t.getMessage(), t);
368        }
369    }
370
371    /**
372     * Renders the scene.
373     * <p>
374     * {@link #acquire(long)} must have been called before this.
375     *
376     * @throws IllegalStateException if the current context is different than the one owned by
377     *      the scene, or if {@link #acquire(long)} was not called.
378     *
379     * @see SceneParams#getRenderingMode()
380     * @see LayoutScene#render(long)
381     */
382    public Result render() {
383        checkLock();
384
385        try {
386            if (mViewRoot == null) {
387                return ERROR_NOT_INFLATED.createResult();
388            }
389            // measure the views
390            int w_spec, h_spec;
391
392            RenderingMode renderingMode = mParams.getRenderingMode();
393
394            // only do the screen measure when needed.
395            boolean newRenderSize = false;
396            if (mMeasuredScreenWidth == -1) {
397                newRenderSize = true;
398                mMeasuredScreenWidth = mParams.getScreenWidth();
399                mMeasuredScreenHeight = mParams.getScreenHeight();
400
401                if (renderingMode != RenderingMode.NORMAL) {
402                    // measure the full size needed by the layout.
403                    w_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenWidth,
404                            renderingMode.isHorizExpand() ?
405                                    MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
406                                    : MeasureSpec.EXACTLY);
407                    h_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenHeight - mScreenOffset,
408                            renderingMode.isVertExpand() ?
409                                    MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
410                                    : MeasureSpec.EXACTLY);
411                    mViewRoot.measure(w_spec, h_spec);
412
413                    if (renderingMode.isHorizExpand()) {
414                        int neededWidth = mViewRoot.getChildAt(0).getMeasuredWidth();
415                        if (neededWidth > mMeasuredScreenWidth) {
416                            mMeasuredScreenWidth = neededWidth;
417                        }
418                    }
419
420                    if (renderingMode.isVertExpand()) {
421                        int neededHeight = mViewRoot.getChildAt(0).getMeasuredHeight();
422                        if (neededHeight > mMeasuredScreenHeight - mScreenOffset) {
423                            mMeasuredScreenHeight = neededHeight + mScreenOffset;
424                        }
425                    }
426                }
427            }
428
429            // remeasure with the size we need
430            // This must always be done before the call to layout
431            w_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenWidth, MeasureSpec.EXACTLY);
432            h_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenHeight - mScreenOffset,
433                    MeasureSpec.EXACTLY);
434            mViewRoot.measure(w_spec, h_spec);
435
436            // now do the layout.
437            mViewRoot.layout(0, mScreenOffset, mMeasuredScreenWidth, mMeasuredScreenHeight);
438
439            // draw the views
440            // create the BufferedImage into which the layout will be rendered.
441            if (newRenderSize || mCanvas == null) {
442                if (mParams.getImageFactory() != null) {
443                    mImage = mParams.getImageFactory().getImage(mMeasuredScreenWidth,
444                            mMeasuredScreenHeight - mScreenOffset);
445                } else {
446                    mImage = new BufferedImage(mMeasuredScreenWidth,
447                            mMeasuredScreenHeight - mScreenOffset, BufferedImage.TYPE_INT_ARGB);
448                }
449
450                if (mParams.isBgColorOverridden()) {
451                    Graphics2D gc = mImage.createGraphics();
452                    gc.setColor(new Color(mParams.getOverrideBgColor(), true));
453                    gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight - mScreenOffset);
454                    gc.dispose();
455                }
456
457                // create an Android bitmap around the BufferedImage
458                Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage,
459                        true /*isMutable*/,
460                        ResourceDensity.getEnum(mParams.getDensity()));
461
462                // create a Canvas around the Android bitmap
463                mCanvas = new Canvas(bitmap);
464                mCanvas.setDensity(mParams.getDensity());
465            }
466
467            mViewRoot.draw(mCanvas);
468
469            mViewInfo = visit(((ViewGroup)mViewRoot).getChildAt(0), mContext);
470
471            // success!
472            return SUCCESS.createResult();
473        } catch (Throwable e) {
474            // get the real cause of the exception.
475            Throwable t = e;
476            while (t.getCause() != null) {
477                t = t.getCause();
478            }
479
480            return ERROR_UNKNOWN.createResult(t.getMessage(), t);
481        }
482    }
483
484    /**
485     * Animate an object
486     * <p>
487     * {@link #acquire(long)} must have been called before this.
488     *
489     * @throws IllegalStateException if the current context is different than the one owned by
490     *      the scene, or if {@link #acquire(long)} was not called.
491     *
492     * @see LayoutScene#animate(Object, String, boolean, IAnimationListener)
493     */
494    public Result animate(Object targetObject, String animationName,
495            boolean isFrameworkAnimation, IAnimationListener listener) {
496        checkLock();
497
498        // find the animation file.
499        ResourceValue animationResource = null;
500        int animationId = 0;
501        if (isFrameworkAnimation) {
502            animationResource = mContext.getFrameworkResource(BridgeConstants.RES_ANIMATOR,
503                    animationName);
504            if (animationResource != null) {
505                animationId = Bridge.getResourceValue(BridgeConstants.RES_ANIMATOR,
506                        animationName);
507            }
508        } else {
509            animationResource = mContext.getProjectResource(BridgeConstants.RES_ANIMATOR,
510                    animationName);
511            if (animationResource != null) {
512                animationId = mContext.getProjectCallback().getResourceValue(
513                        BridgeConstants.RES_ANIMATOR, 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, final 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
649            // there is no support for animating views across layouts, so in case the new and old
650            // parent views are different we fake the animation through a no animation thread.
651            if (previousParent != newParentView) {
652                new Thread("not animated moveChild") {
653                    @Override
654                    public void run() {
655                        Result result = moveView(previousParent, newParentView, childView, index,
656                                params);
657                        if (result.isSuccess() == false) {
658                            listener.done(result);
659                        }
660
661                        // ready to do the work, acquire the scene.
662                        result = acquire(250);
663                        if (result.isSuccess() == false) {
664                            listener.done(result);
665                            return;
666                        }
667
668                        try {
669                            result = render();
670                            if (result.isSuccess()) {
671                                listener.onNewFrame(RenderSessionImpl.this.getSession());
672                            }
673                        } finally {
674                            release();
675                        }
676
677                        listener.done(result);
678                    }
679                }.start();
680            } else {
681                new AnimationThread(this, "moveChild", listener) {
682
683                    @Override
684                    public Result preAnimation() {
685                        // set up the transition for the parent.
686                        LayoutTransition transition = new LayoutTransition();
687                        previousParent.setLayoutTransition(transition);
688
689                        // tweak the animation durations and start delays (to match the duration of
690                        // animation playing just before).
691                        // Note: Cannot user Animation.setDuration() directly. Have to set it
692                        // on the LayoutTransition.
693                        transition.setDuration(LayoutTransition.DISAPPEARING, 100);
694                        // CHANGE_DISAPPEARING plays after DISAPPEARING
695                        transition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 100);
696
697                        transition.setDuration(LayoutTransition.CHANGE_DISAPPEARING, 100);
698
699                        transition.setDuration(LayoutTransition.CHANGE_APPEARING, 100);
700                        // CHANGE_APPEARING plays after CHANGE_APPEARING
701                        transition.setStartDelay(LayoutTransition.APPEARING, 100);
702
703                        transition.setDuration(LayoutTransition.APPEARING, 100);
704
705                        return moveView(previousParent, newParentView, childView, index, params);
706                    }
707
708                    @Override
709                    public void postAnimation() {
710                        previousParent.setLayoutTransition(null);
711                        newParentView.setLayoutTransition(null);
712                    }
713                }.start();
714            }
715
716            // always return success since the real status will come through the listener.
717            return SUCCESS.createResult(layoutParams);
718        }
719
720        Result result = moveView(previousParent, newParentView, childView, index, layoutParams);
721        if (result.isSuccess() == false) {
722            return result;
723        }
724
725        result = render();
726        if (layoutParams != null && result.isSuccess()) {
727            result = result.getCopyWithData(layoutParams);
728        }
729
730        return result;
731    }
732
733    /**
734     * Moves a View from its current parent to a new given parent at a new given location, with
735     * an optional new {@link LayoutParams} instance
736     *
737     * @param previousParent the previous parent, still owning the child at the time of the call.
738     * @param newParent the new parent
739     * @param movedView the view to move
740     * @param index the new location in the new parent
741     * @param params an option (can be null) {@link LayoutParams} instance.
742     *
743     * @return a Result with {@link Status#SUCCESS} or
744     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
745     *     adding views.
746     */
747    private Result moveView(ViewGroup previousParent, final ViewGroup newParent,
748            final View movedView, final int index, final LayoutParams params) {
749        try {
750            // check if there is a transition on the previousParent.
751            LayoutTransition previousTransition = previousParent.getLayoutTransition();
752            if (previousTransition != null) {
753                // in this case there is an animation. This means we have to wait for the child's
754                // parent reference to be null'ed out so that we can add it to the new parent.
755                // It is technically removed right before the DISAPPEARING animation is done (if
756                // the animation of this type is not null, otherwise it's after which is impossible
757                // to handle).
758                // Because there is no move animation, if the new parent is the same as the old
759                // parent, we need to wait until the CHANGE_DISAPPEARING animation is done before
760                // adding the child or the child will appear in its new location before the
761                // other children have made room for it.
762
763                // add a listener to the transition to be notified of the actual removal.
764                previousTransition.addTransitionListener(new TransitionListener() {
765                    private int mChangeDisappearingCount = 0;
766
767                    public void startTransition(LayoutTransition transition, ViewGroup container,
768                            View view, int transitionType) {
769                        if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) {
770                            mChangeDisappearingCount++;
771                        }
772                    }
773
774                    public void endTransition(LayoutTransition transition, ViewGroup container,
775                            View view, int transitionType) {
776                        if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) {
777                            mChangeDisappearingCount--;
778                        }
779
780                        if (transitionType == LayoutTransition.CHANGE_DISAPPEARING &&
781                                mChangeDisappearingCount == 0) {
782                            // add it to the parentView in the correct location
783                            if (params != null) {
784                                newParent.addView(movedView, index, params);
785                            } else {
786                                newParent.addView(movedView, index);
787                            }
788                        }
789                    }
790                });
791
792                // remove the view from the current parent.
793                previousParent.removeView(movedView);
794
795                // and return since adding the view to the new parent is done in the listener.
796                return SUCCESS.createResult();
797            } else {
798                // standard code with no animation. pretty simple.
799                previousParent.removeView(movedView);
800
801                // add it to the parentView in the correct location
802                if (params != null) {
803                    newParent.addView(movedView, index, params);
804                } else {
805                    newParent.addView(movedView, index);
806                }
807
808                return SUCCESS.createResult();
809            }
810        } catch (UnsupportedOperationException e) {
811            // looks like this is a view class that doesn't support children manipulation!
812            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
813        }
814    }
815
816    /**
817     * Removes a child from its current parent.
818     * <p>
819     * {@link #acquire(long)} must have been called before this.
820     *
821     * @throws IllegalStateException if the current context is different than the one owned by
822     *      the scene, or if {@link #acquire(long)} was not called.
823     *
824     * @see LayoutScene#removeChild(Object, IAnimationListener)
825     */
826    public Result removeChild(final View childView, IAnimationListener listener) {
827        checkLock();
828
829        invalidateRenderingSize();
830
831        final ViewGroup parent = (ViewGroup) childView.getParent();
832
833        if (listener != null) {
834            new AnimationThread(this, "moveChild", listener) {
835
836                @Override
837                public Result preAnimation() {
838                    parent.setLayoutTransition(new LayoutTransition());
839                    return removeView(parent, childView);
840                }
841
842                @Override
843                public void postAnimation() {
844                    parent.setLayoutTransition(null);
845                }
846            }.start();
847
848            // always return success since the real status will come through the listener.
849            return SUCCESS.createResult();
850        }
851
852        Result result = removeView(parent, childView);
853        if (result.isSuccess() == false) {
854            return result;
855        }
856
857        return render();
858    }
859
860    /**
861     * Removes a given view from its current parent.
862     *
863     * @param view the view to remove from its parent
864     *
865     * @return a Result with {@link Status#SUCCESS} or
866     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
867     *     adding views.
868     */
869    private Result removeView(ViewGroup parent, View view) {
870        try {
871            parent.removeView(view);
872            return SUCCESS.createResult();
873        } catch (UnsupportedOperationException e) {
874            // looks like this is a view class that doesn't support children manipulation!
875            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
876        }
877    }
878
879    /**
880     * Returns the log associated with the session.
881     * @return the log or null if there are none.
882     */
883    public LayoutLog getLog() {
884        if (mParams != null) {
885            return mParams.getLog();
886        }
887
888        return null;
889    }
890
891    /**
892     * Checks that the lock is owned by the current thread and that the current context is the one
893     * from this scene.
894     *
895     * @throws IllegalStateException if the current context is different than the one owned by
896     *      the scene, or if {@link #acquire(long)} was not called.
897     */
898    private void checkLock() {
899        ReentrantLock lock = Bridge.getLock();
900        if (lock.isHeldByCurrentThread() == false) {
901            throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
902        }
903        if (sCurrentContext != mContext) {
904            throw new IllegalStateException("Thread acquired a scene but is rendering a different one");
905        }
906    }
907
908
909    /**
910     * Compute style information from the given list of style for the project and framework.
911     * @param themeName the name of the current theme.  In order to differentiate project and
912     * platform themes sharing the same name, all project themes must be prepended with
913     * a '*' character.
914     * @param isProjectTheme Is this a project theme
915     * @param inProjectStyleMap the project style map
916     * @param inFrameworkStyleMap the framework style map
917     * @param outInheritanceMap the map of style inheritance. This is filled by the method
918     * @return the {@link StyleResourceValue} matching <var>themeName</var>
919     */
920    private StyleResourceValue computeStyleMaps(
921            String themeName, boolean isProjectTheme, Map<String,
922            ResourceValue> inProjectStyleMap, Map<String, ResourceValue> inFrameworkStyleMap,
923            Map<StyleResourceValue, StyleResourceValue> outInheritanceMap) {
924
925        if (inProjectStyleMap != null && inFrameworkStyleMap != null) {
926            // first, get the theme
927            ResourceValue theme = null;
928
929            // project theme names have been prepended with a *
930            if (isProjectTheme) {
931                theme = inProjectStyleMap.get(themeName);
932            } else {
933                theme = inFrameworkStyleMap.get(themeName);
934            }
935
936            if (theme instanceof StyleResourceValue) {
937                // compute the inheritance map for both the project and framework styles
938                computeStyleInheritance(inProjectStyleMap.values(), inProjectStyleMap,
939                        inFrameworkStyleMap, outInheritanceMap);
940
941                // Compute the style inheritance for the framework styles/themes.
942                // Since, for those, the style parent values do not contain 'android:'
943                // we want to force looking in the framework style only to avoid using
944                // similarly named styles from the project.
945                // To do this, we pass null in lieu of the project style map.
946                computeStyleInheritance(inFrameworkStyleMap.values(), null /*inProjectStyleMap */,
947                        inFrameworkStyleMap, outInheritanceMap);
948
949                return (StyleResourceValue)theme;
950            }
951        }
952
953        return null;
954    }
955
956    /**
957     * Compute the parent style for all the styles in a given list.
958     * @param styles the styles for which we compute the parent.
959     * @param inProjectStyleMap the map of project styles.
960     * @param inFrameworkStyleMap the map of framework styles.
961     * @param outInheritanceMap the map of style inheritance. This is filled by the method.
962     */
963    private void computeStyleInheritance(Collection<ResourceValue> styles,
964            Map<String, ResourceValue> inProjectStyleMap,
965            Map<String, ResourceValue> inFrameworkStyleMap,
966            Map<StyleResourceValue, StyleResourceValue> outInheritanceMap) {
967        for (ResourceValue value : styles) {
968            if (value instanceof StyleResourceValue) {
969                StyleResourceValue style = (StyleResourceValue)value;
970                StyleResourceValue parentStyle = null;
971
972                // first look for a specified parent.
973                String parentName = style.getParentStyle();
974
975                // no specified parent? try to infer it from the name of the style.
976                if (parentName == null) {
977                    parentName = getParentName(value.getName());
978                }
979
980                if (parentName != null) {
981                    parentStyle = getStyle(parentName, inProjectStyleMap, inFrameworkStyleMap);
982
983                    if (parentStyle != null) {
984                        outInheritanceMap.put(style, parentStyle);
985                    }
986                }
987            }
988        }
989    }
990
991    /**
992     * Searches for and returns the {@link StyleResourceValue} from a given name.
993     * <p/>The format of the name can be:
994     * <ul>
995     * <li>[android:]&lt;name&gt;</li>
996     * <li>[android:]style/&lt;name&gt;</li>
997     * <li>@[android:]style/&lt;name&gt;</li>
998     * </ul>
999     * @param parentName the name of the style.
1000     * @param inProjectStyleMap the project style map. Can be <code>null</code>
1001     * @param inFrameworkStyleMap the framework style map.
1002     * @return The matching {@link StyleResourceValue} object or <code>null</code> if not found.
1003     */
1004    private StyleResourceValue getStyle(String parentName,
1005            Map<String, ResourceValue> inProjectStyleMap,
1006            Map<String, ResourceValue> inFrameworkStyleMap) {
1007        boolean frameworkOnly = false;
1008
1009        String name = parentName;
1010
1011        // remove the useless @ if it's there
1012        if (name.startsWith(BridgeConstants.PREFIX_RESOURCE_REF)) {
1013            name = name.substring(BridgeConstants.PREFIX_RESOURCE_REF.length());
1014        }
1015
1016        // check for framework identifier.
1017        if (name.startsWith(BridgeConstants.PREFIX_ANDROID)) {
1018            frameworkOnly = true;
1019            name = name.substring(BridgeConstants.PREFIX_ANDROID.length());
1020        }
1021
1022        // at this point we could have the format <type>/<name>. we want only the name as long as
1023        // the type is style.
1024        if (name.startsWith(BridgeConstants.REFERENCE_STYLE)) {
1025            name = name.substring(BridgeConstants.REFERENCE_STYLE.length());
1026        } else if (name.indexOf('/') != -1) {
1027            return null;
1028        }
1029
1030        ResourceValue parent = null;
1031
1032        // if allowed, search in the project resources.
1033        if (frameworkOnly == false && inProjectStyleMap != null) {
1034            parent = inProjectStyleMap.get(name);
1035        }
1036
1037        // if not found, then look in the framework resources.
1038        if (parent == null) {
1039            parent = inFrameworkStyleMap.get(name);
1040        }
1041
1042        // make sure the result is the proper class type and return it.
1043        if (parent instanceof StyleResourceValue) {
1044            return (StyleResourceValue)parent;
1045        }
1046
1047        assert false;
1048        mParams.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE,
1049                String.format("Unable to resolve parent style name: %s", parentName),
1050                null /*data*/);
1051
1052        return null;
1053    }
1054
1055    /**
1056     * Computes the name of the parent style, or <code>null</code> if the style is a root style.
1057     */
1058    private String getParentName(String styleName) {
1059        int index = styleName.lastIndexOf('.');
1060        if (index != -1) {
1061            return styleName.substring(0, index);
1062        }
1063
1064        return null;
1065    }
1066
1067    /**
1068     * Returns the top screen offset. This depends on whether the current theme defines the user
1069     * of the title and status bars.
1070     * @param frameworkResources The framework resources
1071     * @param currentTheme The current theme
1072     * @param context The context
1073     * @return the pixel height offset
1074     */
1075    private int getScreenOffset(Map<String, Map<String, ResourceValue>> frameworkResources,
1076            StyleResourceValue currentTheme, BridgeContext context) {
1077        int offset = 0;
1078
1079        // get the title bar flag from the current theme.
1080        ResourceValue value = context.findItemInStyle(currentTheme, "windowNoTitle");
1081
1082        // because it may reference something else, we resolve it.
1083        value = context.resolveResValue(value);
1084
1085        // if there's a value and it's true (default is false)
1086        if (value == null || value.getValue() == null ||
1087                XmlUtils.convertValueToBoolean(value.getValue(), false /* defValue */) == false) {
1088            // default size of the window title bar
1089            int defaultOffset = DEFAULT_TITLE_BAR_HEIGHT;
1090
1091            // get value from the theme.
1092            value = context.findItemInStyle(currentTheme, "windowTitleSize");
1093
1094            // resolve it
1095            value = context.resolveResValue(value);
1096
1097            if (value != null) {
1098                // get the numerical value, if available
1099                TypedValue typedValue = ResourceHelper.getValue(value.getValue());
1100                if (typedValue != null) {
1101                    // compute the pixel value based on the display metrics
1102                    defaultOffset = (int)typedValue.getDimension(context.getResources().mMetrics);
1103                }
1104            }
1105
1106            offset += defaultOffset;
1107        }
1108
1109        // get the fullscreen flag from the current theme.
1110        value = context.findItemInStyle(currentTheme, "windowFullscreen");
1111
1112        // because it may reference something else, we resolve it.
1113        value = context.resolveResValue(value);
1114
1115        if (value == null || value.getValue() == null ||
1116                XmlUtils.convertValueToBoolean(value.getValue(), false /* defValue */) == false) {
1117
1118            // default value
1119            int defaultOffset = DEFAULT_STATUS_BAR_HEIGHT;
1120
1121            // get the real value, first the list of Dimensions from the framework map
1122            Map<String, ResourceValue> dimens = frameworkResources.get(BridgeConstants.RES_DIMEN);
1123
1124            // now get the value
1125            value = dimens.get("status_bar_height");
1126            if (value != null) {
1127                TypedValue typedValue = ResourceHelper.getValue(value.getValue());
1128                if (typedValue != null) {
1129                    // compute the pixel value based on the display metrics
1130                    defaultOffset = (int)typedValue.getDimension(context.getResources().mMetrics);
1131                }
1132            }
1133
1134            // add the computed offset.
1135            offset += defaultOffset;
1136        }
1137
1138        return offset;
1139
1140    }
1141
1142    /**
1143     * Post process on a view hierachy that was just inflated.
1144     * <p/>At the moment this only support TabHost: If {@link TabHost} is detected, look for the
1145     * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically
1146     * based on the content of the {@link FrameLayout}.
1147     * @param view the root view to process.
1148     * @param projectCallback callback to the project.
1149     */
1150    private void postInflateProcess(View view, IProjectCallback projectCallback)
1151            throws PostInflateException {
1152        if (view instanceof TabHost) {
1153            setupTabHost((TabHost)view, projectCallback);
1154        } else if (view instanceof ViewGroup) {
1155            ViewGroup group = (ViewGroup)view;
1156            final int count = group.getChildCount();
1157            for (int c = 0 ; c < count ; c++) {
1158                View child = group.getChildAt(c);
1159                postInflateProcess(child, projectCallback);
1160            }
1161        }
1162    }
1163
1164    /**
1165     * Sets up a {@link TabHost} object.
1166     * @param tabHost the TabHost to setup.
1167     * @param projectCallback The project callback object to access the project R class.
1168     * @throws PostInflateException
1169     */
1170    private void setupTabHost(TabHost tabHost, IProjectCallback projectCallback)
1171            throws PostInflateException {
1172        // look for the TabWidget, and the FrameLayout. They have their own specific names
1173        View v = tabHost.findViewById(android.R.id.tabs);
1174
1175        if (v == null) {
1176            throw new PostInflateException(
1177                    "TabHost requires a TabWidget with id \"android:id/tabs\".\n");
1178        }
1179
1180        if ((v instanceof TabWidget) == false) {
1181            throw new PostInflateException(String.format(
1182                    "TabHost requires a TabWidget with id \"android:id/tabs\".\n" +
1183                    "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName()));
1184        }
1185
1186        v = tabHost.findViewById(android.R.id.tabcontent);
1187
1188        if (v == null) {
1189            // TODO: see if we can fake tabs even without the FrameLayout (same below when the framelayout is empty)
1190            throw new PostInflateException(
1191                    "TabHost requires a FrameLayout with id \"android:id/tabcontent\".");
1192        }
1193
1194        if ((v instanceof FrameLayout) == false) {
1195            throw new PostInflateException(String.format(
1196                    "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" +
1197                    "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName()));
1198        }
1199
1200        FrameLayout content = (FrameLayout)v;
1201
1202        // now process the content of the framelayout and dynamically create tabs for it.
1203        final int count = content.getChildCount();
1204
1205        if (count == 0) {
1206            throw new PostInflateException(
1207                    "The FrameLayout for the TabHost has no content. Rendering failed.\n");
1208        }
1209
1210        // this must be called before addTab() so that the TabHost searches its TabWidget
1211        // and FrameLayout.
1212        tabHost.setup();
1213
1214        // for each child of the framelayout, add a new TabSpec
1215        for (int i = 0 ; i < count ; i++) {
1216            View child = content.getChildAt(i);
1217            String tabSpec = String.format("tab_spec%d", i+1);
1218            int id = child.getId();
1219            String[] resource = projectCallback.resolveResourceValue(id);
1220            String name;
1221            if (resource != null) {
1222                name = resource[0]; // 0 is resource name, 1 is resource type.
1223            } else {
1224                name = String.format("Tab %d", i+1); // default name if id is unresolved.
1225            }
1226            tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id));
1227        }
1228    }
1229
1230
1231    /**
1232     * Visits a View and its children and generate a {@link ViewInfo} containing the
1233     * bounds of all the views.
1234     * @param view the root View
1235     * @param context the context.
1236     */
1237    private ViewInfo visit(View view, BridgeContext context) {
1238        if (view == null) {
1239            return null;
1240        }
1241
1242        ViewInfo result = new ViewInfo(view.getClass().getName(),
1243                context.getViewKey(view),
1244                view.getLeft(), view.getTop(), view.getRight(), view.getBottom(),
1245                view, view.getLayoutParams());
1246
1247        if (view instanceof ViewGroup) {
1248            ViewGroup group = ((ViewGroup) view);
1249            List<ViewInfo> children = new ArrayList<ViewInfo>();
1250            for (int i = 0; i < group.getChildCount(); i++) {
1251                children.add(visit(group.getChildAt(i), context));
1252            }
1253            result.setChildren(children);
1254        }
1255
1256        return result;
1257    }
1258
1259    private void invalidateRenderingSize() {
1260        mMeasuredScreenWidth = mMeasuredScreenHeight = -1;
1261    }
1262
1263    public BufferedImage getImage() {
1264        return mImage;
1265    }
1266
1267    public ViewInfo getViewInfo() {
1268        return mViewInfo;
1269    }
1270
1271    public Map<String, String> getDefaultProperties(Object viewObject) {
1272        return mContext.getDefaultPropMap(viewObject);
1273    }
1274
1275    public void setScene(RenderSession session) {
1276        mScene = session;
1277    }
1278
1279    public RenderSession getSession() {
1280        return mScene;
1281    }
1282}
1283