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 
17 package com.android.layoutlib.bridge.impl;
18 
19 import static com.android.ide.common.rendering.api.Result.Status.ERROR_ANIM_NOT_FOUND;
20 import static com.android.ide.common.rendering.api.Result.Status.ERROR_INFLATION;
21 import static com.android.ide.common.rendering.api.Result.Status.ERROR_NOT_INFLATED;
22 import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
23 import static com.android.ide.common.rendering.api.Result.Status.ERROR_VIEWGROUP_NO_CHILDREN;
24 import static com.android.ide.common.rendering.api.Result.Status.SUCCESS;
25 
26 import com.android.ide.common.rendering.api.AdapterBinding;
27 import com.android.ide.common.rendering.api.HardwareConfig;
28 import com.android.ide.common.rendering.api.IAnimationListener;
29 import com.android.ide.common.rendering.api.ILayoutPullParser;
30 import com.android.ide.common.rendering.api.IProjectCallback;
31 import com.android.ide.common.rendering.api.RenderResources;
32 import com.android.ide.common.rendering.api.RenderSession;
33 import com.android.ide.common.rendering.api.ResourceReference;
34 import com.android.ide.common.rendering.api.ResourceValue;
35 import com.android.ide.common.rendering.api.Result;
36 import com.android.ide.common.rendering.api.Result.Status;
37 import com.android.ide.common.rendering.api.SessionParams;
38 import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
39 import com.android.ide.common.rendering.api.StyleResourceValue;
40 import com.android.ide.common.rendering.api.ViewInfo;
41 import com.android.ide.common.rendering.api.ViewType;
42 import com.android.internal.util.XmlUtils;
43 import com.android.internal.view.menu.ActionMenuItemView;
44 import com.android.internal.view.menu.BridgeMenuItemImpl;
45 import com.android.internal.view.menu.IconMenuItemView;
46 import com.android.internal.view.menu.ListMenuItemView;
47 import com.android.internal.view.menu.MenuItemImpl;
48 import com.android.internal.view.menu.MenuView;
49 import com.android.layoutlib.bridge.Bridge;
50 import com.android.layoutlib.bridge.android.BridgeContext;
51 import com.android.layoutlib.bridge.android.BridgeLayoutParamsMapAttributes;
52 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
53 import com.android.layoutlib.bridge.android.SessionParamsFlags;
54 import com.android.layoutlib.bridge.bars.BridgeActionBar;
55 import com.android.layoutlib.bridge.bars.AppCompatActionBar;
56 import com.android.layoutlib.bridge.bars.Config;
57 import com.android.layoutlib.bridge.bars.NavigationBar;
58 import com.android.layoutlib.bridge.bars.StatusBar;
59 import com.android.layoutlib.bridge.bars.TitleBar;
60 import com.android.layoutlib.bridge.bars.FrameworkActionBar;
61 import com.android.layoutlib.bridge.impl.binding.FakeAdapter;
62 import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter;
63 import com.android.resources.Density;
64 import com.android.resources.ResourceType;
65 import com.android.resources.ScreenOrientation;
66 import com.android.util.Pair;
67 
68 import org.xmlpull.v1.XmlPullParserException;
69 
70 import android.animation.AnimationThread;
71 import android.animation.Animator;
72 import android.animation.AnimatorInflater;
73 import android.animation.LayoutTransition;
74 import android.animation.LayoutTransition.TransitionListener;
75 import android.app.Fragment_Delegate;
76 import android.graphics.Bitmap;
77 import android.graphics.Bitmap_Delegate;
78 import android.graphics.Canvas;
79 import android.graphics.drawable.Drawable;
80 import android.preference.Preference_Delegate;
81 import android.util.DisplayMetrics;
82 import android.util.TypedValue;
83 import android.view.AttachInfo_Accessor;
84 import android.view.BridgeInflater;
85 import android.view.IWindowManager;
86 import android.view.IWindowManagerImpl;
87 import android.view.Surface;
88 import android.view.View;
89 import android.view.View.MeasureSpec;
90 import android.view.ViewGroup;
91 import android.view.ViewGroup.LayoutParams;
92 import android.view.ViewGroup.MarginLayoutParams;
93 import android.view.ViewParent;
94 import android.view.WindowManagerGlobal_Delegate;
95 import android.widget.AbsListView;
96 import android.widget.AbsSpinner;
97 import android.widget.ActionMenuView;
98 import android.widget.AdapterView;
99 import android.widget.ExpandableListView;
100 import android.widget.FrameLayout;
101 import android.widget.LinearLayout;
102 import android.widget.ListView;
103 import android.widget.QuickContactBadge;
104 import android.widget.TabHost;
105 import android.widget.TabHost.TabSpec;
106 import android.widget.TabWidget;
107 
108 import java.awt.AlphaComposite;
109 import java.awt.Color;
110 import java.awt.Graphics2D;
111 import java.awt.image.BufferedImage;
112 import java.util.ArrayList;
113 import java.util.List;
114 import java.util.Map;
115 
116 /**
117  * Class implementing the render session.
118  * <p/>
119  * A session is a stateful representation of a layout file. It is initialized with data coming
120  * through the {@link Bridge} API to inflate the layout. Further actions and rendering can then
121  * be done on the layout.
122  */
123 public class RenderSessionImpl extends RenderAction<SessionParams> {
124 
125     private static final int DEFAULT_TITLE_BAR_HEIGHT = 25;
126     private static final int DEFAULT_STATUS_BAR_HEIGHT = 25;
127 
128     // scene state
129     private RenderSession mScene;
130     private BridgeXmlBlockParser mBlockParser;
131     private BridgeInflater mInflater;
132     private ResourceValue mWindowBackground;
133     private ViewGroup mViewRoot;
134     private FrameLayout mContentRoot;
135     private Canvas mCanvas;
136     private int mMeasuredScreenWidth = -1;
137     private int mMeasuredScreenHeight = -1;
138     private boolean mIsAlphaChannelImage;
139     private boolean mWindowIsFloating;
140     private Boolean mIsThemeAppCompat;
141 
142     private int mStatusBarSize;
143     private int mNavigationBarSize;
144     private int mNavigationBarOrientation = LinearLayout.HORIZONTAL;
145     private int mTitleBarSize;
146     private int mActionBarSize;
147 
148 
149     // information being returned through the API
150     private BufferedImage mImage;
151     private List<ViewInfo> mViewInfoList;
152     private List<ViewInfo> mSystemViewInfoList;
153 
154     private static final class PostInflateException extends Exception {
155         private static final long serialVersionUID = 1L;
156 
PostInflateException(String message)157         public PostInflateException(String message) {
158             super(message);
159         }
160     }
161 
162     /**
163      * Creates a layout scene with all the information coming from the layout bridge API.
164      * <p>
165      * This <b>must</b> be followed by a call to {@link RenderSessionImpl#init(long)},
166      * which act as a
167      * call to {@link RenderSessionImpl#acquire(long)}
168      *
169      * @see Bridge#createSession(SessionParams)
170      */
RenderSessionImpl(SessionParams params)171     public RenderSessionImpl(SessionParams params) {
172         super(new SessionParams(params));
173     }
174 
175     /**
176      * Initializes and acquires the scene, creating various Android objects such as context,
177      * inflater, and parser.
178      *
179      * @param timeout the time to wait if another rendering is happening.
180      *
181      * @return whether the scene was prepared
182      *
183      * @see #acquire(long)
184      * @see #release()
185      */
186     @Override
init(long timeout)187     public Result init(long timeout) {
188         Result result = super.init(timeout);
189         if (!result.isSuccess()) {
190             return result;
191         }
192 
193         SessionParams params = getParams();
194         BridgeContext context = getContext();
195 
196 
197         RenderResources resources = getParams().getResources();
198         DisplayMetrics metrics = getContext().getMetrics();
199 
200         // use default of true in case it's not found to use alpha by default
201         mIsAlphaChannelImage  = getBooleanThemeValue(resources, "windowIsFloating", true, true);
202         // FIXME: Find out why both variables are taking the same value.
203         mWindowIsFloating = getBooleanThemeValue(resources, "windowIsFloating", true, true);
204 
205         findBackground(resources);
206         findStatusBar(resources, metrics);
207         findActionBar(resources, metrics);
208         findNavigationBar(resources, metrics);
209 
210         // FIXME: find those out, and possibly add them to the render params
211         boolean hasNavigationBar = true;
212         //noinspection ConstantConditions
213         IWindowManager iwm = new IWindowManagerImpl(getContext().getConfiguration(),
214                 metrics, Surface.ROTATION_0,
215                 hasNavigationBar);
216         WindowManagerGlobal_Delegate.setWindowManagerService(iwm);
217 
218         // build the inflater and parser.
219         mInflater = new BridgeInflater(context, params.getProjectCallback());
220         context.setBridgeInflater(mInflater);
221 
222         mBlockParser = new BridgeXmlBlockParser(
223                 params.getLayoutDescription(), context, false /* platformResourceFlag */);
224 
225         return SUCCESS.createResult();
226     }
227 
228     /**
229      * Inflates the layout.
230      * <p>
231      * {@link #acquire(long)} must have been called before this.
232      *
233      * @throws IllegalStateException if the current context is different than the one owned by
234      *      the scene, or if {@link #init(long)} was not called.
235      */
inflate()236     public Result inflate() {
237         checkLock();
238 
239         try {
240 
241             SessionParams params = getParams();
242             HardwareConfig hardwareConfig = params.getHardwareConfig();
243             BridgeContext context = getContext();
244             boolean isRtl = Bridge.isLocaleRtl(params.getLocale());
245             int layoutDirection = isRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR;
246 
247             // the view group that receives the window background.
248             ViewGroup backgroundView;
249 
250             if (mWindowIsFloating || params.isForceNoDecor()) {
251                 backgroundView = mViewRoot = mContentRoot = new FrameLayout(context);
252                 mViewRoot.setLayoutDirection(layoutDirection);
253             } else {
254                 int simulatedPlatformVersion = params.getSimulatedPlatformVersion();
255                 if (hasSoftwareButtons() && mNavigationBarOrientation == LinearLayout.VERTICAL) {
256                     /*
257                      * This is a special case where the navigation bar is on the right.
258                        +-------------------------------------------------+---+
259                        | Status bar (always)                             |   |
260                        +-------------------------------------------------+   |
261                        | (Layout with background drawable)               |   |
262                        | +---------------------------------------------+ |   |
263                        | | Title/Action bar (optional)                 | |   |
264                        | +---------------------------------------------+ |   |
265                        | | Content, vertical extending                 | |   |
266                        | |                                             | |   |
267                        | +---------------------------------------------+ |   |
268                        +-------------------------------------------------+---+
269 
270                        So we create a horizontal layout, with the nav bar on the right,
271                        and the left part is the normal layout below without the nav bar at
272                        the bottom
273                      */
274                     LinearLayout topLayout = new LinearLayout(context);
275                     topLayout.setLayoutDirection(layoutDirection);
276                     mViewRoot = topLayout;
277                     topLayout.setOrientation(LinearLayout.HORIZONTAL);
278 
279                     if (Config.showOnScreenNavBar(simulatedPlatformVersion)) {
280                         try {
281                             NavigationBar navigationBar = createNavigationBar(context,
282                                     hardwareConfig.getDensity(), isRtl, params.isRtlSupported(),
283                                     simulatedPlatformVersion);
284                             topLayout.addView(navigationBar);
285                         } catch (XmlPullParserException ignored) {
286                         }
287                     }
288                 }
289 
290                 /*
291                  * we're creating the following layout
292                  *
293                    +-------------------------------------------------+
294                    | Status bar (always)                             |
295                    +-------------------------------------------------+
296                    | (Layout with background drawable)               |
297                    | +---------------------------------------------+ |
298                    | | Title/Action bar (optional)                 | |
299                    | +---------------------------------------------+ |
300                    | | Content, vertical extending                 | |
301                    | |                                             | |
302                    | +---------------------------------------------+ |
303                    +-------------------------------------------------+
304                    | Navigation bar for soft buttons, maybe see above|
305                    +-------------------------------------------------+
306 
307                  */
308 
309                 LinearLayout topLayout = new LinearLayout(context);
310                 topLayout.setOrientation(LinearLayout.VERTICAL);
311                 topLayout.setLayoutDirection(layoutDirection);
312                 // if we don't already have a view root this is it
313                 if (mViewRoot == null) {
314                     mViewRoot = topLayout;
315                 } else {
316                     int topLayoutWidth =
317                             params.getHardwareConfig().getScreenWidth() - mNavigationBarSize;
318                     LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
319                             topLayoutWidth, LayoutParams.MATCH_PARENT);
320                     topLayout.setLayoutParams(layoutParams);
321 
322                     // this is the case of soft buttons + vertical bar.
323                     // this top layout is the first layout in the horizontal layout. see above)
324                     if (isRtl && params.isRtlSupported()) {
325                         // If RTL is enabled, layoutlib will mirror the layouts. So, add the
326                         // topLayout to the right of Navigation Bar and layoutlib will draw it
327                         // to the left.
328                         mViewRoot.addView(topLayout);
329                     } else {
330                         // Add the top layout to the left of the Navigation Bar.
331                         mViewRoot.addView(topLayout, 0);
332                     }
333                 }
334 
335                 if (mStatusBarSize > 0) {
336                     // system bar
337                     try {
338                         StatusBar statusBar = createStatusBar(context, hardwareConfig.getDensity(),
339                                 layoutDirection, params.isRtlSupported(),
340                                 simulatedPlatformVersion);
341                         topLayout.addView(statusBar);
342                     } catch (XmlPullParserException ignored) {
343 
344                     }
345                 }
346 
347                 LinearLayout backgroundLayout = new LinearLayout(context);
348                 backgroundView = backgroundLayout;
349                 backgroundLayout.setOrientation(LinearLayout.VERTICAL);
350                 LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
351                         LayoutParams.MATCH_PARENT, 0);
352                 layoutParams.weight = 1;
353                 backgroundLayout.setLayoutParams(layoutParams);
354                 topLayout.addView(backgroundLayout);
355 
356 
357                 // if the theme says no title/action bar, then the size will be 0
358                 if (mActionBarSize > 0) {
359                     BridgeActionBar actionBar = createActionBar(context, params, backgroundLayout);
360                     actionBar.createMenuPopup();
361                     mContentRoot = actionBar.getContentRoot();
362                 } else if (mTitleBarSize > 0) {
363                     try {
364                         TitleBar titleBar = createTitleBar(context,
365                                 params.getAppLabel(),
366                                 simulatedPlatformVersion);
367                         backgroundLayout.addView(titleBar);
368                     } catch (XmlPullParserException ignored) {
369 
370                     }
371                 }
372 
373                 // content frame
374                 if (mContentRoot == null) {
375                     mContentRoot = new FrameLayout(context);
376                     layoutParams = new LinearLayout.LayoutParams(
377                             LayoutParams.MATCH_PARENT, 0);
378                     layoutParams.weight = 1;
379                     mContentRoot.setLayoutParams(layoutParams);
380                     backgroundLayout.addView(mContentRoot);
381                 }
382 
383                 if (Config.showOnScreenNavBar(simulatedPlatformVersion) &&
384                         mNavigationBarOrientation == LinearLayout.HORIZONTAL &&
385                         mNavigationBarSize > 0) {
386                     // system bar
387                     try {
388                         NavigationBar navigationBar = createNavigationBar(context,
389                                 hardwareConfig.getDensity(), isRtl, params.isRtlSupported(),
390                                 simulatedPlatformVersion);
391                         topLayout.addView(navigationBar);
392                     } catch (XmlPullParserException ignored) {
393 
394                     }
395                 }
396             }
397 
398 
399             // Sets the project callback (custom view loader) to the fragment delegate so that
400             // it can instantiate the custom Fragment.
401             Fragment_Delegate.setProjectCallback(params.getProjectCallback());
402 
403             String rootTag = params.getFlag(SessionParamsFlags.FLAG_KEY_ROOT_TAG);
404             boolean isPreference = "PreferenceScreen".equals(rootTag);
405             View view;
406             if (isPreference) {
407                 view = Preference_Delegate.inflatePreference(getContext(), mBlockParser,
408                   mContentRoot);
409             } else {
410                 view = mInflater.inflate(mBlockParser, mContentRoot);
411             }
412 
413             // done with the parser, pop it.
414             context.popParser();
415 
416             Fragment_Delegate.setProjectCallback(null);
417 
418             // set the AttachInfo on the root view.
419             AttachInfo_Accessor.setAttachInfo(mViewRoot);
420 
421             // post-inflate process. For now this supports TabHost/TabWidget
422             postInflateProcess(view, params.getProjectCallback(), isPreference ? view : null);
423 
424             // get the background drawable
425             if (mWindowBackground != null) {
426                 Drawable d = ResourceHelper.getDrawable(mWindowBackground, context);
427                 backgroundView.setBackground(d);
428             }
429 
430             return SUCCESS.createResult();
431         } catch (PostInflateException e) {
432             return ERROR_INFLATION.createResult(e.getMessage(), e);
433         } catch (Throwable e) {
434             // get the real cause of the exception.
435             Throwable t = e;
436             while (t.getCause() != null) {
437                 t = t.getCause();
438             }
439 
440             return ERROR_INFLATION.createResult(t.getMessage(), t);
441         }
442     }
443 
444     /**
445      * Renders the scene.
446      * <p>
447      * {@link #acquire(long)} must have been called before this.
448      *
449      * @param freshRender whether the render is a new one and should erase the existing bitmap (in
450      *      the case where bitmaps are reused). This is typically needed when not playing
451      *      animations.)
452      *
453      * @throws IllegalStateException if the current context is different than the one owned by
454      *      the scene, or if {@link #acquire(long)} was not called.
455      *
456      * @see SessionParams#getRenderingMode()
457      * @see RenderSession#render(long)
458      */
render(boolean freshRender)459     public Result render(boolean freshRender) {
460         checkLock();
461 
462         SessionParams params = getParams();
463 
464         try {
465             if (mViewRoot == null) {
466                 return ERROR_NOT_INFLATED.createResult();
467             }
468 
469             RenderingMode renderingMode = params.getRenderingMode();
470             HardwareConfig hardwareConfig = params.getHardwareConfig();
471 
472             // only do the screen measure when needed.
473             boolean newRenderSize = false;
474             if (mMeasuredScreenWidth == -1) {
475                 newRenderSize = true;
476                 mMeasuredScreenWidth = hardwareConfig.getScreenWidth();
477                 mMeasuredScreenHeight = hardwareConfig.getScreenHeight();
478 
479                 if (renderingMode != RenderingMode.NORMAL) {
480                     int widthMeasureSpecMode = renderingMode.isHorizExpand() ?
481                             MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
482                             : MeasureSpec.EXACTLY;
483                     int heightMeasureSpecMode = renderingMode.isVertExpand() ?
484                             MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
485                             : MeasureSpec.EXACTLY;
486 
487                     // We used to compare the measured size of the content to the screen size but
488                     // this does not work anymore due to the 2 following issues:
489                     // - If the content is in a decor (system bar, title/action bar), the root view
490                     //   will not resize even with the UNSPECIFIED because of the embedded layout.
491                     // - If there is no decor, but a dialog frame, then the dialog padding prevents
492                     //   comparing the size of the content to the screen frame (as it would not
493                     //   take into account the dialog padding).
494 
495                     // The solution is to first get the content size in a normal rendering, inside
496                     // the decor or the dialog padding.
497                     // Then measure only the content with UNSPECIFIED to see the size difference
498                     // and apply this to the screen size.
499 
500                     // first measure the full layout, with EXACTLY to get the size of the
501                     // content as it is inside the decor/dialog
502                     @SuppressWarnings("deprecation")
503                     Pair<Integer, Integer> exactMeasure = measureView(
504                             mViewRoot, mContentRoot.getChildAt(0),
505                             mMeasuredScreenWidth, MeasureSpec.EXACTLY,
506                             mMeasuredScreenHeight, MeasureSpec.EXACTLY);
507 
508                     // now measure the content only using UNSPECIFIED (where applicable, based on
509                     // the rendering mode). This will give us the size the content needs.
510                     @SuppressWarnings("deprecation")
511                     Pair<Integer, Integer> result = measureView(
512                             mContentRoot, mContentRoot.getChildAt(0),
513                             mMeasuredScreenWidth, widthMeasureSpecMode,
514                             mMeasuredScreenHeight, heightMeasureSpecMode);
515 
516                     // now look at the difference and add what is needed.
517                     if (renderingMode.isHorizExpand()) {
518                         int measuredWidth = exactMeasure.getFirst();
519                         int neededWidth = result.getFirst();
520                         if (neededWidth > measuredWidth) {
521                             mMeasuredScreenWidth += neededWidth - measuredWidth;
522                         }
523                     }
524 
525                     if (renderingMode.isVertExpand()) {
526                         int measuredHeight = exactMeasure.getSecond();
527                         int neededHeight = result.getSecond();
528                         if (neededHeight > measuredHeight) {
529                             mMeasuredScreenHeight += neededHeight - measuredHeight;
530                         }
531                     }
532                 }
533             }
534 
535             // measure again with the size we need
536             // This must always be done before the call to layout
537             measureView(mViewRoot, null /*measuredView*/,
538                     mMeasuredScreenWidth, MeasureSpec.EXACTLY,
539                     mMeasuredScreenHeight, MeasureSpec.EXACTLY);
540 
541             // now do the layout.
542             mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight);
543 
544             if (params.isLayoutOnly()) {
545                 // delete the canvas and image to reset them on the next full rendering
546                 mImage = null;
547                 mCanvas = null;
548             } else {
549                 AttachInfo_Accessor.dispatchOnPreDraw(mViewRoot);
550 
551                 // draw the views
552                 // create the BufferedImage into which the layout will be rendered.
553                 boolean newImage = false;
554                 if (newRenderSize || mCanvas == null) {
555                     if (params.getImageFactory() != null) {
556                         mImage = params.getImageFactory().getImage(
557                                 mMeasuredScreenWidth,
558                                 mMeasuredScreenHeight);
559                     } else {
560                         mImage = new BufferedImage(
561                                 mMeasuredScreenWidth,
562                                 mMeasuredScreenHeight,
563                                 BufferedImage.TYPE_INT_ARGB);
564                         newImage = true;
565                     }
566 
567                     if (params.isBgColorOverridden()) {
568                         // since we override the content, it's the same as if it was a new image.
569                         newImage = true;
570                         Graphics2D gc = mImage.createGraphics();
571                         gc.setColor(new Color(params.getOverrideBgColor(), true));
572                         gc.setComposite(AlphaComposite.Src);
573                         gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight);
574                         gc.dispose();
575                     }
576 
577                     // create an Android bitmap around the BufferedImage
578                     Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage,
579                             true /*isMutable*/, hardwareConfig.getDensity());
580 
581                     // create a Canvas around the Android bitmap
582                     mCanvas = new Canvas(bitmap);
583                     mCanvas.setDensity(hardwareConfig.getDensity().getDpiValue());
584                 }
585 
586                 if (freshRender && !newImage) {
587                     Graphics2D gc = mImage.createGraphics();
588                     gc.setComposite(AlphaComposite.Src);
589 
590                     gc.setColor(new Color(0x00000000, true));
591                     gc.fillRect(0, 0,
592                             mMeasuredScreenWidth, mMeasuredScreenHeight);
593 
594                     // done
595                     gc.dispose();
596                 }
597 
598                 mViewRoot.draw(mCanvas);
599             }
600 
601             mSystemViewInfoList = visitAllChildren(mViewRoot, 0, params.getExtendedViewInfoMode(),
602                     false);
603 
604             // success!
605             return SUCCESS.createResult();
606         } catch (Throwable e) {
607             // get the real cause of the exception.
608             Throwable t = e;
609             while (t.getCause() != null) {
610                 t = t.getCause();
611             }
612 
613             return ERROR_UNKNOWN.createResult(t.getMessage(), t);
614         }
615     }
616 
617     /**
618      * Executes {@link View#measure(int, int)} on a given view with the given parameters (used
619      * to create measure specs with {@link MeasureSpec#makeMeasureSpec(int, int)}.
620      *
621      * if <var>measuredView</var> is non null, the method returns a {@link Pair} of (width, height)
622      * for the view (using {@link View#getMeasuredWidth()} and {@link View#getMeasuredHeight()}).
623      *
624      * @param viewToMeasure the view on which to execute measure().
625      * @param measuredView if non null, the view to query for its measured width/height.
626      * @param width the width to use in the MeasureSpec.
627      * @param widthMode the MeasureSpec mode to use for the width.
628      * @param height the height to use in the MeasureSpec.
629      * @param heightMode the MeasureSpec mode to use for the height.
630      * @return the measured width/height if measuredView is non-null, null otherwise.
631      */
632     @SuppressWarnings("deprecation")  // For the use of Pair
measureView(ViewGroup viewToMeasure, View measuredView, int width, int widthMode, int height, int heightMode)633     private Pair<Integer, Integer> measureView(ViewGroup viewToMeasure, View measuredView,
634             int width, int widthMode, int height, int heightMode) {
635         int w_spec = MeasureSpec.makeMeasureSpec(width, widthMode);
636         int h_spec = MeasureSpec.makeMeasureSpec(height, heightMode);
637         viewToMeasure.measure(w_spec, h_spec);
638 
639         if (measuredView != null) {
640             return Pair.of(measuredView.getMeasuredWidth(), measuredView.getMeasuredHeight());
641         }
642 
643         return null;
644     }
645 
646     /**
647      * Animate an object
648      * <p>
649      * {@link #acquire(long)} must have been called before this.
650      *
651      * @throws IllegalStateException if the current context is different than the one owned by
652      *      the scene, or if {@link #acquire(long)} was not called.
653      *
654      * @see RenderSession#animate(Object, String, boolean, IAnimationListener)
655      */
animate(Object targetObject, String animationName, boolean isFrameworkAnimation, IAnimationListener listener)656     public Result animate(Object targetObject, String animationName,
657             boolean isFrameworkAnimation, IAnimationListener listener) {
658         checkLock();
659 
660         BridgeContext context = getContext();
661 
662         // find the animation file.
663         ResourceValue animationResource;
664         int animationId = 0;
665         if (isFrameworkAnimation) {
666             animationResource = context.getRenderResources().getFrameworkResource(
667                     ResourceType.ANIMATOR, animationName);
668             if (animationResource != null) {
669                 animationId = Bridge.getResourceId(ResourceType.ANIMATOR, animationName);
670             }
671         } else {
672             animationResource = context.getRenderResources().getProjectResource(
673                     ResourceType.ANIMATOR, animationName);
674             if (animationResource != null) {
675                 animationId = context.getProjectCallback().getResourceId(
676                         ResourceType.ANIMATOR, animationName);
677             }
678         }
679 
680         if (animationResource != null) {
681             try {
682                 Animator anim = AnimatorInflater.loadAnimator(context, animationId);
683                 if (anim != null) {
684                     anim.setTarget(targetObject);
685 
686                     new PlayAnimationThread(anim, this, animationName, listener).start();
687 
688                     return SUCCESS.createResult();
689                 }
690             } catch (Exception e) {
691                 // get the real cause of the exception.
692                 Throwable t = e;
693                 while (t.getCause() != null) {
694                     t = t.getCause();
695                 }
696 
697                 return ERROR_UNKNOWN.createResult(t.getMessage(), t);
698             }
699         }
700 
701         return ERROR_ANIM_NOT_FOUND.createResult();
702     }
703 
704     /**
705      * Insert a new child into an existing parent.
706      * <p>
707      * {@link #acquire(long)} must have been called before this.
708      *
709      * @throws IllegalStateException if the current context is different than the one owned by
710      *      the scene, or if {@link #acquire(long)} was not called.
711      *
712      * @see RenderSession#insertChild(Object, ILayoutPullParser, int, IAnimationListener)
713      */
insertChild(final ViewGroup parentView, ILayoutPullParser childXml, final int index, IAnimationListener listener)714     public Result insertChild(final ViewGroup parentView, ILayoutPullParser childXml,
715             final int index, IAnimationListener listener) {
716         checkLock();
717 
718         BridgeContext context = getContext();
719 
720         // create a block parser for the XML
721         BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(
722                 childXml, context, false /* platformResourceFlag */);
723 
724         // inflate the child without adding it to the root since we want to control where it'll
725         // get added. We do pass the parentView however to ensure that the layoutParams will
726         // be created correctly.
727         final View child = mInflater.inflate(blockParser, parentView, false /*attachToRoot*/);
728         blockParser.ensurePopped();
729 
730         invalidateRenderingSize();
731 
732         if (listener != null) {
733             new AnimationThread(this, "insertChild", listener) {
734 
735                 @Override
736                 public Result preAnimation() {
737                     parentView.setLayoutTransition(new LayoutTransition());
738                     return addView(parentView, child, index);
739                 }
740 
741                 @Override
742                 public void postAnimation() {
743                     parentView.setLayoutTransition(null);
744                 }
745             }.start();
746 
747             // always return success since the real status will come through the listener.
748             return SUCCESS.createResult(child);
749         }
750 
751         // add it to the parentView in the correct location
752         Result result = addView(parentView, child, index);
753         if (!result.isSuccess()) {
754             return result;
755         }
756 
757         result = render(false /*freshRender*/);
758         if (result.isSuccess()) {
759             result = result.getCopyWithData(child);
760         }
761 
762         return result;
763     }
764 
765     /**
766      * Adds a given view to a given parent at a given index.
767      *
768      * @param parent the parent to receive the view
769      * @param view the view to add to the parent
770      * @param index the index where to do the add.
771      *
772      * @return a Result with {@link Status#SUCCESS} or
773      *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
774      *     adding views.
775      */
addView(ViewGroup parent, View view, int index)776     private Result addView(ViewGroup parent, View view, int index) {
777         try {
778             parent.addView(view, index);
779             return SUCCESS.createResult();
780         } catch (UnsupportedOperationException e) {
781             // looks like this is a view class that doesn't support children manipulation!
782             return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
783         }
784     }
785 
786     /**
787      * Moves a view to a new parent at a given location
788      * <p>
789      * {@link #acquire(long)} must have been called before this.
790      *
791      * @throws IllegalStateException if the current context is different than the one owned by
792      *      the scene, or if {@link #acquire(long)} was not called.
793      *
794      * @see RenderSession#moveChild(Object, Object, int, Map, IAnimationListener)
795      */
moveChild(final ViewGroup newParentView, final View childView, final int index, Map<String, String> layoutParamsMap, final IAnimationListener listener)796     public Result moveChild(final ViewGroup newParentView, final View childView, final int index,
797             Map<String, String> layoutParamsMap, final IAnimationListener listener) {
798         checkLock();
799 
800         invalidateRenderingSize();
801 
802         LayoutParams layoutParams = null;
803         if (layoutParamsMap != null) {
804             // need to create a new LayoutParams object for the new parent.
805             layoutParams = newParentView.generateLayoutParams(
806                     new BridgeLayoutParamsMapAttributes(layoutParamsMap));
807         }
808 
809         // get the current parent of the view that needs to be moved.
810         final ViewGroup previousParent = (ViewGroup) childView.getParent();
811 
812         if (listener != null) {
813             final LayoutParams params = layoutParams;
814 
815             // there is no support for animating views across layouts, so in case the new and old
816             // parent views are different we fake the animation through a no animation thread.
817             if (previousParent != newParentView) {
818                 new Thread("not animated moveChild") {
819                     @Override
820                     public void run() {
821                         Result result = moveView(previousParent, newParentView, childView, index,
822                                 params);
823                         if (!result.isSuccess()) {
824                             listener.done(result);
825                         }
826 
827                         // ready to do the work, acquire the scene.
828                         result = acquire(250);
829                         if (!result.isSuccess()) {
830                             listener.done(result);
831                             return;
832                         }
833 
834                         try {
835                             result = render(false /*freshRender*/);
836                             if (result.isSuccess()) {
837                                 listener.onNewFrame(RenderSessionImpl.this.getSession());
838                             }
839                         } finally {
840                             release();
841                         }
842 
843                         listener.done(result);
844                     }
845                 }.start();
846             } else {
847                 new AnimationThread(this, "moveChild", listener) {
848 
849                     @Override
850                     public Result preAnimation() {
851                         // set up the transition for the parent.
852                         LayoutTransition transition = new LayoutTransition();
853                         previousParent.setLayoutTransition(transition);
854 
855                         // tweak the animation durations and start delays (to match the duration of
856                         // animation playing just before).
857                         // Note: Cannot user Animation.setDuration() directly. Have to set it
858                         // on the LayoutTransition.
859                         transition.setDuration(LayoutTransition.DISAPPEARING, 100);
860                         // CHANGE_DISAPPEARING plays after DISAPPEARING
861                         transition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 100);
862 
863                         transition.setDuration(LayoutTransition.CHANGE_DISAPPEARING, 100);
864 
865                         transition.setDuration(LayoutTransition.CHANGE_APPEARING, 100);
866                         // CHANGE_APPEARING plays after CHANGE_APPEARING
867                         transition.setStartDelay(LayoutTransition.APPEARING, 100);
868 
869                         transition.setDuration(LayoutTransition.APPEARING, 100);
870 
871                         return moveView(previousParent, newParentView, childView, index, params);
872                     }
873 
874                     @Override
875                     public void postAnimation() {
876                         previousParent.setLayoutTransition(null);
877                         newParentView.setLayoutTransition(null);
878                     }
879                 }.start();
880             }
881 
882             // always return success since the real status will come through the listener.
883             return SUCCESS.createResult(layoutParams);
884         }
885 
886         Result result = moveView(previousParent, newParentView, childView, index, layoutParams);
887         if (!result.isSuccess()) {
888             return result;
889         }
890 
891         result = render(false /*freshRender*/);
892         if (layoutParams != null && result.isSuccess()) {
893             result = result.getCopyWithData(layoutParams);
894         }
895 
896         return result;
897     }
898 
899     /**
900      * Moves a View from its current parent to a new given parent at a new given location, with
901      * an optional new {@link LayoutParams} instance
902      *
903      * @param previousParent the previous parent, still owning the child at the time of the call.
904      * @param newParent the new parent
905      * @param movedView the view to move
906      * @param index the new location in the new parent
907      * @param params an option (can be null) {@link LayoutParams} instance.
908      *
909      * @return a Result with {@link Status#SUCCESS} or
910      *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
911      *     adding views.
912      */
moveView(ViewGroup previousParent, final ViewGroup newParent, final View movedView, final int index, final LayoutParams params)913     private Result moveView(ViewGroup previousParent, final ViewGroup newParent,
914             final View movedView, final int index, final LayoutParams params) {
915         try {
916             // check if there is a transition on the previousParent.
917             LayoutTransition previousTransition = previousParent.getLayoutTransition();
918             if (previousTransition != null) {
919                 // in this case there is an animation. This means we have to wait for the child's
920                 // parent reference to be null'ed out so that we can add it to the new parent.
921                 // It is technically removed right before the DISAPPEARING animation is done (if
922                 // the animation of this type is not null, otherwise it's after which is impossible
923                 // to handle).
924                 // Because there is no move animation, if the new parent is the same as the old
925                 // parent, we need to wait until the CHANGE_DISAPPEARING animation is done before
926                 // adding the child or the child will appear in its new location before the
927                 // other children have made room for it.
928 
929                 // add a listener to the transition to be notified of the actual removal.
930                 previousTransition.addTransitionListener(new TransitionListener() {
931                     private int mChangeDisappearingCount = 0;
932 
933                     @Override
934                     public void startTransition(LayoutTransition transition, ViewGroup container,
935                             View view, int transitionType) {
936                         if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) {
937                             mChangeDisappearingCount++;
938                         }
939                     }
940 
941                     @Override
942                     public void endTransition(LayoutTransition transition, ViewGroup container,
943                             View view, int transitionType) {
944                         if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) {
945                             mChangeDisappearingCount--;
946                         }
947 
948                         if (transitionType == LayoutTransition.CHANGE_DISAPPEARING &&
949                                 mChangeDisappearingCount == 0) {
950                             // add it to the parentView in the correct location
951                             if (params != null) {
952                                 newParent.addView(movedView, index, params);
953                             } else {
954                                 newParent.addView(movedView, index);
955                             }
956                         }
957                     }
958                 });
959 
960                 // remove the view from the current parent.
961                 previousParent.removeView(movedView);
962 
963                 // and return since adding the view to the new parent is done in the listener.
964                 return SUCCESS.createResult();
965             } else {
966                 // standard code with no animation. pretty simple.
967                 previousParent.removeView(movedView);
968 
969                 // add it to the parentView in the correct location
970                 if (params != null) {
971                     newParent.addView(movedView, index, params);
972                 } else {
973                     newParent.addView(movedView, index);
974                 }
975 
976                 return SUCCESS.createResult();
977             }
978         } catch (UnsupportedOperationException e) {
979             // looks like this is a view class that doesn't support children manipulation!
980             return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
981         }
982     }
983 
984     /**
985      * Removes a child from its current parent.
986      * <p>
987      * {@link #acquire(long)} must have been called before this.
988      *
989      * @throws IllegalStateException if the current context is different than the one owned by
990      *      the scene, or if {@link #acquire(long)} was not called.
991      *
992      * @see RenderSession#removeChild(Object, IAnimationListener)
993      */
removeChild(final View childView, IAnimationListener listener)994     public Result removeChild(final View childView, IAnimationListener listener) {
995         checkLock();
996 
997         invalidateRenderingSize();
998 
999         final ViewGroup parent = (ViewGroup) childView.getParent();
1000 
1001         if (listener != null) {
1002             new AnimationThread(this, "moveChild", listener) {
1003 
1004                 @Override
1005                 public Result preAnimation() {
1006                     parent.setLayoutTransition(new LayoutTransition());
1007                     return removeView(parent, childView);
1008                 }
1009 
1010                 @Override
1011                 public void postAnimation() {
1012                     parent.setLayoutTransition(null);
1013                 }
1014             }.start();
1015 
1016             // always return success since the real status will come through the listener.
1017             return SUCCESS.createResult();
1018         }
1019 
1020         Result result = removeView(parent, childView);
1021         if (!result.isSuccess()) {
1022             return result;
1023         }
1024 
1025         return render(false /*freshRender*/);
1026     }
1027 
1028     /**
1029      * Removes a given view from its current parent.
1030      *
1031      * @param view the view to remove from its parent
1032      *
1033      * @return a Result with {@link Status#SUCCESS} or
1034      *     {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
1035      *     adding views.
1036      */
removeView(ViewGroup parent, View view)1037     private Result removeView(ViewGroup parent, View view) {
1038         try {
1039             parent.removeView(view);
1040             return SUCCESS.createResult();
1041         } catch (UnsupportedOperationException e) {
1042             // looks like this is a view class that doesn't support children manipulation!
1043             return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
1044         }
1045     }
1046 
1047 
findBackground(RenderResources resources)1048     private void findBackground(RenderResources resources) {
1049         if (!getParams().isBgColorOverridden()) {
1050             mWindowBackground = resources.findItemInTheme("windowBackground",
1051                     true /*isFrameworkAttr*/);
1052             if (mWindowBackground != null) {
1053                 mWindowBackground = resources.resolveResValue(mWindowBackground);
1054             }
1055         }
1056     }
1057 
hasSoftwareButtons()1058     private boolean hasSoftwareButtons() {
1059         return getParams().getHardwareConfig().hasSoftwareButtons();
1060     }
1061 
findStatusBar(RenderResources resources, DisplayMetrics metrics)1062     private void findStatusBar(RenderResources resources, DisplayMetrics metrics) {
1063         boolean windowFullscreen = getBooleanThemeValue(resources,
1064                 "windowFullscreen", false, !isThemeAppCompat(resources));
1065 
1066         if (!windowFullscreen && !mWindowIsFloating) {
1067             // default value
1068             mStatusBarSize = DEFAULT_STATUS_BAR_HEIGHT;
1069 
1070             // get the real value
1071             ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN,
1072                     "status_bar_height");
1073 
1074             if (value != null) {
1075                 TypedValue typedValue = ResourceHelper.getValue("status_bar_height",
1076                         value.getValue(), true /*requireUnit*/);
1077                 if (typedValue != null) {
1078                     // compute the pixel value based on the display metrics
1079                     mStatusBarSize = (int)typedValue.getDimension(metrics);
1080                 }
1081             }
1082         }
1083     }
1084 
findActionBar(RenderResources resources, DisplayMetrics metrics)1085     private void findActionBar(RenderResources resources, DisplayMetrics metrics) {
1086         if (mWindowIsFloating) {
1087             return;
1088         }
1089 
1090         boolean windowActionBar = getBooleanThemeValue(resources,
1091                 "windowActionBar", true, !isThemeAppCompat(resources));
1092 
1093         // if there's a value and it's false (default is true)
1094         if (windowActionBar) {
1095 
1096             // default size of the window title bar
1097             mActionBarSize = DEFAULT_TITLE_BAR_HEIGHT;
1098 
1099             // get value from the theme.
1100             ResourceValue value = resources.findItemInTheme("actionBarSize",
1101                     true /*isFrameworkAttr*/);
1102 
1103             // resolve it
1104             value = resources.resolveResValue(value);
1105 
1106             if (value != null) {
1107                 // get the numerical value, if available
1108                 TypedValue typedValue = ResourceHelper.getValue("actionBarSize", value.getValue(),
1109                         true /*requireUnit*/);
1110                 if (typedValue != null) {
1111                     // compute the pixel value based on the display metrics
1112                     mActionBarSize = (int)typedValue.getDimension(metrics);
1113                 }
1114             }
1115         } else {
1116             // action bar overrides title bar so only look for this one if action bar is hidden
1117             boolean windowNoTitle = getBooleanThemeValue(resources,
1118                     "windowNoTitle", false, !isThemeAppCompat(resources));
1119 
1120             if (!windowNoTitle) {
1121 
1122                 // default size of the window title bar
1123                 mTitleBarSize = DEFAULT_TITLE_BAR_HEIGHT;
1124 
1125                 // get value from the theme.
1126                 ResourceValue value = resources.findItemInTheme("windowTitleSize",
1127                         true /*isFrameworkAttr*/);
1128 
1129                 // resolve it
1130                 value = resources.resolveResValue(value);
1131 
1132                 if (value != null) {
1133                     // get the numerical value, if available
1134                     TypedValue typedValue = ResourceHelper.getValue("windowTitleSize",
1135                             value.getValue(), true /*requireUnit*/);
1136                     if (typedValue != null) {
1137                         // compute the pixel value based on the display metrics
1138                         mTitleBarSize = (int)typedValue.getDimension(metrics);
1139                     }
1140                 }
1141             }
1142 
1143         }
1144     }
1145 
findNavigationBar(RenderResources resources, DisplayMetrics metrics)1146     private void findNavigationBar(RenderResources resources, DisplayMetrics metrics) {
1147         if (hasSoftwareButtons() && !mWindowIsFloating) {
1148 
1149             // default value
1150             mNavigationBarSize = 48; // ??
1151 
1152             HardwareConfig hardwareConfig = getParams().getHardwareConfig();
1153 
1154             boolean barOnBottom = true;
1155 
1156             if (hardwareConfig.getOrientation() == ScreenOrientation.LANDSCAPE) {
1157                 // compute the dp of the screen.
1158                 int shortSize = hardwareConfig.getScreenHeight();
1159 
1160                 // compute in dp
1161                 int shortSizeDp = shortSize * DisplayMetrics.DENSITY_DEFAULT /
1162                         hardwareConfig.getDensity().getDpiValue();
1163 
1164                 // 0-599dp: "phone" UI with bar on the side
1165                 // 600+dp: "tablet" UI with bar on the bottom
1166                 barOnBottom = shortSizeDp >= 600;
1167             }
1168 
1169             if (barOnBottom) {
1170                 mNavigationBarOrientation = LinearLayout.HORIZONTAL;
1171             } else {
1172                 mNavigationBarOrientation = LinearLayout.VERTICAL;
1173             }
1174 
1175             // get the real value
1176             ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN,
1177                     barOnBottom ? "navigation_bar_height" : "navigation_bar_width");
1178 
1179             if (value != null) {
1180                 TypedValue typedValue = ResourceHelper.getValue("navigation_bar_height",
1181                         value.getValue(), true /*requireUnit*/);
1182                 if (typedValue != null) {
1183                     // compute the pixel value based on the display metrics
1184                     mNavigationBarSize = (int)typedValue.getDimension(metrics);
1185                 }
1186             }
1187         }
1188     }
1189 
isThemeAppCompat(RenderResources resources)1190     private boolean isThemeAppCompat(RenderResources resources) {
1191         // Ideally, we should check if the corresponding activity extends
1192         // android.support.v7.app.ActionBarActivity, and not care about the theme name at all.
1193         if (mIsThemeAppCompat == null) {
1194             StyleResourceValue defaultTheme = resources.getDefaultTheme();
1195           // We can't simply check for parent using resources.themeIsParentOf() since the
1196           // inheritance structure isn't really what one would expect. The first common parent
1197           // between Theme.AppCompat.Light and Theme.AppCompat is Theme.Material (for v21).
1198             boolean isThemeAppCompat = false;
1199             for (int i = 0; i < 50; i++) {
1200                 // for loop ensures that we don't run into cyclic theme inheritance.
1201                 if (defaultTheme.getName().startsWith("Theme.AppCompat")) {
1202                     isThemeAppCompat = true;
1203                     break;
1204                 }
1205                 defaultTheme = resources.getParent(defaultTheme);
1206                 if (defaultTheme == null) {
1207                     break;
1208                 }
1209             }
1210             mIsThemeAppCompat = isThemeAppCompat;
1211         }
1212         return mIsThemeAppCompat;
1213     }
1214 
1215     /**
1216      * Looks for an attribute in the current theme.
1217      *
1218      * @param resources the render resources
1219      * @param name the name of the attribute
1220      * @param defaultValue the default value.
1221      * @param isFrameworkAttr if the attribute is in android namespace
1222      * @return the value of the attribute or the default one if not found.
1223      */
getBooleanThemeValue(RenderResources resources, String name, boolean defaultValue, boolean isFrameworkAttr)1224     private boolean getBooleanThemeValue(RenderResources resources,
1225             String name, boolean defaultValue, boolean isFrameworkAttr) {
1226 
1227         ResourceValue value = resources.findItemInTheme(name, isFrameworkAttr);
1228 
1229         // because it may reference something else, we resolve it.
1230         value = resources.resolveResValue(value);
1231 
1232         // if there's no value, return the default.
1233         if (value == null || value.getValue() == null) {
1234             return defaultValue;
1235         }
1236 
1237         return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue);
1238     }
1239 
1240     /**
1241      * Post process on a view hierarchy that was just inflated.
1242      * <p/>
1243      * At the moment this only supports TabHost: If {@link TabHost} is detected, look for the
1244      * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically
1245      * based on the content of the {@link FrameLayout}.
1246      * @param view the root view to process.
1247      * @param projectCallback callback to the project.
1248      * @param skip the view and it's children are not processed.
1249      */
1250     @SuppressWarnings("deprecation")  // For the use of Pair
postInflateProcess(View view, IProjectCallback projectCallback, View skip)1251     private void postInflateProcess(View view, IProjectCallback projectCallback, View skip)
1252             throws PostInflateException {
1253         if (view == skip) {
1254             return;
1255         }
1256         if (view instanceof TabHost) {
1257             setupTabHost((TabHost) view, projectCallback);
1258         } else if (view instanceof QuickContactBadge) {
1259             QuickContactBadge badge = (QuickContactBadge) view;
1260             badge.setImageToDefault();
1261         } else if (view instanceof AdapterView<?>) {
1262             // get the view ID.
1263             int id = view.getId();
1264 
1265             BridgeContext context = getContext();
1266 
1267             // get a ResourceReference from the integer ID.
1268             ResourceReference listRef = context.resolveId(id);
1269 
1270             if (listRef != null) {
1271                 SessionParams params = getParams();
1272                 AdapterBinding binding = params.getAdapterBindings().get(listRef);
1273 
1274                 // if there was no adapter binding, trying to get it from the call back.
1275                 if (binding == null) {
1276                     binding = params.getProjectCallback().getAdapterBinding(listRef,
1277                             context.getViewKey(view), view);
1278                 }
1279 
1280                 if (binding != null) {
1281 
1282                     if (view instanceof AbsListView) {
1283                         if ((binding.getFooterCount() > 0 || binding.getHeaderCount() > 0) &&
1284                                 view instanceof ListView) {
1285                             ListView list = (ListView) view;
1286 
1287                             boolean skipCallbackParser = false;
1288 
1289                             int count = binding.getHeaderCount();
1290                             for (int i = 0; i < count; i++) {
1291                                 Pair<View, Boolean> pair = context.inflateView(
1292                                         binding.getHeaderAt(i),
1293                                         list, false /*attachToRoot*/, skipCallbackParser);
1294                                 if (pair.getFirst() != null) {
1295                                     list.addHeaderView(pair.getFirst());
1296                                 }
1297 
1298                                 skipCallbackParser |= pair.getSecond();
1299                             }
1300 
1301                             count = binding.getFooterCount();
1302                             for (int i = 0; i < count; i++) {
1303                                 Pair<View, Boolean> pair = context.inflateView(
1304                                         binding.getFooterAt(i),
1305                                         list, false /*attachToRoot*/, skipCallbackParser);
1306                                 if (pair.getFirst() != null) {
1307                                     list.addFooterView(pair.getFirst());
1308                                 }
1309 
1310                                 skipCallbackParser |= pair.getSecond();
1311                             }
1312                         }
1313 
1314                         if (view instanceof ExpandableListView) {
1315                             ((ExpandableListView) view).setAdapter(
1316                                     new FakeExpandableAdapter(
1317                                             listRef, binding, params.getProjectCallback()));
1318                         } else {
1319                             ((AbsListView) view).setAdapter(
1320                                     new FakeAdapter(
1321                                             listRef, binding, params.getProjectCallback()));
1322                         }
1323                     } else if (view instanceof AbsSpinner) {
1324                         ((AbsSpinner) view).setAdapter(
1325                                 new FakeAdapter(
1326                                         listRef, binding, params.getProjectCallback()));
1327                     }
1328                 }
1329             }
1330         } else if (view instanceof ViewGroup) {
1331             ViewGroup group = (ViewGroup) view;
1332             final int count = group.getChildCount();
1333             for (int c = 0; c < count; c++) {
1334                 View child = group.getChildAt(c);
1335                 postInflateProcess(child, projectCallback, skip);
1336             }
1337         }
1338     }
1339 
1340     /**
1341      * Sets up a {@link TabHost} object.
1342      * @param tabHost the TabHost to setup.
1343      * @param projectCallback The project callback object to access the project R class.
1344      * @throws PostInflateException
1345      */
setupTabHost(TabHost tabHost, IProjectCallback projectCallback)1346     private void setupTabHost(TabHost tabHost, IProjectCallback projectCallback)
1347             throws PostInflateException {
1348         // look for the TabWidget, and the FrameLayout. They have their own specific names
1349         View v = tabHost.findViewById(android.R.id.tabs);
1350 
1351         if (v == null) {
1352             throw new PostInflateException(
1353                     "TabHost requires a TabWidget with id \"android:id/tabs\".\n");
1354         }
1355 
1356         if (!(v instanceof TabWidget)) {
1357             throw new PostInflateException(String.format(
1358                     "TabHost requires a TabWidget with id \"android:id/tabs\".\n" +
1359                     "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName()));
1360         }
1361 
1362         v = tabHost.findViewById(android.R.id.tabcontent);
1363 
1364         if (v == null) {
1365             // TODO: see if we can fake tabs even without the FrameLayout (same below when the frameLayout is empty)
1366             //noinspection SpellCheckingInspection
1367             throw new PostInflateException(
1368                     "TabHost requires a FrameLayout with id \"android:id/tabcontent\".");
1369         }
1370 
1371         if (!(v instanceof FrameLayout)) {
1372             //noinspection SpellCheckingInspection
1373             throw new PostInflateException(String.format(
1374                     "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" +
1375                     "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName()));
1376         }
1377 
1378         FrameLayout content = (FrameLayout)v;
1379 
1380         // now process the content of the frameLayout and dynamically create tabs for it.
1381         final int count = content.getChildCount();
1382 
1383         // this must be called before addTab() so that the TabHost searches its TabWidget
1384         // and FrameLayout.
1385         tabHost.setup();
1386 
1387         if (count == 0) {
1388             // Create a dummy child to get a single tab
1389             TabSpec spec = tabHost.newTabSpec("tag").setIndicator("Tab Label",
1390                     tabHost.getResources().getDrawable(android.R.drawable.ic_menu_info_details))
1391                     .setContent(new TabHost.TabContentFactory() {
1392                         @Override
1393                         public View createTabContent(String tag) {
1394                             return new LinearLayout(getContext());
1395                         }
1396                     });
1397             tabHost.addTab(spec);
1398         } else {
1399             // for each child of the frameLayout, add a new TabSpec
1400             for (int i = 0 ; i < count ; i++) {
1401                 View child = content.getChildAt(i);
1402                 String tabSpec = String.format("tab_spec%d", i+1);
1403                 @SuppressWarnings("ConstantConditions")  // child cannot be null.
1404                 int id = child.getId();
1405                 @SuppressWarnings("deprecation")
1406                 Pair<ResourceType, String> resource = projectCallback.resolveResourceId(id);
1407                 String name;
1408                 if (resource != null) {
1409                     name = resource.getSecond();
1410                 } else {
1411                     name = String.format("Tab %d", i+1); // default name if id is unresolved.
1412                 }
1413                 tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id));
1414             }
1415         }
1416     }
1417 
1418     /**
1419      * Visits a {@link View} and its children and generate a {@link ViewInfo} containing the
1420      * bounds of all the views.
1421      *
1422      * @param view the root View
1423      * @param offset an offset for the view bounds.
1424      * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object.
1425      * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the
1426      *                       content frame.
1427      *
1428      * @return {@code ViewInfo} containing the bounds of the view and it children otherwise.
1429      */
visit(View view, int offset, boolean setExtendedInfo, boolean isContentFrame)1430     private ViewInfo visit(View view, int offset, boolean setExtendedInfo,
1431             boolean isContentFrame) {
1432         ViewInfo result = createViewInfo(view, offset, setExtendedInfo, isContentFrame);
1433 
1434         if (view instanceof ViewGroup) {
1435             ViewGroup group = ((ViewGroup) view);
1436             result.setChildren(visitAllChildren(group, isContentFrame ? 0 : offset,
1437                     setExtendedInfo, isContentFrame));
1438         }
1439         return result;
1440     }
1441 
1442     /**
1443      * Visits all the children of a given ViewGroup and generates a list of {@link ViewInfo}
1444      * containing the bounds of all the views. It also initializes the {@link #mViewInfoList} with
1445      * the children of the {@code mContentRoot}.
1446      *
1447      * @param viewGroup the root View
1448      * @param offset an offset from the top for the content view frame.
1449      * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object.
1450      * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the
1451      *                       content frame. {@code false} if the {@code ViewInfo} to be created is
1452      *                       part of the system decor.
1453      */
visitAllChildren(ViewGroup viewGroup, int offset, boolean setExtendedInfo, boolean isContentFrame)1454     private List<ViewInfo> visitAllChildren(ViewGroup viewGroup, int offset,
1455             boolean setExtendedInfo, boolean isContentFrame) {
1456         if (viewGroup == null) {
1457             return null;
1458         }
1459 
1460         if (!isContentFrame) {
1461             offset += viewGroup.getTop();
1462         }
1463 
1464         int childCount = viewGroup.getChildCount();
1465         if (viewGroup == mContentRoot) {
1466             List<ViewInfo> childrenWithoutOffset = new ArrayList<ViewInfo>(childCount);
1467             List<ViewInfo> childrenWithOffset = new ArrayList<ViewInfo>(childCount);
1468             for (int i = 0; i < childCount; i++) {
1469                 ViewInfo[] childViewInfo = visitContentRoot(viewGroup.getChildAt(i), offset,
1470                         setExtendedInfo);
1471                 childrenWithoutOffset.add(childViewInfo[0]);
1472                 childrenWithOffset.add(childViewInfo[1]);
1473             }
1474             mViewInfoList = childrenWithOffset;
1475             return childrenWithoutOffset;
1476         } else {
1477             List<ViewInfo> children = new ArrayList<ViewInfo>(childCount);
1478             for (int i = 0; i < childCount; i++) {
1479                 children.add(visit(viewGroup.getChildAt(i), offset, setExtendedInfo,
1480                         isContentFrame));
1481             }
1482             return children;
1483         }
1484     }
1485 
1486     /**
1487      * Visits the children of {@link #mContentRoot} and generates {@link ViewInfo} containing the
1488      * bounds of all the views. It returns two {@code ViewInfo} objects with the same children,
1489      * one with the {@code offset} and other without the {@code offset}. The offset is needed to
1490      * get the right bounds if the {@code ViewInfo} hierarchy is accessed from
1491      * {@code mViewInfoList}. When the hierarchy is accessed via {@code mSystemViewInfoList}, the
1492      * offset is not needed.
1493      *
1494      * @return an array of length two, with ViewInfo at index 0 is without offset and ViewInfo at
1495      *         index 1 is with the offset.
1496      */
visitContentRoot(View view, int offset, boolean setExtendedInfo)1497     private ViewInfo[] visitContentRoot(View view, int offset, boolean setExtendedInfo) {
1498         ViewInfo[] result = new ViewInfo[2];
1499         if (view == null) {
1500             return result;
1501         }
1502 
1503         result[0] = createViewInfo(view, 0, setExtendedInfo, true);
1504         result[1] = createViewInfo(view, offset, setExtendedInfo, true);
1505         if (view instanceof ViewGroup) {
1506             List<ViewInfo> children = visitAllChildren((ViewGroup) view, 0, setExtendedInfo, true);
1507             result[0].setChildren(children);
1508             result[1].setChildren(children);
1509         }
1510         return result;
1511     }
1512 
1513     /**
1514      * Creates a {@link ViewInfo} for the view. The {@code ViewInfo} corresponding to the children
1515      * of the {@code view} are not created. Consequently, the children of {@code ViewInfo} is not
1516      * set.
1517      * @param offset an offset for the view bounds. Used only if view is part of the content frame.
1518      */
createViewInfo(View view, int offset, boolean setExtendedInfo, boolean isContentFrame)1519     private ViewInfo createViewInfo(View view, int offset, boolean setExtendedInfo,
1520             boolean isContentFrame) {
1521         if (view == null) {
1522             return null;
1523         }
1524 
1525         ViewInfo result;
1526         if (isContentFrame) {
1527             // The view is part of the layout added by the user. Hence,
1528             // the ViewCookie may be obtained only through the Context.
1529             result = new ViewInfo(view.getClass().getName(),
1530                     getContext().getViewKey(view),
1531                     view.getLeft(), view.getTop() + offset, view.getRight(),
1532                     view.getBottom() + offset, view, view.getLayoutParams());
1533         } else {
1534             // We are part of the system decor.
1535             SystemViewInfo r = new SystemViewInfo(view.getClass().getName(),
1536                     getViewKey(view),
1537                     view.getLeft(), view.getTop(), view.getRight(),
1538                     view.getBottom(), view, view.getLayoutParams());
1539             result = r;
1540             // We currently mark three kinds of views:
1541             // 1. Menus in the Action Bar
1542             // 2. Menus in the Overflow popup.
1543             // 3. The overflow popup button.
1544             if (view instanceof ListMenuItemView) {
1545                 // Mark 2.
1546                 // All menus in the popup are of type ListMenuItemView.
1547                 r.setViewType(ViewType.ACTION_BAR_OVERFLOW_MENU);
1548             } else {
1549                 // Mark 3.
1550                 ViewGroup.LayoutParams lp = view.getLayoutParams();
1551                 if (lp instanceof ActionMenuView.LayoutParams &&
1552                         ((ActionMenuView.LayoutParams) lp).isOverflowButton) {
1553                     r.setViewType(ViewType.ACTION_BAR_OVERFLOW);
1554                 } else {
1555                     // Mark 1.
1556                     // A view is a menu in the Action Bar is it is not the overflow button and of
1557                     // its parent is of type ActionMenuView. We can also check if the view is
1558                     // instanceof ActionMenuItemView but that will fail for menus using
1559                     // actionProviderClass.
1560                     ViewParent parent = view.getParent();
1561                     while (parent != mViewRoot && parent instanceof ViewGroup) {
1562                         if (parent instanceof ActionMenuView) {
1563                             r.setViewType(ViewType.ACTION_BAR_MENU);
1564                             break;
1565                         }
1566                         parent = parent.getParent();
1567                     }
1568                 }
1569             }
1570         }
1571 
1572         if (setExtendedInfo) {
1573             MarginLayoutParams marginParams = null;
1574             LayoutParams params = view.getLayoutParams();
1575             if (params instanceof MarginLayoutParams) {
1576                 marginParams = (MarginLayoutParams) params;
1577             }
1578             result.setExtendedInfo(view.getBaseline(),
1579                     marginParams != null ? marginParams.leftMargin : 0,
1580                     marginParams != null ? marginParams.topMargin : 0,
1581                     marginParams != null ? marginParams.rightMargin : 0,
1582                     marginParams != null ? marginParams.bottomMargin : 0);
1583         }
1584 
1585         return result;
1586     }
1587 
1588     /* (non-Javadoc)
1589      * The cookie for menu items are stored in menu item and not in the map from View stored in
1590      * BridgeContext.
1591      */
getViewKey(View view)1592     private Object getViewKey(View view) {
1593         BridgeContext context = getContext();
1594         if (!(view instanceof MenuView.ItemView)) {
1595             return context.getViewKey(view);
1596         }
1597         MenuItemImpl menuItem;
1598         if (view instanceof ActionMenuItemView) {
1599             menuItem = ((ActionMenuItemView) view).getItemData();
1600         } else if (view instanceof ListMenuItemView) {
1601             menuItem = ((ListMenuItemView) view).getItemData();
1602         } else if (view instanceof IconMenuItemView) {
1603             menuItem = ((IconMenuItemView) view).getItemData();
1604         } else {
1605             menuItem = null;
1606         }
1607         if (menuItem instanceof BridgeMenuItemImpl) {
1608             return ((BridgeMenuItemImpl) menuItem).getViewCookie();
1609         }
1610 
1611         return null;
1612     }
1613 
invalidateRenderingSize()1614     private void invalidateRenderingSize() {
1615         mMeasuredScreenWidth = mMeasuredScreenHeight = -1;
1616     }
1617 
1618     /**
1619      * Creates the status bar with wifi and battery icons.
1620      */
createStatusBar(BridgeContext context, Density density, int direction, boolean isRtlSupported, int platformVersion)1621     private StatusBar createStatusBar(BridgeContext context, Density density, int direction,
1622             boolean isRtlSupported, int platformVersion) throws XmlPullParserException {
1623         StatusBar statusBar = new StatusBar(context, density,
1624                 direction, isRtlSupported, platformVersion);
1625         statusBar.setLayoutParams(
1626                 new LinearLayout.LayoutParams(
1627                         LayoutParams.MATCH_PARENT, mStatusBarSize));
1628         return statusBar;
1629     }
1630 
1631     /**
1632      * Creates the navigation bar with back, home and recent buttons.
1633      *
1634      * @param isRtl true if the current locale is right-to-left
1635      * @param isRtlSupported true is the project manifest declares that the application
1636      *        is RTL aware.
1637      */
createNavigationBar(BridgeContext context, Density density, boolean isRtl, boolean isRtlSupported, int simulatedPlatformVersion)1638     private NavigationBar createNavigationBar(BridgeContext context, Density density,
1639             boolean isRtl, boolean isRtlSupported, int simulatedPlatformVersion)
1640             throws XmlPullParserException {
1641         NavigationBar navigationBar = new NavigationBar(context,
1642                 density, mNavigationBarOrientation, isRtl,
1643                 isRtlSupported, simulatedPlatformVersion);
1644         if (mNavigationBarOrientation == LinearLayout.VERTICAL) {
1645             navigationBar.setLayoutParams(new LinearLayout.LayoutParams(mNavigationBarSize,
1646                     LayoutParams.MATCH_PARENT));
1647         } else {
1648             navigationBar.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
1649                     mNavigationBarSize));
1650         }
1651         return navigationBar;
1652     }
1653 
createTitleBar(BridgeContext context, String title, int simulatedPlatformVersion)1654     private TitleBar createTitleBar(BridgeContext context, String title,
1655             int simulatedPlatformVersion)
1656             throws XmlPullParserException {
1657         TitleBar titleBar = new TitleBar(context, title, simulatedPlatformVersion);
1658         titleBar.setLayoutParams(
1659                 new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, mTitleBarSize));
1660         return titleBar;
1661     }
1662 
1663     /**
1664      * Creates the action bar. Also queries the project callback for missing information.
1665      */
createActionBar(BridgeContext context, SessionParams params, ViewGroup parentView)1666     private BridgeActionBar createActionBar(BridgeContext context, SessionParams params,
1667             ViewGroup parentView) {
1668         if (mIsThemeAppCompat == Boolean.TRUE) {
1669             return new AppCompatActionBar(context, params, parentView);
1670         } else {
1671             return new FrameworkActionBar(context, params, parentView);
1672         }
1673     }
1674 
getImage()1675     public BufferedImage getImage() {
1676         return mImage;
1677     }
1678 
isAlphaChannelImage()1679     public boolean isAlphaChannelImage() {
1680         return mIsAlphaChannelImage;
1681     }
1682 
getViewInfos()1683     public List<ViewInfo> getViewInfos() {
1684         return mViewInfoList;
1685     }
1686 
getSystemViewInfos()1687     public List<ViewInfo> getSystemViewInfos() {
1688         return mSystemViewInfoList;
1689     }
1690 
getDefaultProperties(Object viewObject)1691     public Map<String, String> getDefaultProperties(Object viewObject) {
1692         return getContext().getDefaultPropMap(viewObject);
1693     }
1694 
setScene(RenderSession session)1695     public void setScene(RenderSession session) {
1696         mScene = session;
1697     }
1698 
getSession()1699     public RenderSession getSession() {
1700         return mScene;
1701     }
1702 }
1703