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