RenderSessionImpl.java revision bbbb8326020368958a3f1d248878329e9d6b10c0
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.RenderParams;
33import com.android.ide.common.rendering.api.RenderResources;
34import com.android.ide.common.rendering.api.RenderSession;
35import com.android.ide.common.rendering.api.ResourceValue;
36import com.android.ide.common.rendering.api.Result;
37import com.android.ide.common.rendering.api.ViewInfo;
38import com.android.ide.common.rendering.api.RenderParams.RenderingMode;
39import com.android.ide.common.rendering.api.RenderResources.FrameworkResourceIdProvider;
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.android.BridgeContext;
44import com.android.layoutlib.bridge.android.BridgeInflater;
45import com.android.layoutlib.bridge.android.BridgeLayoutParamsMapAttributes;
46import com.android.layoutlib.bridge.android.BridgeWindow;
47import com.android.layoutlib.bridge.android.BridgeWindowSession;
48import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
49import com.android.layoutlib.bridge.bars.FakeActionBar;
50import com.android.layoutlib.bridge.bars.PhoneSystemBar;
51import com.android.layoutlib.bridge.bars.TabletSystemBar;
52import com.android.layoutlib.bridge.bars.TitleBar;
53import com.android.resources.ResourceType;
54import com.android.resources.ScreenSize;
55import com.android.util.Pair;
56
57import org.xmlpull.v1.XmlPullParserException;
58
59import android.animation.Animator;
60import android.animation.AnimatorInflater;
61import android.animation.LayoutTransition;
62import android.animation.LayoutTransition.TransitionListener;
63import android.app.Fragment_Delegate;
64import android.graphics.Bitmap;
65import android.graphics.Bitmap_Delegate;
66import android.graphics.Canvas;
67import android.graphics.drawable.Drawable;
68import android.os.Handler;
69import android.util.DisplayMetrics;
70import android.util.TypedValue;
71import android.view.View;
72import android.view.ViewGroup;
73import android.view.View.AttachInfo;
74import android.view.View.MeasureSpec;
75import android.view.ViewGroup.LayoutParams;
76import android.widget.FrameLayout;
77import android.widget.LinearLayout;
78import android.widget.QuickContactBadge;
79import android.widget.TabHost;
80import android.widget.TabWidget;
81import android.widget.TabHost.TabSpec;
82
83import java.awt.AlphaComposite;
84import java.awt.Color;
85import java.awt.Graphics2D;
86import java.awt.image.BufferedImage;
87import java.util.ArrayList;
88import java.util.List;
89import java.util.Map;
90import java.util.concurrent.TimeUnit;
91import java.util.concurrent.locks.ReentrantLock;
92
93/**
94 * Class implementing the render session.
95 *
96 * A session is a stateful representation of a layout file. It is initialized with data coming
97 * through the {@link Bridge} API to inflate the layout. Further actions and rendering can then
98 * be done on the layout.
99 *
100 */
101public class RenderSessionImpl extends FrameworkResourceIdProvider {
102
103    private static final int DEFAULT_TITLE_BAR_HEIGHT = 25;
104    private static final int DEFAULT_STATUS_BAR_HEIGHT = 25;
105
106    /**
107     * The current context being rendered. This is set through {@link #acquire(long)} and
108     * {@link #init(long)}, and unset in {@link #release()}.
109     */
110    private static BridgeContext sCurrentContext = null;
111
112    private final RenderParams mParams;
113
114    // scene state
115    private RenderSession mScene;
116    private BridgeContext mContext;
117    private BridgeXmlBlockParser mBlockParser;
118    private BridgeInflater mInflater;
119    private ResourceValue mWindowBackground;
120    private ViewGroup mViewRoot;
121    private FrameLayout mContentRoot;
122    private Canvas mCanvas;
123    private int mMeasuredScreenWidth = -1;
124    private int mMeasuredScreenHeight = -1;
125    private boolean mIsAlphaChannelImage;
126    private boolean mWindowIsFloating;
127
128    private int mStatusBarSize;
129    private int mSystemBarSize;
130    private int mTitleBarSize;
131    private int mActionBarSize;
132
133
134    // information being returned through the API
135    private BufferedImage mImage;
136    private List<ViewInfo> mViewInfoList;
137
138    private static final class PostInflateException extends Exception {
139        private static final long serialVersionUID = 1L;
140
141        public PostInflateException(String message) {
142            super(message);
143        }
144    }
145
146    /**
147     * Creates a layout scene with all the information coming from the layout bridge API.
148     * <p>
149     * This <b>must</b> be followed by a call to {@link RenderSessionImpl#init()}, which act as a
150     * call to {@link RenderSessionImpl#acquire(long)}
151     *
152     * @see LayoutBridge#createScene(com.android.layoutlib.api.SceneParams)
153     */
154    public RenderSessionImpl(RenderParams params) {
155        // copy the params.
156        mParams = new RenderParams(params);
157    }
158
159    /**
160     * Initializes and acquires the scene, creating various Android objects such as context,
161     * inflater, and parser.
162     *
163     * @param timeout the time to wait if another rendering is happening.
164     *
165     * @return whether the scene was prepared
166     *
167     * @see #acquire(long)
168     * @see #release()
169     */
170    public Result init(long timeout) {
171        // acquire the lock. if the result is null, lock was just acquired, otherwise, return
172        // the result.
173        Result result = acquireLock(timeout);
174        if (result != null) {
175            return result;
176        }
177
178        // setup the display Metrics.
179        DisplayMetrics metrics = new DisplayMetrics();
180        metrics.densityDpi = mParams.getDensity().getDpiValue();
181        metrics.density = metrics.densityDpi / (float) DisplayMetrics.DENSITY_DEFAULT;
182        metrics.scaledDensity = metrics.density;
183        metrics.widthPixels = mParams.getScreenWidth();
184        metrics.heightPixels = mParams.getScreenHeight();
185        metrics.xdpi = mParams.getXdpi();
186        metrics.ydpi = mParams.getYdpi();
187
188        RenderResources resources = mParams.getResources();
189
190        // build the context
191        mContext = new BridgeContext(mParams.getProjectKey(), metrics, resources,
192                mParams.getProjectCallback(), mParams.getTargetSdkVersion());
193
194        // use default of true in case it's not found to use alpha by default
195        mIsAlphaChannelImage  = getBooleanThemeValue(resources,
196                "windowIsFloating", true /*defaultValue*/);
197
198        mWindowIsFloating = getBooleanThemeValue(resources, "windowIsFloating",
199                true /*defaultValue*/);
200
201        setUp();
202
203        findBackground(resources);
204        findStatusBar(resources, metrics);
205        findActionBar(resources, metrics);
206        findSystemBar(resources, metrics);
207
208        // build the inflater and parser.
209        mInflater = new BridgeInflater(mContext, mParams.getProjectCallback());
210        mContext.setBridgeInflater(mInflater);
211        mInflater.setFactory2(mContext);
212
213        mBlockParser = new BridgeXmlBlockParser(mParams.getLayoutDescription(),
214                mContext, false /* platformResourceFlag */);
215
216        return SUCCESS.createResult();
217    }
218
219    /**
220     * Prepares the scene for action.
221     * <p>
222     * This call is blocking if another rendering/inflating is currently happening, and will return
223     * whether the preparation worked.
224     *
225     * The preparation can fail if another rendering took too long and the timeout was elapsed.
226     *
227     * More than one call to this from the same thread will have no effect and will return
228     * {@link Result#SUCCESS}.
229     *
230     * After scene actions have taken place, only one call to {@link #release()} must be
231     * done.
232     *
233     * @param timeout the time to wait if another rendering is happening.
234     *
235     * @return whether the scene was prepared
236     *
237     * @see #release()
238     *
239     * @throws IllegalStateException if {@link #init(long)} was never called.
240     */
241    public Result acquire(long timeout) {
242        if (mContext == null) {
243            throw new IllegalStateException("After scene creation, #init() must be called");
244        }
245
246        // acquire the lock. if the result is null, lock was just acquired, otherwise, return
247        // the result.
248        Result result = acquireLock(timeout);
249        if (result != null) {
250            return result;
251        }
252
253        setUp();
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            tearDown();
307            lock.unlock();
308        }
309    }
310
311    /**
312     * Sets up the session for rendering.
313     * <p/>
314     * The counterpart is {@link #tearDown()}.
315     */
316    private void setUp() {
317        // make sure the Resources object references the context (and other objects) for this
318        // scene
319        mContext.initResources();
320        sCurrentContext = mContext;
321
322        LayoutLog currentLog = mParams.getLog();
323        Bridge.setLog(currentLog);
324        mContext.getRenderResources().setFrameworkResourceIdProvider(this);
325        mContext.getRenderResources().setLogger(currentLog);
326    }
327
328    /**
329     * Tear down the session after rendering.
330     * <p/>
331     * The counterpart is {@link #setUp()}.
332     */
333    private void tearDown() {
334        // Make sure to remove static references, otherwise we could not unload the lib
335        mContext.disposeResources();
336        sCurrentContext = null;
337
338        Bridge.setLog(null);
339        mContext.getRenderResources().setFrameworkResourceIdProvider(null);
340        mContext.getRenderResources().setLogger(null);
341    }
342
343    public static BridgeContext getCurrentContext() {
344        return sCurrentContext;
345    }
346
347    /**
348     * Inflates the layout.
349     * <p>
350     * {@link #acquire(long)} must have been called before this.
351     *
352     * @throws IllegalStateException if the current context is different than the one owned by
353     *      the scene, or if {@link #init(long)} was not called.
354     */
355    public Result inflate() {
356        checkLock();
357
358        try {
359
360            if (mWindowIsFloating || mParams.isForceNoDecor()) {
361                mViewRoot = mContentRoot = new FrameLayout(mContext);
362            } else {
363                /*
364                 * we're creating the following layout
365                 *
366                   +-------------------------------------------------+
367                   | System bar (only in phone UI)                   |
368                   +-------------------------------------------------+
369                   | Title/Action bar (optional)                     |
370                   +-------------------------------------------------+
371                   | Content, vertical extending                     |
372                   |                                                 |
373                   +-------------------------------------------------+
374                   | System bar (only in tablet UI)                  |
375                   +-------------------------------------------------+
376
377                 */
378
379                LinearLayout topLayout = new LinearLayout(mContext);
380                mViewRoot = topLayout;
381                topLayout.setOrientation(LinearLayout.VERTICAL);
382
383                if (mStatusBarSize > 0) {
384                    // system bar
385                    try {
386                        PhoneSystemBar systemBar = new PhoneSystemBar(mContext,
387                                mParams.getDensity());
388                        systemBar.setLayoutParams(
389                                new LinearLayout.LayoutParams(
390                                        LayoutParams.MATCH_PARENT, mStatusBarSize));
391                        topLayout.addView(systemBar);
392                    } catch (XmlPullParserException e) {
393
394                    }
395                }
396
397                // if the theme says no title/action bar, then the size will be 0
398                if (mActionBarSize > 0) {
399                    try {
400                        FakeActionBar actionBar = new FakeActionBar(mContext,
401                                mParams.getDensity(),
402                                mParams.getAppLabel(), mParams.getAppIcon());
403                        actionBar.setLayoutParams(
404                                new LinearLayout.LayoutParams(
405                                        LayoutParams.MATCH_PARENT, mActionBarSize));
406                        topLayout.addView(actionBar);
407                    } catch (XmlPullParserException e) {
408
409                    }
410                } else if (mTitleBarSize > 0) {
411                    try {
412                        TitleBar titleBar = new TitleBar(mContext,
413                                mParams.getDensity(), mParams.getAppLabel());
414                        titleBar.setLayoutParams(
415                                new LinearLayout.LayoutParams(
416                                        LayoutParams.MATCH_PARENT, mTitleBarSize));
417                        topLayout.addView(titleBar);
418                    } catch (XmlPullParserException e) {
419
420                    }
421                }
422
423
424                // content frame
425                mContentRoot = new FrameLayout(mContext);
426                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
427                        LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
428                params.weight = 1;
429                mContentRoot.setLayoutParams(params);
430                topLayout.addView(mContentRoot);
431
432                if (mSystemBarSize > 0) {
433                    // system bar
434                    try {
435                        TabletSystemBar systemBar = new TabletSystemBar(mContext,
436                                mParams.getDensity());
437                        systemBar.setLayoutParams(
438                                new LinearLayout.LayoutParams(
439                                        LayoutParams.MATCH_PARENT, mSystemBarSize));
440                        topLayout.addView(systemBar);
441                    } catch (XmlPullParserException e) {
442
443                    }
444                }
445
446            }
447
448
449            // Sets the project callback (custom view loader) to the fragment delegate so that
450            // it can instantiate the custom Fragment.
451            Fragment_Delegate.setProjectCallback(mParams.getProjectCallback());
452
453            View view = mInflater.inflate(mBlockParser, mContentRoot);
454
455            Fragment_Delegate.setProjectCallback(null);
456
457            // set the AttachInfo on the root view.
458            AttachInfo info = new AttachInfo(new BridgeWindowSession(), new BridgeWindow(),
459                    new Handler(), null);
460            info.mHasWindowFocus = true;
461            info.mWindowVisibility = View.VISIBLE;
462            info.mInTouchMode = false; // this is so that we can display selections.
463            info.mHardwareAccelerated = false;
464            mViewRoot.dispatchAttachedToWindow(info, 0);
465
466            // post-inflate process. For now this supports TabHost/TabWidget
467            postInflateProcess(view, mParams.getProjectCallback());
468
469            // get the background drawable
470            if (mWindowBackground != null) {
471                Drawable d = ResourceHelper.getDrawable(mWindowBackground, mContext);
472                mContentRoot.setBackgroundDrawable(d);
473            }
474
475            return SUCCESS.createResult();
476        } catch (PostInflateException e) {
477            return ERROR_INFLATION.createResult(e.getMessage(), e);
478        } catch (Throwable e) {
479            // get the real cause of the exception.
480            Throwable t = e;
481            while (t.getCause() != null) {
482                t = t.getCause();
483            }
484
485            return ERROR_INFLATION.createResult(t.getMessage(), t);
486        }
487    }
488
489    /**
490     * Renders the scene.
491     * <p>
492     * {@link #acquire(long)} must have been called before this.
493     *
494     * @param freshRender whether the render is a new one and should erase the existing bitmap (in
495     *      the case where bitmaps are reused). This is typically needed when not playing
496     *      animations.)
497     *
498     * @throws IllegalStateException if the current context is different than the one owned by
499     *      the scene, or if {@link #acquire(long)} was not called.
500     *
501     * @see RenderParams#getRenderingMode()
502     * @see RenderSession#render(long)
503     */
504    public Result render(boolean freshRender) {
505        checkLock();
506
507        try {
508            if (mViewRoot == null) {
509                return ERROR_NOT_INFLATED.createResult();
510            }
511            // measure the views
512            int w_spec, h_spec;
513
514            RenderingMode renderingMode = mParams.getRenderingMode();
515
516            // only do the screen measure when needed.
517            boolean newRenderSize = false;
518            if (mMeasuredScreenWidth == -1) {
519                newRenderSize = true;
520                mMeasuredScreenWidth = mParams.getScreenWidth();
521                mMeasuredScreenHeight = mParams.getScreenHeight();
522
523                if (renderingMode != RenderingMode.NORMAL) {
524                    // measure the full size needed by the layout.
525                    w_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenWidth,
526                            renderingMode.isHorizExpand() ?
527                                    MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
528                                    : MeasureSpec.EXACTLY);
529                    h_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenHeight,
530                            renderingMode.isVertExpand() ?
531                                    MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
532                                    : MeasureSpec.EXACTLY);
533                    mViewRoot.measure(w_spec, h_spec);
534
535                    if (renderingMode.isHorizExpand()) {
536                        int neededWidth = mViewRoot.getChildAt(0).getMeasuredWidth();
537                        if (neededWidth > mMeasuredScreenWidth) {
538                            mMeasuredScreenWidth = neededWidth;
539                        }
540                    }
541
542                    if (renderingMode.isVertExpand()) {
543                        int neededHeight = mViewRoot.getChildAt(0).getMeasuredHeight();
544                        if (neededHeight > mMeasuredScreenHeight) {
545                            mMeasuredScreenHeight = neededHeight;
546                        }
547                    }
548                }
549            }
550
551            // remeasure with the size we need
552            // This must always be done before the call to layout
553            w_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenWidth, MeasureSpec.EXACTLY);
554            h_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenHeight, MeasureSpec.EXACTLY);
555            mViewRoot.measure(w_spec, h_spec);
556
557            // now do the layout.
558            mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight);
559
560            mViewRoot.mAttachInfo.mTreeObserver.dispatchOnPreDraw();
561
562            // draw the views
563            // create the BufferedImage into which the layout will be rendered.
564            boolean newImage = false;
565            if (newRenderSize || mCanvas == null) {
566                if (mParams.getImageFactory() != null) {
567                    mImage = mParams.getImageFactory().getImage(
568                            mMeasuredScreenWidth,
569                            mMeasuredScreenHeight);
570                } else {
571                    mImage = new BufferedImage(
572                            mMeasuredScreenWidth,
573                            mMeasuredScreenHeight,
574                            BufferedImage.TYPE_INT_ARGB);
575                    newImage = true;
576                }
577
578                if (mParams.isBgColorOverridden()) {
579                    // since we override the content, it's the same as if it was a new image.
580                    newImage = true;
581                    Graphics2D gc = mImage.createGraphics();
582                    gc.setColor(new Color(mParams.getOverrideBgColor(), true));
583                    gc.setComposite(AlphaComposite.Src);
584                    gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight);
585                    gc.dispose();
586                }
587
588                // create an Android bitmap around the BufferedImage
589                Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage,
590                        true /*isMutable*/, mParams.getDensity());
591
592                // create a Canvas around the Android bitmap
593                mCanvas = new Canvas(bitmap);
594                mCanvas.setDensity(mParams.getDensity().getDpiValue());
595            }
596
597            if (freshRender && newImage == false) {
598                Graphics2D gc = mImage.createGraphics();
599                gc.setComposite(AlphaComposite.Src);
600
601                gc.setColor(new Color(0x00000000, true));
602                gc.fillRect(0, 0,
603                        mMeasuredScreenWidth, mMeasuredScreenHeight);
604
605                // done
606                gc.dispose();
607            }
608
609            mViewRoot.draw(mCanvas);
610
611            mViewInfoList = startVisitingViews(mViewRoot, 0);
612
613            // success!
614            return SUCCESS.createResult();
615        } catch (Throwable e) {
616            // get the real cause of the exception.
617            Throwable t = e;
618            while (t.getCause() != null) {
619                t = t.getCause();
620            }
621
622            return ERROR_UNKNOWN.createResult(t.getMessage(), t);
623        }
624    }
625
626    /**
627     * Animate an object
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 RenderSession#animate(Object, String, boolean, IAnimationListener)
635     */
636    public Result animate(Object targetObject, String animationName,
637            boolean isFrameworkAnimation, IAnimationListener listener) {
638        checkLock();
639
640        // find the animation file.
641        ResourceValue animationResource = null;
642        int animationId = 0;
643        if (isFrameworkAnimation) {
644            animationResource = mContext.getRenderResources().getFrameworkResource(
645                    ResourceType.ANIMATOR, animationName);
646            if (animationResource != null) {
647                animationId = Bridge.getResourceId(ResourceType.ANIMATOR, animationName);
648            }
649        } else {
650            animationResource = mContext.getRenderResources().getProjectResource(
651                    ResourceType.ANIMATOR, animationName);
652            if (animationResource != null) {
653                animationId = mContext.getProjectCallback().getResourceId(
654                        ResourceType.ANIMATOR, animationName);
655            }
656        }
657
658        if (animationResource != null) {
659            try {
660                Animator anim = AnimatorInflater.loadAnimator(mContext, animationId);
661                if (anim != null) {
662                    anim.setTarget(targetObject);
663
664                    new PlayAnimationThread(anim, this, animationName, listener).start();
665
666                    return SUCCESS.createResult();
667                }
668            } catch (Exception e) {
669                // get the real cause of the exception.
670                Throwable t = e;
671                while (t.getCause() != null) {
672                    t = t.getCause();
673                }
674
675                return ERROR_UNKNOWN.createResult(t.getMessage(), t);
676            }
677        }
678
679        return ERROR_ANIM_NOT_FOUND.createResult();
680    }
681
682    /**
683     * Insert a new child into an existing parent.
684     * <p>
685     * {@link #acquire(long)} must have been called before this.
686     *
687     * @throws IllegalStateException if the current context is different than the one owned by
688     *      the scene, or if {@link #acquire(long)} was not called.
689     *
690     * @see RenderSession#insertChild(Object, ILayoutPullParser, int, IAnimationListener)
691     */
692    public Result insertChild(final ViewGroup parentView, ILayoutPullParser childXml,
693            final int index, IAnimationListener listener) {
694        checkLock();
695
696        // create a block parser for the XML
697        BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(childXml, mContext,
698                false /* platformResourceFlag */);
699
700        // inflate the child without adding it to the root since we want to control where it'll
701        // get added. We do pass the parentView however to ensure that the layoutParams will
702        // be created correctly.
703        final View child = mInflater.inflate(blockParser, parentView, false /*attachToRoot*/);
704
705        invalidateRenderingSize();
706
707        if (listener != null) {
708            new AnimationThread(this, "insertChild", listener) {
709
710                @Override
711                public Result preAnimation() {
712                    parentView.setLayoutTransition(new LayoutTransition());
713                    return addView(parentView, child, index);
714                }
715
716                @Override
717                public void postAnimation() {
718                    parentView.setLayoutTransition(null);
719                }
720            }.start();
721
722            // always return success since the real status will come through the listener.
723            return SUCCESS.createResult(child);
724        }
725
726        // add it to the parentView in the correct location
727        Result result = addView(parentView, child, index);
728        if (result.isSuccess() == false) {
729            return result;
730        }
731
732        result = render(false /*freshRender*/);
733        if (result.isSuccess()) {
734            result = result.getCopyWithData(child);
735        }
736
737        return result;
738    }
739
740    /**
741     * Adds a given view to a given parent at a given index.
742     *
743     * @param parent the parent to receive the view
744     * @param view the view to add to the parent
745     * @param index the index where to do the add.
746     *
747     * @return a Result with {@link Status#SUCCESS} or
748     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
749     *     adding views.
750     */
751    private Result addView(ViewGroup parent, View view, int index) {
752        try {
753            parent.addView(view, index);
754            return SUCCESS.createResult();
755        } catch (UnsupportedOperationException e) {
756            // looks like this is a view class that doesn't support children manipulation!
757            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
758        }
759    }
760
761    /**
762     * Moves a view to a new parent at a given location
763     * <p>
764     * {@link #acquire(long)} must have been called before this.
765     *
766     * @throws IllegalStateException if the current context is different than the one owned by
767     *      the scene, or if {@link #acquire(long)} was not called.
768     *
769     * @see RenderSession#moveChild(Object, Object, int, Map, IAnimationListener)
770     */
771    public Result moveChild(final ViewGroup newParentView, final View childView, final int index,
772            Map<String, String> layoutParamsMap, final IAnimationListener listener) {
773        checkLock();
774
775        invalidateRenderingSize();
776
777        LayoutParams layoutParams = null;
778        if (layoutParamsMap != null) {
779            // need to create a new LayoutParams object for the new parent.
780            layoutParams = newParentView.generateLayoutParams(
781                    new BridgeLayoutParamsMapAttributes(layoutParamsMap));
782        }
783
784        // get the current parent of the view that needs to be moved.
785        final ViewGroup previousParent = (ViewGroup) childView.getParent();
786
787        if (listener != null) {
788            final LayoutParams params = layoutParams;
789
790            // there is no support for animating views across layouts, so in case the new and old
791            // parent views are different we fake the animation through a no animation thread.
792            if (previousParent != newParentView) {
793                new Thread("not animated moveChild") {
794                    @Override
795                    public void run() {
796                        Result result = moveView(previousParent, newParentView, childView, index,
797                                params);
798                        if (result.isSuccess() == false) {
799                            listener.done(result);
800                        }
801
802                        // ready to do the work, acquire the scene.
803                        result = acquire(250);
804                        if (result.isSuccess() == false) {
805                            listener.done(result);
806                            return;
807                        }
808
809                        try {
810                            result = render(false /*freshRender*/);
811                            if (result.isSuccess()) {
812                                listener.onNewFrame(RenderSessionImpl.this.getSession());
813                            }
814                        } finally {
815                            release();
816                        }
817
818                        listener.done(result);
819                    }
820                }.start();
821            } else {
822                new AnimationThread(this, "moveChild", listener) {
823
824                    @Override
825                    public Result preAnimation() {
826                        // set up the transition for the parent.
827                        LayoutTransition transition = new LayoutTransition();
828                        previousParent.setLayoutTransition(transition);
829
830                        // tweak the animation durations and start delays (to match the duration of
831                        // animation playing just before).
832                        // Note: Cannot user Animation.setDuration() directly. Have to set it
833                        // on the LayoutTransition.
834                        transition.setDuration(LayoutTransition.DISAPPEARING, 100);
835                        // CHANGE_DISAPPEARING plays after DISAPPEARING
836                        transition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 100);
837
838                        transition.setDuration(LayoutTransition.CHANGE_DISAPPEARING, 100);
839
840                        transition.setDuration(LayoutTransition.CHANGE_APPEARING, 100);
841                        // CHANGE_APPEARING plays after CHANGE_APPEARING
842                        transition.setStartDelay(LayoutTransition.APPEARING, 100);
843
844                        transition.setDuration(LayoutTransition.APPEARING, 100);
845
846                        return moveView(previousParent, newParentView, childView, index, params);
847                    }
848
849                    @Override
850                    public void postAnimation() {
851                        previousParent.setLayoutTransition(null);
852                        newParentView.setLayoutTransition(null);
853                    }
854                }.start();
855            }
856
857            // always return success since the real status will come through the listener.
858            return SUCCESS.createResult(layoutParams);
859        }
860
861        Result result = moveView(previousParent, newParentView, childView, index, layoutParams);
862        if (result.isSuccess() == false) {
863            return result;
864        }
865
866        result = render(false /*freshRender*/);
867        if (layoutParams != null && result.isSuccess()) {
868            result = result.getCopyWithData(layoutParams);
869        }
870
871        return result;
872    }
873
874    /**
875     * Moves a View from its current parent to a new given parent at a new given location, with
876     * an optional new {@link LayoutParams} instance
877     *
878     * @param previousParent the previous parent, still owning the child at the time of the call.
879     * @param newParent the new parent
880     * @param movedView the view to move
881     * @param index the new location in the new parent
882     * @param params an option (can be null) {@link LayoutParams} instance.
883     *
884     * @return a Result with {@link Status#SUCCESS} or
885     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
886     *     adding views.
887     */
888    private Result moveView(ViewGroup previousParent, final ViewGroup newParent,
889            final View movedView, final int index, final LayoutParams params) {
890        try {
891            // check if there is a transition on the previousParent.
892            LayoutTransition previousTransition = previousParent.getLayoutTransition();
893            if (previousTransition != null) {
894                // in this case there is an animation. This means we have to wait for the child's
895                // parent reference to be null'ed out so that we can add it to the new parent.
896                // It is technically removed right before the DISAPPEARING animation is done (if
897                // the animation of this type is not null, otherwise it's after which is impossible
898                // to handle).
899                // Because there is no move animation, if the new parent is the same as the old
900                // parent, we need to wait until the CHANGE_DISAPPEARING animation is done before
901                // adding the child or the child will appear in its new location before the
902                // other children have made room for it.
903
904                // add a listener to the transition to be notified of the actual removal.
905                previousTransition.addTransitionListener(new TransitionListener() {
906                    private int mChangeDisappearingCount = 0;
907
908                    public void startTransition(LayoutTransition transition, ViewGroup container,
909                            View view, int transitionType) {
910                        if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) {
911                            mChangeDisappearingCount++;
912                        }
913                    }
914
915                    public void endTransition(LayoutTransition transition, ViewGroup container,
916                            View view, int transitionType) {
917                        if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) {
918                            mChangeDisappearingCount--;
919                        }
920
921                        if (transitionType == LayoutTransition.CHANGE_DISAPPEARING &&
922                                mChangeDisappearingCount == 0) {
923                            // add it to the parentView in the correct location
924                            if (params != null) {
925                                newParent.addView(movedView, index, params);
926                            } else {
927                                newParent.addView(movedView, index);
928                            }
929                        }
930                    }
931                });
932
933                // remove the view from the current parent.
934                previousParent.removeView(movedView);
935
936                // and return since adding the view to the new parent is done in the listener.
937                return SUCCESS.createResult();
938            } else {
939                // standard code with no animation. pretty simple.
940                previousParent.removeView(movedView);
941
942                // add it to the parentView in the correct location
943                if (params != null) {
944                    newParent.addView(movedView, index, params);
945                } else {
946                    newParent.addView(movedView, index);
947                }
948
949                return SUCCESS.createResult();
950            }
951        } catch (UnsupportedOperationException e) {
952            // looks like this is a view class that doesn't support children manipulation!
953            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
954        }
955    }
956
957    /**
958     * Removes a child from its current parent.
959     * <p>
960     * {@link #acquire(long)} must have been called before this.
961     *
962     * @throws IllegalStateException if the current context is different than the one owned by
963     *      the scene, or if {@link #acquire(long)} was not called.
964     *
965     * @see RenderSession#removeChild(Object, IAnimationListener)
966     */
967    public Result removeChild(final View childView, IAnimationListener listener) {
968        checkLock();
969
970        invalidateRenderingSize();
971
972        final ViewGroup parent = (ViewGroup) childView.getParent();
973
974        if (listener != null) {
975            new AnimationThread(this, "moveChild", listener) {
976
977                @Override
978                public Result preAnimation() {
979                    parent.setLayoutTransition(new LayoutTransition());
980                    return removeView(parent, childView);
981                }
982
983                @Override
984                public void postAnimation() {
985                    parent.setLayoutTransition(null);
986                }
987            }.start();
988
989            // always return success since the real status will come through the listener.
990            return SUCCESS.createResult();
991        }
992
993        Result result = removeView(parent, childView);
994        if (result.isSuccess() == false) {
995            return result;
996        }
997
998        return render(false /*freshRender*/);
999    }
1000
1001    /**
1002     * Removes a given view from its current parent.
1003     *
1004     * @param view the view to remove from its parent
1005     *
1006     * @return a Result with {@link Status#SUCCESS} or
1007     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
1008     *     adding views.
1009     */
1010    private Result removeView(ViewGroup parent, View view) {
1011        try {
1012            parent.removeView(view);
1013            return SUCCESS.createResult();
1014        } catch (UnsupportedOperationException e) {
1015            // looks like this is a view class that doesn't support children manipulation!
1016            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
1017        }
1018    }
1019
1020    /**
1021     * Returns the log associated with the session.
1022     * @return the log or null if there are none.
1023     */
1024    public LayoutLog getLog() {
1025        if (mParams != null) {
1026            return mParams.getLog();
1027        }
1028
1029        return null;
1030    }
1031
1032    /**
1033     * Checks that the lock is owned by the current thread and that the current context is the one
1034     * from this scene.
1035     *
1036     * @throws IllegalStateException if the current context is different than the one owned by
1037     *      the scene, or if {@link #acquire(long)} was not called.
1038     */
1039    private void checkLock() {
1040        ReentrantLock lock = Bridge.getLock();
1041        if (lock.isHeldByCurrentThread() == false) {
1042            throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
1043        }
1044        if (sCurrentContext != mContext) {
1045            throw new IllegalStateException("Thread acquired a scene but is rendering a different one");
1046        }
1047    }
1048
1049    private void findBackground(RenderResources resources) {
1050        if (mParams.isBgColorOverridden() == false) {
1051            mWindowBackground = resources.findItemInTheme("windowBackground");
1052            if (mWindowBackground != null) {
1053                mWindowBackground = resources.resolveResValue(mWindowBackground);
1054            }
1055        }
1056    }
1057
1058    private boolean isTabletUi() {
1059        return mParams.getConfigScreenSize() == ScreenSize.XLARGE;
1060    }
1061
1062    private void findStatusBar(RenderResources resources, DisplayMetrics metrics) {
1063        if (isTabletUi() == false) {
1064            boolean windowFullscreen = getBooleanThemeValue(resources,
1065                    "windowFullscreen", false /*defaultValue*/);
1066
1067            if (windowFullscreen == false && mWindowIsFloating == false) {
1068                // default value
1069                mStatusBarSize = DEFAULT_STATUS_BAR_HEIGHT;
1070
1071                // get the real value
1072                ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN,
1073                        "status_bar_height");
1074
1075                if (value != null) {
1076                    TypedValue typedValue = ResourceHelper.getValue(value.getValue());
1077                    if (typedValue != null) {
1078                        // compute the pixel value based on the display metrics
1079                        mStatusBarSize = (int)typedValue.getDimension(metrics);
1080                    }
1081                }
1082            }
1083        }
1084    }
1085
1086    private void findActionBar(RenderResources resources, DisplayMetrics metrics) {
1087        if (mWindowIsFloating) {
1088            return;
1089        }
1090
1091        boolean windowActionBar = getBooleanThemeValue(resources,
1092                "windowActionBar", true /*defaultValue*/);
1093
1094        // if there's a value and it's false (default is true)
1095        if (windowActionBar) {
1096
1097            // default size of the window title bar
1098            mActionBarSize = DEFAULT_TITLE_BAR_HEIGHT;
1099
1100            // get value from the theme.
1101            ResourceValue value = resources.findItemInTheme("actionBarSize");
1102
1103            // resolve it
1104            value = resources.resolveResValue(value);
1105
1106            if (value != null) {
1107                // get the numerical value, if available
1108                TypedValue typedValue = ResourceHelper.getValue(value.getValue());
1109                if (typedValue != null) {
1110                    // compute the pixel value based on the display metrics
1111                    mActionBarSize = (int)typedValue.getDimension(metrics);
1112                }
1113            }
1114        } else {
1115            // action bar overrides title bar so only look for this one if action bar is hidden
1116            boolean windowNoTitle = getBooleanThemeValue(resources,
1117                    "windowNoTitle", false /*defaultValue*/);
1118
1119            if (windowNoTitle == false) {
1120
1121                // default size of the window title bar
1122                mTitleBarSize = DEFAULT_TITLE_BAR_HEIGHT;
1123
1124                // get value from the theme.
1125                ResourceValue value = resources.findItemInTheme("windowTitleSize");
1126
1127                // resolve it
1128                value = resources.resolveResValue(value);
1129
1130                if (value != null) {
1131                    // get the numerical value, if available
1132                    TypedValue typedValue = ResourceHelper.getValue(value.getValue());
1133                    if (typedValue != null) {
1134                        // compute the pixel value based on the display metrics
1135                        mTitleBarSize = (int)typedValue.getDimension(metrics);
1136                    }
1137                }
1138            }
1139
1140        }
1141    }
1142
1143    private void findSystemBar(RenderResources resources, DisplayMetrics metrics) {
1144        if (isTabletUi() && mWindowIsFloating == false) {
1145
1146            // default value
1147            mSystemBarSize = 48; // ??
1148
1149            // get the real value
1150            ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN,
1151                    "status_bar_height");
1152
1153            if (value != null) {
1154                TypedValue typedValue = ResourceHelper.getValue(value.getValue());
1155                if (typedValue != null) {
1156                    // compute the pixel value based on the display metrics
1157                    mSystemBarSize = (int)typedValue.getDimension(metrics);
1158                }
1159            }
1160        }
1161    }
1162
1163    private boolean getBooleanThemeValue(RenderResources resources,
1164            String name, boolean defaultValue) {
1165
1166        // get the title bar flag from the current theme.
1167        ResourceValue value = resources.findItemInTheme(name);
1168
1169        // because it may reference something else, we resolve it.
1170        value = resources.resolveResValue(value);
1171
1172        // if there's no value, return the default.
1173        if (value == null || value.getValue() == null) {
1174            return defaultValue;
1175        }
1176
1177        return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue);
1178    }
1179
1180    /**
1181     * Post process on a view hierachy that was just inflated.
1182     * <p/>At the moment this only support TabHost: If {@link TabHost} is detected, look for the
1183     * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically
1184     * based on the content of the {@link FrameLayout}.
1185     * @param view the root view to process.
1186     * @param projectCallback callback to the project.
1187     */
1188    private void postInflateProcess(View view, IProjectCallback projectCallback)
1189            throws PostInflateException {
1190        if (view instanceof TabHost) {
1191            setupTabHost((TabHost)view, projectCallback);
1192        } else if (view instanceof QuickContactBadge) {
1193            QuickContactBadge badge = (QuickContactBadge) view;
1194            badge.setImageToDefault();
1195        } else if (view instanceof ViewGroup) {
1196            ViewGroup group = (ViewGroup)view;
1197            final int count = group.getChildCount();
1198            for (int c = 0 ; c < count ; c++) {
1199                View child = group.getChildAt(c);
1200                postInflateProcess(child, projectCallback);
1201            }
1202        }
1203    }
1204
1205    /**
1206     * Sets up a {@link TabHost} object.
1207     * @param tabHost the TabHost to setup.
1208     * @param projectCallback The project callback object to access the project R class.
1209     * @throws PostInflateException
1210     */
1211    private void setupTabHost(TabHost tabHost, IProjectCallback projectCallback)
1212            throws PostInflateException {
1213        // look for the TabWidget, and the FrameLayout. They have their own specific names
1214        View v = tabHost.findViewById(android.R.id.tabs);
1215
1216        if (v == null) {
1217            throw new PostInflateException(
1218                    "TabHost requires a TabWidget with id \"android:id/tabs\".\n");
1219        }
1220
1221        if ((v instanceof TabWidget) == false) {
1222            throw new PostInflateException(String.format(
1223                    "TabHost requires a TabWidget with id \"android:id/tabs\".\n" +
1224                    "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName()));
1225        }
1226
1227        v = tabHost.findViewById(android.R.id.tabcontent);
1228
1229        if (v == null) {
1230            // TODO: see if we can fake tabs even without the FrameLayout (same below when the framelayout is empty)
1231            throw new PostInflateException(
1232                    "TabHost requires a FrameLayout with id \"android:id/tabcontent\".");
1233        }
1234
1235        if ((v instanceof FrameLayout) == false) {
1236            throw new PostInflateException(String.format(
1237                    "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" +
1238                    "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName()));
1239        }
1240
1241        FrameLayout content = (FrameLayout)v;
1242
1243        // now process the content of the framelayout and dynamically create tabs for it.
1244        final int count = content.getChildCount();
1245
1246        // this must be called before addTab() so that the TabHost searches its TabWidget
1247        // and FrameLayout.
1248        tabHost.setup();
1249
1250        if (count == 0) {
1251            // Create a dummy child to get a single tab
1252            TabSpec spec = tabHost.newTabSpec("tag").setIndicator("Tab Label",
1253                    tabHost.getResources().getDrawable(android.R.drawable.ic_menu_info_details))
1254                    .setContent(new TabHost.TabContentFactory() {
1255                        public View createTabContent(String tag) {
1256                            return new LinearLayout(mContext);
1257                        }
1258                    });
1259            tabHost.addTab(spec);
1260            return;
1261        } else {
1262            // for each child of the framelayout, add a new TabSpec
1263            for (int i = 0 ; i < count ; i++) {
1264                View child = content.getChildAt(i);
1265                String tabSpec = String.format("tab_spec%d", i+1);
1266                int id = child.getId();
1267                Pair<ResourceType, String> resource = projectCallback.resolveResourceId(id);
1268                String name;
1269                if (resource != null) {
1270                    name = resource.getSecond();
1271                } else {
1272                    name = String.format("Tab %d", i+1); // default name if id is unresolved.
1273                }
1274                tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id));
1275            }
1276        }
1277    }
1278
1279    private List<ViewInfo> startVisitingViews(View view, int offset) {
1280        if (view == null) {
1281            return null;
1282        }
1283
1284        // adjust the offset to this view.
1285        offset += view.getTop();
1286
1287        if (view == mContentRoot) {
1288            return visitAllChildren(mContentRoot, offset);
1289        }
1290
1291        // otherwise, look for mContentRoot in the children
1292        if (view instanceof ViewGroup) {
1293            ViewGroup group = ((ViewGroup) view);
1294
1295            for (int i = 0; i < group.getChildCount(); i++) {
1296                List<ViewInfo> list = startVisitingViews(group.getChildAt(i), offset);
1297                if (list != null) {
1298                    return list;
1299                }
1300            }
1301        }
1302
1303        return null;
1304    }
1305
1306    /**
1307     * Visits a View and its children and generate a {@link ViewInfo} containing the
1308     * bounds of all the views.
1309     * @param view the root View
1310     * @param offset an offset for the view bounds.
1311     */
1312    private ViewInfo visit(View view, int offset) {
1313        if (view == null) {
1314            return null;
1315        }
1316
1317        ViewInfo result = new ViewInfo(view.getClass().getName(),
1318                mContext.getViewKey(view),
1319                view.getLeft(), view.getTop() + offset, view.getRight(), view.getBottom() + offset,
1320                view, view.getLayoutParams());
1321
1322        if (view instanceof ViewGroup) {
1323            ViewGroup group = ((ViewGroup) view);
1324            result.setChildren(visitAllChildren(group, 0 /*offset*/));
1325        }
1326
1327        return result;
1328    }
1329
1330    /**
1331     * Visits all the children of a given ViewGroup generate a list of {@link ViewInfo}
1332     * containing the bounds of all the views.
1333     * @param view the root View
1334     * @param offset an offset for the view bounds.
1335     */
1336    private List<ViewInfo> visitAllChildren(ViewGroup viewGroup, int offset) {
1337        if (viewGroup == null) {
1338            return null;
1339        }
1340
1341        List<ViewInfo> children = new ArrayList<ViewInfo>();
1342        for (int i = 0; i < viewGroup.getChildCount(); i++) {
1343            children.add(visit(viewGroup.getChildAt(i), offset));
1344        }
1345        return children;
1346    }
1347
1348
1349    private void invalidateRenderingSize() {
1350        mMeasuredScreenWidth = mMeasuredScreenHeight = -1;
1351    }
1352
1353    public BufferedImage getImage() {
1354        return mImage;
1355    }
1356
1357    public boolean isAlphaChannelImage() {
1358        return mIsAlphaChannelImage;
1359    }
1360
1361    public List<ViewInfo> getViewInfos() {
1362        return mViewInfoList;
1363    }
1364
1365    public Map<String, String> getDefaultProperties(Object viewObject) {
1366        return mContext.getDefaultPropMap(viewObject);
1367    }
1368
1369    public void setScene(RenderSession session) {
1370        mScene = session;
1371    }
1372
1373    public RenderSession getSession() {
1374        return mScene;
1375    }
1376
1377    // --- FrameworkResourceIdProvider methods
1378
1379    @Override
1380    public Integer getId(ResourceType resType, String resName) {
1381        return Bridge.getResourceId(resType, resName);
1382    }
1383}
1384