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_INFLATION;
20import static com.android.ide.common.rendering.api.Result.Status.ERROR_NOT_INFLATED;
21import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
22import static com.android.ide.common.rendering.api.Result.Status.ERROR_VIEWGROUP_NO_CHILDREN;
23import static com.android.ide.common.rendering.api.Result.Status.SUCCESS;
24
25import com.android.ide.common.rendering.api.AdapterBinding;
26import com.android.ide.common.rendering.api.IAnimationListener;
27import com.android.ide.common.rendering.api.ILayoutPullParser;
28import com.android.ide.common.rendering.api.IProjectCallback;
29import com.android.ide.common.rendering.api.RenderParams;
30import com.android.ide.common.rendering.api.RenderResources;
31import com.android.ide.common.rendering.api.RenderSession;
32import com.android.ide.common.rendering.api.ResourceReference;
33import com.android.ide.common.rendering.api.ResourceValue;
34import com.android.ide.common.rendering.api.Result;
35import com.android.ide.common.rendering.api.SessionParams;
36import com.android.ide.common.rendering.api.ViewInfo;
37import com.android.ide.common.rendering.api.Result.Status;
38import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
39import com.android.internal.util.XmlUtils;
40import com.android.layoutlib.bridge.Bridge;
41import com.android.layoutlib.bridge.android.BridgeContext;
42import com.android.layoutlib.bridge.android.BridgeInflater;
43import com.android.layoutlib.bridge.android.BridgeLayoutParamsMapAttributes;
44import com.android.layoutlib.bridge.android.BridgeWindow;
45import com.android.layoutlib.bridge.android.BridgeWindowSession;
46import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
47import com.android.layoutlib.bridge.bars.PhoneSystemBar;
48import com.android.layoutlib.bridge.bars.TitleBar;
49import com.android.layoutlib.bridge.impl.binding.FakeAdapter;
50import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter;
51import com.android.resources.ResourceType;
52import com.android.resources.ScreenSize;
53import com.android.util.Pair;
54
55import org.xmlpull.v1.XmlPullParserException;
56
57import android.graphics.Bitmap;
58import android.graphics.Bitmap_Delegate;
59import android.graphics.Canvas;
60import android.graphics.drawable.Drawable;
61import android.os.Handler;
62import android.util.DisplayMetrics;
63import android.util.TypedValue;
64import android.view.View;
65import android.view.ViewGroup;
66import android.view.View.AttachInfo;
67import android.view.View.MeasureSpec;
68import android.view.ViewGroup.LayoutParams;
69import android.view.ViewGroup.MarginLayoutParams;
70import android.widget.AbsListView;
71import android.widget.AbsSpinner;
72import android.widget.AdapterView;
73import android.widget.ExpandableListView;
74import android.widget.FrameLayout;
75import android.widget.LinearLayout;
76import android.widget.ListView;
77import android.widget.TabHost;
78import android.widget.TabWidget;
79import android.widget.TabHost.TabSpec;
80
81import java.awt.AlphaComposite;
82import java.awt.Color;
83import java.awt.Graphics2D;
84import java.awt.image.BufferedImage;
85import java.util.ArrayList;
86import java.util.List;
87import java.util.Map;
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 RenderAction<SessionParams> {
98
99    private static final int DEFAULT_TITLE_BAR_HEIGHT = 25;
100    private static final int DEFAULT_STATUS_BAR_HEIGHT = 25;
101
102    // scene state
103    private RenderSession mScene;
104    private BridgeXmlBlockParser mBlockParser;
105    private BridgeInflater mInflater;
106    private ResourceValue mWindowBackground;
107    private ViewGroup mViewRoot;
108    private FrameLayout mContentRoot;
109    private Canvas mCanvas;
110    private int mMeasuredScreenWidth = -1;
111    private int mMeasuredScreenHeight = -1;
112    private boolean mIsAlphaChannelImage;
113    private boolean mWindowIsFloating;
114
115    private int mStatusBarSize;
116    private int mTitleBarSize;
117
118
119    // information being returned through the API
120    private BufferedImage mImage;
121    private List<ViewInfo> mViewInfoList;
122
123    private static final class PostInflateException extends Exception {
124        private static final long serialVersionUID = 1L;
125
126        public PostInflateException(String message) {
127            super(message);
128        }
129    }
130
131    /**
132     * Creates a layout scene with all the information coming from the layout bridge API.
133     * <p>
134     * This <b>must</b> be followed by a call to {@link RenderSessionImpl#init()}, which act as a
135     * call to {@link RenderSessionImpl#acquire(long)}
136     *
137     * @see LayoutBridge#createScene(com.android.layoutlib.api.SceneParams)
138     */
139    public RenderSessionImpl(SessionParams params) {
140        super(new SessionParams(params));
141    }
142
143    /**
144     * Initializes and acquires the scene, creating various Android objects such as context,
145     * inflater, and parser.
146     *
147     * @param timeout the time to wait if another rendering is happening.
148     *
149     * @return whether the scene was prepared
150     *
151     * @see #acquire(long)
152     * @see #release()
153     */
154    @Override
155    public Result init(long timeout) {
156        Result result = super.init(timeout);
157        if (result.isSuccess() == false) {
158            return result;
159        }
160
161        SessionParams params = getParams();
162        BridgeContext context = getContext();
163
164        RenderResources resources = getParams().getResources();
165        DisplayMetrics metrics = getContext().getMetrics();
166
167        // use default of true in case it's not found to use alpha by default
168        mIsAlphaChannelImage  = getBooleanThemeValue(resources,
169                "windowIsFloating", true /*defaultValue*/);
170
171        mWindowIsFloating = getBooleanThemeValue(resources, "windowIsFloating",
172                true /*defaultValue*/);
173
174        findBackground(resources);
175        findStatusBar(resources, metrics);
176        findTitleBar(resources, metrics);
177
178        // build the inflater and parser.
179        mInflater = new BridgeInflater(context, params.getProjectCallback());
180        context.setBridgeInflater(mInflater);
181
182        mBlockParser = new BridgeXmlBlockParser(
183                params.getLayoutDescription(), context, false /* platformResourceFlag */);
184
185        return SUCCESS.createResult();
186    }
187
188    /**
189     * Inflates the layout.
190     * <p>
191     * {@link #acquire(long)} must have been called before this.
192     *
193     * @throws IllegalStateException if the current context is different than the one owned by
194     *      the scene, or if {@link #init(long)} was not called.
195     */
196    public Result inflate() {
197        checkLock();
198
199        try {
200
201            SessionParams params = getParams();
202            BridgeContext context = getContext();
203
204            // the view group that receives the window background.
205            ViewGroup backgroundView = null;
206
207            if (mWindowIsFloating || params.isForceNoDecor()) {
208                backgroundView = mViewRoot = mContentRoot = new FrameLayout(context);
209            } else {
210                /*
211                 * we're creating the following layout
212                 *
213                   +-------------------------------------------------+
214                   | System bar                                      |
215                   +-------------------------------------------------+
216                   | (Layout with background drawable)               |
217                   | +---------------------------------------------+ |
218                   | | Title (optional)                            | |
219                   | +---------------------------------------------+ |
220                   | | Content, vertical extending                 | |
221                   | |                                             | |
222                   | +---------------------------------------------+ |
223                   +-------------------------------------------------+
224
225                 */
226
227                LinearLayout topLayout = new LinearLayout(context);
228                mViewRoot = topLayout;
229                topLayout.setOrientation(LinearLayout.VERTICAL);
230
231                if (mStatusBarSize > 0) {
232                    // system bar
233                    try {
234                        PhoneSystemBar systemBar = new PhoneSystemBar(context,
235                                params.getDensity());
236                        systemBar.setLayoutParams(
237                                new LinearLayout.LayoutParams(
238                                        LayoutParams.MATCH_PARENT, mStatusBarSize));
239                        topLayout.addView(systemBar);
240                    } catch (XmlPullParserException e) {
241
242                    }
243                }
244
245                LinearLayout backgroundLayout = new LinearLayout(context);
246                backgroundView = backgroundLayout;
247                backgroundLayout.setOrientation(LinearLayout.VERTICAL);
248                LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
249                        LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
250                layoutParams.weight = 1;
251                backgroundLayout.setLayoutParams(layoutParams);
252                topLayout.addView(backgroundLayout);
253
254
255                // if the theme says no title, then the size will be 0
256                if (mTitleBarSize > 0) {
257                    try {
258                        TitleBar titleBar = new TitleBar(context,
259                                params.getDensity(), params.getAppLabel());
260                        titleBar.setLayoutParams(
261                                new LinearLayout.LayoutParams(
262                                        LayoutParams.MATCH_PARENT, mTitleBarSize));
263                        backgroundLayout.addView(titleBar);
264                    } catch (XmlPullParserException e) {
265
266                    }
267                }
268
269                // content frame
270                mContentRoot = new FrameLayout(context);
271                layoutParams = new LinearLayout.LayoutParams(
272                        LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
273                layoutParams.weight = 1;
274                mContentRoot.setLayoutParams(layoutParams);
275                backgroundLayout.addView(mContentRoot);
276            }
277
278
279            View view = mInflater.inflate(mBlockParser, mContentRoot);
280
281            // done with the parser, pop it.
282            context.popParser();
283
284            // set the AttachInfo on the root view.
285            AttachInfo info = new AttachInfo(new BridgeWindowSession(), new BridgeWindow(),
286                    new Handler(), null);
287            info.mHasWindowFocus = true;
288            info.mWindowVisibility = View.VISIBLE;
289            info.mInTouchMode = false; // this is so that we can display selections.
290            mViewRoot.dispatchAttachedToWindow(info, 0);
291
292            // post-inflate process. For now this supports TabHost/TabWidget
293            postInflateProcess(view, params.getProjectCallback());
294
295            // get the background drawable
296            if (mWindowBackground != null && backgroundView != null) {
297                Drawable d = ResourceHelper.getDrawable(mWindowBackground, context);
298                backgroundView.setBackgroundDrawable(d);
299            }
300
301            return SUCCESS.createResult();
302        } catch (PostInflateException e) {
303            return ERROR_INFLATION.createResult(e.getMessage(), e);
304        } catch (Throwable e) {
305            // get the real cause of the exception.
306            Throwable t = e;
307            while (t.getCause() != null) {
308                t = t.getCause();
309            }
310
311            return ERROR_INFLATION.createResult(t.getMessage(), t);
312        }
313    }
314
315    /**
316     * Renders the scene.
317     * <p>
318     * {@link #acquire(long)} must have been called before this.
319     *
320     * @param freshRender whether the render is a new one and should erase the existing bitmap (in
321     *      the case where bitmaps are reused). This is typically needed when not playing
322     *      animations.)
323     *
324     * @throws IllegalStateException if the current context is different than the one owned by
325     *      the scene, or if {@link #acquire(long)} was not called.
326     *
327     * @see RenderParams#getRenderingMode()
328     * @see RenderSession#render(long)
329     */
330    public Result render(boolean freshRender) {
331        checkLock();
332
333        SessionParams params = getParams();
334
335        try {
336            if (mViewRoot == null) {
337                return ERROR_NOT_INFLATED.createResult();
338            }
339
340            RenderingMode renderingMode = params.getRenderingMode();
341
342            // only do the screen measure when needed.
343            boolean newRenderSize = false;
344            if (mMeasuredScreenWidth == -1) {
345                newRenderSize = true;
346                mMeasuredScreenWidth = params.getScreenWidth();
347                mMeasuredScreenHeight = params.getScreenHeight();
348
349                if (renderingMode != RenderingMode.NORMAL) {
350                    int widthMeasureSpecMode = renderingMode.isHorizExpand() ?
351                            MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
352                            : MeasureSpec.EXACTLY;
353                    int heightMeasureSpecMode = renderingMode.isVertExpand() ?
354                            MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
355                            : MeasureSpec.EXACTLY;
356
357                    // We used to compare the measured size of the content to the screen size but
358                    // this does not work anymore due to the 2 following issues:
359                    // - If the content is in a decor (system bar, title/action bar), the root view
360                    //   will not resize even with the UNSPECIFIED because of the embedded layout.
361                    // - If there is no decor, but a dialog frame, then the dialog padding prevents
362                    //   comparing the size of the content to the screen frame (as it would not
363                    //   take into account the dialog padding).
364
365                    // The solution is to first get the content size in a normal rendering, inside
366                    // the decor or the dialog padding.
367                    // Then measure only the content with UNSPECIFIED to see the size difference
368                    // and apply this to the screen size.
369
370                    // first measure the full layout, with EXACTLY to get the size of the
371                    // content as it is inside the decor/dialog
372                    Pair<Integer, Integer> exactMeasure = measureView(
373                            mViewRoot, mContentRoot.getChildAt(0),
374                            mMeasuredScreenWidth, MeasureSpec.EXACTLY,
375                            mMeasuredScreenHeight, MeasureSpec.EXACTLY);
376
377                    // now measure the content only using UNSPECIFIED (where applicable, based on
378                    // the rendering mode). This will give us the size the content needs.
379                    Pair<Integer, Integer> result = measureView(
380                            mContentRoot, mContentRoot.getChildAt(0),
381                            mMeasuredScreenWidth, widthMeasureSpecMode,
382                            mMeasuredScreenHeight, heightMeasureSpecMode);
383
384                    // now look at the difference and add what is needed.
385                    if (renderingMode.isHorizExpand()) {
386                        int measuredWidth = exactMeasure.getFirst();
387                        int neededWidth = result.getFirst();
388                        if (neededWidth > measuredWidth) {
389                            mMeasuredScreenWidth += neededWidth - measuredWidth;
390                        }
391                    }
392
393                    if (renderingMode.isVertExpand()) {
394                        int measuredHeight = exactMeasure.getSecond();
395                        int neededHeight = result.getSecond();
396                        if (neededHeight > measuredHeight) {
397                            mMeasuredScreenHeight += neededHeight - measuredHeight;
398                        }
399                    }
400                }
401            }
402
403            // measure again with the size we need
404            // This must always be done before the call to layout
405            measureView(mViewRoot, null /*measuredView*/,
406                    mMeasuredScreenWidth, MeasureSpec.EXACTLY,
407                    mMeasuredScreenHeight, MeasureSpec.EXACTLY);
408
409            // now do the layout.
410            mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight);
411
412            if (params.isLayoutOnly()) {
413                // delete the canvas and image to reset them on the next full rendering
414                mImage = null;
415                mCanvas = null;
416            } else {
417                mViewRoot.mAttachInfo.mTreeObserver.dispatchOnPreDraw();
418
419                // draw the views
420                // create the BufferedImage into which the layout will be rendered.
421                boolean newImage = false;
422                if (newRenderSize || mCanvas == null) {
423                    if (params.getImageFactory() != null) {
424                        mImage = params.getImageFactory().getImage(
425                                mMeasuredScreenWidth,
426                                mMeasuredScreenHeight);
427                    } else {
428                        mImage = new BufferedImage(
429                                mMeasuredScreenWidth,
430                                mMeasuredScreenHeight,
431                                BufferedImage.TYPE_INT_ARGB);
432                        newImage = true;
433                    }
434
435                    if (params.isBgColorOverridden()) {
436                        // since we override the content, it's the same as if it was a new image.
437                        newImage = true;
438                        Graphics2D gc = mImage.createGraphics();
439                        gc.setColor(new Color(params.getOverrideBgColor(), true));
440                        gc.setComposite(AlphaComposite.Src);
441                        gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight);
442                        gc.dispose();
443                    }
444
445                    // create an Android bitmap around the BufferedImage
446                    Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage,
447                            true /*isMutable*/, params.getDensity());
448
449                    // create a Canvas around the Android bitmap
450                    mCanvas = new Canvas(bitmap);
451                    mCanvas.setDensity(params.getDensity().getDpiValue());
452                }
453
454                if (freshRender && newImage == false) {
455                    Graphics2D gc = mImage.createGraphics();
456                    gc.setComposite(AlphaComposite.Src);
457
458                    gc.setColor(new Color(0x00000000, true));
459                    gc.fillRect(0, 0,
460                            mMeasuredScreenWidth, mMeasuredScreenHeight);
461
462                    // done
463                    gc.dispose();
464                }
465
466                mViewRoot.draw(mCanvas);
467            }
468
469            mViewInfoList = startVisitingViews(mViewRoot, 0, params.getExtendedViewInfoMode());
470
471            // success!
472            return SUCCESS.createResult();
473        } catch (Throwable e) {
474            // get the real cause of the exception.
475            Throwable t = e;
476            while (t.getCause() != null) {
477                t = t.getCause();
478            }
479
480            return ERROR_UNKNOWN.createResult(t.getMessage(), t);
481        }
482    }
483
484    /**
485     * Executes {@link View#measure(int, int)} on a given view with the given parameters (used
486     * to create measure specs with {@link MeasureSpec#makeMeasureSpec(int, int)}.
487     *
488     * if <var>measuredView</var> is non null, the method returns a {@link Pair} of (width, height)
489     * for the view (using {@link View#getMeasuredWidth()} and {@link View#getMeasuredHeight()}).
490     *
491     * @param viewToMeasure the view on which to execute measure().
492     * @param measuredView if non null, the view to query for its measured width/height.
493     * @param width the width to use in the MeasureSpec.
494     * @param widthMode the MeasureSpec mode to use for the width.
495     * @param height the height to use in the MeasureSpec.
496     * @param heightMode the MeasureSpec mode to use for the height.
497     * @return the measured width/height if measuredView is non-null, null otherwise.
498     */
499    private Pair<Integer, Integer> measureView(ViewGroup viewToMeasure, View measuredView,
500            int width, int widthMode, int height, int heightMode) {
501        int w_spec = MeasureSpec.makeMeasureSpec(width, widthMode);
502        int h_spec = MeasureSpec.makeMeasureSpec(height, heightMode);
503        viewToMeasure.measure(w_spec, h_spec);
504
505        if (measuredView != null) {
506            return Pair.of(measuredView.getMeasuredWidth(), measuredView.getMeasuredHeight());
507        }
508
509        return null;
510    }
511
512    /**
513     * Insert a new child into an existing parent.
514     * <p>
515     * {@link #acquire(long)} must have been called before this.
516     *
517     * @throws IllegalStateException if the current context is different than the one owned by
518     *      the scene, or if {@link #acquire(long)} was not called.
519     *
520     * @see RenderSession#insertChild(Object, ILayoutPullParser, int, IAnimationListener)
521     */
522    public Result insertChild(final ViewGroup parentView, ILayoutPullParser childXml,
523            final int index, final IAnimationListener listener) {
524        checkLock();
525
526        BridgeContext context = getContext();
527
528        // create a block parser for the XML
529        BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(
530                childXml, context, false /* platformResourceFlag */);
531
532        // inflate the child without adding it to the root since we want to control where it'll
533        // get added. We do pass the parentView however to ensure that the layoutParams will
534        // be created correctly.
535        final View child = mInflater.inflate(blockParser, parentView, false /*attachToRoot*/);
536        blockParser.ensurePopped();
537
538        invalidateRenderingSize();
539
540        if (listener != null) {
541            // there is no support for animating views in this API level, so we fake the animation
542            // through a no animation thread.
543            new Thread("not animated insertChild") {
544                @Override
545                public void run() {
546                    Result result = addView(parentView, child, index);
547                    if (result.isSuccess() == false) {
548                        listener.done(result);
549                    }
550
551                    // ready to do the work, acquire the scene.
552                    result = acquire(250);
553                    if (result.isSuccess() == false) {
554                        listener.done(result);
555                        return;
556                    }
557
558                    try {
559                        result = render(false /*freshRender*/);
560                        if (result.isSuccess()) {
561                            listener.onNewFrame(RenderSessionImpl.this.getSession());
562                        }
563                    } finally {
564                        release();
565                    }
566
567                    listener.done(result);
568                }
569            }.start();
570
571            // always return success since the real status will come through the listener.
572            return SUCCESS.createResult(child);
573        }
574
575        // add it to the parentView in the correct location
576        Result result = addView(parentView, child, index);
577        if (result.isSuccess() == false) {
578            return result;
579        }
580
581        result = render(false /*freshRender*/);
582        if (result.isSuccess()) {
583            result = result.getCopyWithData(child);
584        }
585
586        return result;
587    }
588
589    /**
590     * Adds a given view to a given parent at a given index.
591     *
592     * @param parent the parent to receive the view
593     * @param view the view to add to the parent
594     * @param index the index where to do the add.
595     *
596     * @return a Result with {@link Status#SUCCESS} or
597     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
598     *     adding views.
599     */
600    private Result addView(ViewGroup parent, View view, int index) {
601        try {
602            parent.addView(view, index);
603            return SUCCESS.createResult();
604        } catch (UnsupportedOperationException e) {
605            // looks like this is a view class that doesn't support children manipulation!
606            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
607        }
608    }
609
610    /**
611     * Moves a view to a new parent at a given location
612     * <p>
613     * {@link #acquire(long)} must have been called before this.
614     *
615     * @throws IllegalStateException if the current context is different than the one owned by
616     *      the scene, or if {@link #acquire(long)} was not called.
617     *
618     * @see RenderSession#moveChild(Object, Object, int, Map, IAnimationListener)
619     */
620    public Result moveChild(final ViewGroup newParentView, final View childView, final int index,
621            Map<String, String> layoutParamsMap, final IAnimationListener listener) {
622        checkLock();
623
624        invalidateRenderingSize();
625
626        LayoutParams layoutParams = null;
627        if (layoutParamsMap != null) {
628            // need to create a new LayoutParams object for the new parent.
629            layoutParams = newParentView.generateLayoutParams(
630                    new BridgeLayoutParamsMapAttributes(layoutParamsMap));
631        }
632
633        // get the current parent of the view that needs to be moved.
634        final ViewGroup previousParent = (ViewGroup) childView.getParent();
635
636        if (listener != null) {
637            final LayoutParams params = layoutParams;
638
639            // there is no support for animating views in this API level, so we fake the animation
640            // through a no animation thread.
641            new Thread("not animated moveChild") {
642                @Override
643                public void run() {
644                    Result result = moveView(previousParent, newParentView, childView, index,
645                            params);
646                    if (result.isSuccess() == false) {
647                        listener.done(result);
648                    }
649
650                    // ready to do the work, acquire the scene.
651                    result = acquire(250);
652                    if (result.isSuccess() == false) {
653                        listener.done(result);
654                        return;
655                    }
656
657                    try {
658                        result = render(false /*freshRender*/);
659                        if (result.isSuccess()) {
660                            listener.onNewFrame(RenderSessionImpl.this.getSession());
661                        }
662                    } finally {
663                        release();
664                    }
665
666                    listener.done(result);
667                }
668            }.start();
669
670            // always return success since the real status will come through the listener.
671            return SUCCESS.createResult(layoutParams);
672        }
673
674        Result result = moveView(previousParent, newParentView, childView, index, layoutParams);
675        if (result.isSuccess() == false) {
676            return result;
677        }
678
679        result = render(false /*freshRender*/);
680        if (layoutParams != null && result.isSuccess()) {
681            result = result.getCopyWithData(layoutParams);
682        }
683
684        return result;
685    }
686
687    /**
688     * Moves a View from its current parent to a new given parent at a new given location, with
689     * an optional new {@link LayoutParams} instance
690     *
691     * @param previousParent the previous parent, still owning the child at the time of the call.
692     * @param newParent the new parent
693     * @param movedView the view to move
694     * @param index the new location in the new parent
695     * @param params an option (can be null) {@link LayoutParams} instance.
696     *
697     * @return a Result with {@link Status#SUCCESS} or
698     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
699     *     adding views.
700     */
701    private Result moveView(ViewGroup previousParent, final ViewGroup newParent,
702            final View movedView, final int index, final LayoutParams params) {
703        try {
704            // standard code with no animation. pretty simple.
705            previousParent.removeView(movedView);
706
707            // add it to the parentView in the correct location
708            if (params != null) {
709                newParent.addView(movedView, index, params);
710            } else {
711                newParent.addView(movedView, index);
712            }
713
714            return SUCCESS.createResult();
715        } catch (UnsupportedOperationException e) {
716            // looks like this is a view class that doesn't support children manipulation!
717            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
718        }
719    }
720
721    /**
722     * Removes a child from its current parent.
723     * <p>
724     * {@link #acquire(long)} must have been called before this.
725     *
726     * @throws IllegalStateException if the current context is different than the one owned by
727     *      the scene, or if {@link #acquire(long)} was not called.
728     *
729     * @see RenderSession#removeChild(Object, IAnimationListener)
730     */
731    public Result removeChild(final View childView, final IAnimationListener listener) {
732        checkLock();
733
734        invalidateRenderingSize();
735
736        final ViewGroup parent = (ViewGroup) childView.getParent();
737
738        if (listener != null) {
739            // there is no support for animating views in this API level, so we fake the animation
740            // through a no animation thread.
741            new Thread("not animated moveChild") {
742                @Override
743                public void run() {
744                    Result result = removeView(parent, childView);
745                    if (result.isSuccess() == false) {
746                        listener.done(result);
747                    }
748
749                    // ready to do the work, acquire the scene.
750                    result = acquire(250);
751                    if (result.isSuccess() == false) {
752                        listener.done(result);
753                        return;
754                    }
755
756                    try {
757                        result = render(false /*freshRender*/);
758                        if (result.isSuccess()) {
759                            listener.onNewFrame(RenderSessionImpl.this.getSession());
760                        }
761                    } finally {
762                        release();
763                    }
764
765                    listener.done(result);
766                }
767            }.start();
768
769            // always return success since the real status will come through the listener.
770            return SUCCESS.createResult();
771        }
772
773        Result result = removeView(parent, childView);
774        if (result.isSuccess() == false) {
775            return result;
776        }
777
778        return render(false /*freshRender*/);
779    }
780
781    /**
782     * Removes a given view from its current parent.
783     *
784     * @param view the view to remove from its parent
785     *
786     * @return a Result with {@link Status#SUCCESS} or
787     *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
788     *     adding views.
789     */
790    private Result removeView(ViewGroup parent, View view) {
791        try {
792            parent.removeView(view);
793            return SUCCESS.createResult();
794        } catch (UnsupportedOperationException e) {
795            // looks like this is a view class that doesn't support children manipulation!
796            return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
797        }
798    }
799
800
801    private void findBackground(RenderResources resources) {
802        if (getParams().isBgColorOverridden() == false) {
803            mWindowBackground = resources.findItemInTheme("windowBackground");
804            if (mWindowBackground != null) {
805                mWindowBackground = resources.resolveResValue(mWindowBackground);
806            }
807        }
808    }
809
810    private void findStatusBar(RenderResources resources, DisplayMetrics metrics) {
811        boolean windowFullscreen = getBooleanThemeValue(resources,
812                "windowFullscreen", false /*defaultValue*/);
813
814        if (windowFullscreen == false && mWindowIsFloating == false) {
815            // default value
816            mStatusBarSize = DEFAULT_STATUS_BAR_HEIGHT;
817
818            // get the real value
819            ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN,
820                    "status_bar_height");
821
822            if (value != null) {
823                TypedValue typedValue = ResourceHelper.getValue(value.getValue());
824                if (typedValue != null) {
825                    // compute the pixel value based on the display metrics
826                    mStatusBarSize = (int)typedValue.getDimension(metrics);
827                }
828            }
829        }
830    }
831
832    private void findTitleBar(RenderResources resources, DisplayMetrics metrics) {
833        if (mWindowIsFloating) {
834            return;
835        }
836
837        boolean windowNoTitle = getBooleanThemeValue(resources,
838                "windowNoTitle", false /*defaultValue*/);
839
840        if (windowNoTitle == false) {
841
842            // default size of the window title bar
843            mTitleBarSize = DEFAULT_TITLE_BAR_HEIGHT;
844
845            // get value from the theme.
846            ResourceValue value = resources.findItemInTheme("windowTitleSize");
847
848            // resolve it
849            value = resources.resolveResValue(value);
850
851            if (value != null) {
852                // get the numerical value, if available
853                TypedValue typedValue = ResourceHelper.getValue(value.getValue());
854                if (typedValue != null) {
855                    // compute the pixel value based on the display metrics
856                    mTitleBarSize = (int)typedValue.getDimension(metrics);
857                }
858            }
859        }
860    }
861
862    private boolean getBooleanThemeValue(RenderResources resources,
863            String name, boolean defaultValue) {
864
865        // get the title bar flag from the current theme.
866        ResourceValue value = resources.findItemInTheme(name);
867
868        // because it may reference something else, we resolve it.
869        value = resources.resolveResValue(value);
870
871        // if there's no value, return the default.
872        if (value == null || value.getValue() == null) {
873            return defaultValue;
874        }
875
876        return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue);
877    }
878
879    /**
880     * Post process on a view hierachy that was just inflated.
881     * <p/>At the moment this only support TabHost: If {@link TabHost} is detected, look for the
882     * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically
883     * based on the content of the {@link FrameLayout}.
884     * @param view the root view to process.
885     * @param projectCallback callback to the project.
886     */
887    private void postInflateProcess(View view, IProjectCallback projectCallback)
888            throws PostInflateException {
889        if (view instanceof TabHost) {
890            setupTabHost((TabHost)view, projectCallback);
891        } else if (view instanceof AdapterView<?>) {
892            // get the view ID.
893            int id = view.getId();
894
895            BridgeContext context = getContext();
896
897            // get a ResourceReference from the integer ID.
898            ResourceReference listRef = context.resolveId(id);
899
900            if (listRef != null) {
901                SessionParams params = getParams();
902                AdapterBinding binding = params.getAdapterBindings().get(listRef);
903
904                // if there was no adapter binding, trying to get it from the call back.
905                if (binding == null) {
906                    binding = params.getProjectCallback().getAdapterBinding(listRef,
907                            context.getViewKey(view), view);
908                }
909
910                if (binding != null) {
911
912                    if (view instanceof AbsListView) {
913                        if ((binding.getFooterCount() > 0 || binding.getHeaderCount() > 0) &&
914                                view instanceof ListView) {
915                            ListView list = (ListView) view;
916
917                            boolean skipCallbackParser = false;
918
919                            int count = binding.getHeaderCount();
920                            for (int i = 0 ; i < count ; i++) {
921                                Pair<View, Boolean> pair = context.inflateView(
922                                        binding.getHeaderAt(i),
923                                        list, false /*attachToRoot*/, skipCallbackParser);
924                                if (pair.getFirst() != null) {
925                                    list.addHeaderView(pair.getFirst());
926                                }
927
928                                skipCallbackParser |= pair.getSecond();
929                            }
930
931                            count = binding.getFooterCount();
932                            for (int i = 0 ; i < count ; i++) {
933                                Pair<View, Boolean> pair = context.inflateView(
934                                        binding.getFooterAt(i),
935                                        list, false /*attachToRoot*/, skipCallbackParser);
936                                if (pair.getFirst() != null) {
937                                    list.addFooterView(pair.getFirst());
938                                }
939
940                                skipCallbackParser |= pair.getSecond();
941                            }
942                        }
943
944                        if (view instanceof ExpandableListView) {
945                            ((ExpandableListView) view).setAdapter(
946                                    new FakeExpandableAdapter(
947                                            listRef, binding, params.getProjectCallback()));
948                        } else {
949                            ((AbsListView) view).setAdapter(
950                                    new FakeAdapter(
951                                            listRef, binding, params.getProjectCallback()));
952                        }
953                    } else if (view instanceof AbsSpinner) {
954                        ((AbsSpinner) view).setAdapter(
955                                new FakeAdapter(
956                                        listRef, binding, params.getProjectCallback()));
957                    }
958                }
959            }
960        } else if (view instanceof ViewGroup) {
961            ViewGroup group = (ViewGroup)view;
962            final int count = group.getChildCount();
963            for (int c = 0 ; c < count ; c++) {
964                View child = group.getChildAt(c);
965                postInflateProcess(child, projectCallback);
966            }
967        }
968    }
969
970    /**
971     * Sets up a {@link TabHost} object.
972     * @param tabHost the TabHost to setup.
973     * @param projectCallback The project callback object to access the project R class.
974     * @throws PostInflateException
975     */
976    private void setupTabHost(TabHost tabHost, IProjectCallback projectCallback)
977            throws PostInflateException {
978        // look for the TabWidget, and the FrameLayout. They have their own specific names
979        View v = tabHost.findViewById(android.R.id.tabs);
980
981        if (v == null) {
982            throw new PostInflateException(
983                    "TabHost requires a TabWidget with id \"android:id/tabs\".\n");
984        }
985
986        if ((v instanceof TabWidget) == false) {
987            throw new PostInflateException(String.format(
988                    "TabHost requires a TabWidget with id \"android:id/tabs\".\n" +
989                    "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName()));
990        }
991
992        v = tabHost.findViewById(android.R.id.tabcontent);
993
994        if (v == null) {
995            // TODO: see if we can fake tabs even without the FrameLayout (same below when the framelayout is empty)
996            throw new PostInflateException(
997                    "TabHost requires a FrameLayout with id \"android:id/tabcontent\".");
998        }
999
1000        if ((v instanceof FrameLayout) == false) {
1001            throw new PostInflateException(String.format(
1002                    "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" +
1003                    "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName()));
1004        }
1005
1006        FrameLayout content = (FrameLayout)v;
1007
1008        // now process the content of the framelayout and dynamically create tabs for it.
1009        final int count = content.getChildCount();
1010
1011        // this must be called before addTab() so that the TabHost searches its TabWidget
1012        // and FrameLayout.
1013        tabHost.setup();
1014
1015        if (count == 0) {
1016            // Create a dummy child to get a single tab
1017            TabSpec spec = tabHost.newTabSpec("tag").setIndicator("Tab Label",
1018                    tabHost.getResources().getDrawable(android.R.drawable.ic_menu_info_details))
1019                    .setContent(new TabHost.TabContentFactory() {
1020                        public View createTabContent(String tag) {
1021                            return new LinearLayout(getContext());
1022                        }
1023                    });
1024            tabHost.addTab(spec);
1025            return;
1026        } else {
1027            // for each child of the framelayout, add a new TabSpec
1028            for (int i = 0 ; i < count ; i++) {
1029                View child = content.getChildAt(i);
1030                String tabSpec = String.format("tab_spec%d", i+1);
1031                int id = child.getId();
1032                Pair<ResourceType, String> resource = projectCallback.resolveResourceId(id);
1033                String name;
1034                if (resource != null) {
1035                    name = resource.getSecond();
1036                } else {
1037                    name = String.format("Tab %d", i+1); // default name if id is unresolved.
1038                }
1039                tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id));
1040            }
1041        }
1042    }
1043
1044    private List<ViewInfo> startVisitingViews(View view, int offset, boolean setExtendedInfo) {
1045        if (view == null) {
1046            return null;
1047        }
1048
1049        // adjust the offset to this view.
1050        offset += view.getTop();
1051
1052        if (view == mContentRoot) {
1053            return visitAllChildren(mContentRoot, offset, setExtendedInfo);
1054        }
1055
1056        // otherwise, look for mContentRoot in the children
1057        if (view instanceof ViewGroup) {
1058            ViewGroup group = ((ViewGroup) view);
1059
1060            for (int i = 0; i < group.getChildCount(); i++) {
1061                List<ViewInfo> list = startVisitingViews(group.getChildAt(i), offset,
1062                        setExtendedInfo);
1063                if (list != null) {
1064                    return list;
1065                }
1066            }
1067        }
1068
1069        return null;
1070    }
1071
1072    /**
1073     * Visits a View and its children and generate a {@link ViewInfo} containing the
1074     * bounds of all the views.
1075     * @param view the root View
1076     * @param offset an offset for the view bounds.
1077     * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object.
1078     */
1079    private ViewInfo visit(View view, int offset, boolean setExtendedInfo) {
1080        if (view == null) {
1081            return null;
1082        }
1083
1084        ViewInfo result = new ViewInfo(view.getClass().getName(),
1085                getContext().getViewKey(view),
1086                view.getLeft(), view.getTop() + offset, view.getRight(), view.getBottom() + offset,
1087                view, view.getLayoutParams());
1088
1089        if (setExtendedInfo) {
1090            MarginLayoutParams marginParams = null;
1091            LayoutParams params = view.getLayoutParams();
1092            if (params instanceof MarginLayoutParams) {
1093                marginParams = (MarginLayoutParams) params;
1094            }
1095            result.setExtendedInfo(view.getBaseline(),
1096                    marginParams != null ? marginParams.leftMargin : 0,
1097                    marginParams != null ? marginParams.topMargin : 0,
1098                    marginParams != null ? marginParams.rightMargin : 0,
1099                    marginParams != null ? marginParams.bottomMargin : 0);
1100        }
1101
1102        if (view instanceof ViewGroup) {
1103            ViewGroup group = ((ViewGroup) view);
1104            result.setChildren(visitAllChildren(group, 0 /*offset*/, setExtendedInfo));
1105        }
1106
1107        return result;
1108    }
1109
1110    /**
1111     * Visits all the children of a given ViewGroup generate a list of {@link ViewInfo}
1112     * containing the bounds of all the views.
1113     * @param view the root View
1114     * @param offset an offset for the view bounds.
1115     * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object.
1116     */
1117    private List<ViewInfo> visitAllChildren(ViewGroup viewGroup, int offset,
1118            boolean setExtendedInfo) {
1119        if (viewGroup == null) {
1120            return null;
1121        }
1122
1123        List<ViewInfo> children = new ArrayList<ViewInfo>();
1124        for (int i = 0; i < viewGroup.getChildCount(); i++) {
1125            children.add(visit(viewGroup.getChildAt(i), offset, setExtendedInfo));
1126        }
1127        return children;
1128    }
1129
1130
1131    private void invalidateRenderingSize() {
1132        mMeasuredScreenWidth = mMeasuredScreenHeight = -1;
1133    }
1134
1135    public BufferedImage getImage() {
1136        return mImage;
1137    }
1138
1139    public boolean isAlphaChannelImage() {
1140        return mIsAlphaChannelImage;
1141    }
1142
1143    public List<ViewInfo> getViewInfos() {
1144        return mViewInfoList;
1145    }
1146
1147    public Map<String, String> getDefaultProperties(Object viewObject) {
1148        return getContext().getDefaultPropMap(viewObject);
1149    }
1150
1151    public void setScene(RenderSession session) {
1152        mScene = session;
1153    }
1154
1155    public RenderSession getSession() {
1156        return mScene;
1157    }
1158}
1159