1 /*
2  * Copyright (C) 2015 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 com.android.ide.common.rendering.api.HardwareConfig;
20 import com.android.ide.common.rendering.api.RenderResources;
21 import com.android.ide.common.rendering.api.ResourceValue;
22 import com.android.ide.common.rendering.api.SessionParams;
23 import com.android.ide.common.rendering.api.StyleResourceValue;
24 import com.android.layoutlib.bridge.Bridge;
25 import com.android.layoutlib.bridge.android.BridgeContext;
26 import com.android.layoutlib.bridge.android.RenderParamsFlags;
27 import com.android.layoutlib.bridge.bars.AppCompatActionBar;
28 import com.android.layoutlib.bridge.bars.BridgeActionBar;
29 import com.android.layoutlib.bridge.bars.Config;
30 import com.android.layoutlib.bridge.bars.FrameworkActionBar;
31 import com.android.layoutlib.bridge.bars.NavigationBar;
32 import com.android.layoutlib.bridge.bars.StatusBar;
33 import com.android.layoutlib.bridge.bars.TitleBar;
34 import com.android.resources.Density;
35 import com.android.resources.ResourceType;
36 import com.android.resources.ScreenOrientation;
37 
38 import android.annotation.NonNull;
39 import android.graphics.drawable.Drawable;
40 import android.util.DisplayMetrics;
41 import android.util.TypedValue;
42 import android.view.View;
43 import android.widget.FrameLayout;
44 import android.widget.LinearLayout;
45 import android.widget.RelativeLayout;
46 
47 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
48 import static android.widget.LinearLayout.VERTICAL;
49 import static com.android.layoutlib.bridge.impl.ResourceHelper.getBooleanThemeValue;
50 
51 /**
52  * The Layout used to create the system decor.
53  *
54  * The layout inflated will contain a content frame where the user's layout can be inflated.
55  * <pre>
56  *  +-------------------------------------------------+---+
57  *  | Status bar                                      | N |
58  *  +-------------------------------------------------+ a |
59  *  | Title/Action bar (optional)                     | v |
60  *  +-------------------------------------------------+   |
61  *  | Content, vertical extending                     | b |
62  *  |                                                 | a |
63  *  |                                                 | r |
64  *  +-------------------------------------------------+---+
65  * </pre>
66  * or
67  * <pre>
68  *  +-------------------------------------+
69  *  | Status bar                          |
70  *  +-------------------------------------+
71  *  | Title/Action bar (optional)         |
72  *  +-------------------------------------+
73  *  | Content, vertical extending         |
74  *  |                                     |
75  *  |                                     |
76  *  +-------------------------------------+
77  *  | Nav bar                             |
78  *  +-------------------------------------+
79  * </pre>
80  *
81  */
82 class Layout extends RelativeLayout {
83 
84     // Theme attributes used for configuring appearance of the system decor.
85     private static final String ATTR_WINDOW_FLOATING = "windowIsFloating";
86     private static final String ATTR_WINDOW_BACKGROUND = "windowBackground";
87     private static final String ATTR_WINDOW_FULL_SCREEN = "windowFullscreen";
88     private static final String ATTR_NAV_BAR_HEIGHT = "navigation_bar_height";
89     private static final String ATTR_NAV_BAR_WIDTH = "navigation_bar_width";
90     private static final String ATTR_STATUS_BAR_HEIGHT = "status_bar_height";
91     private static final String ATTR_WINDOW_ACTION_BAR = "windowActionBar";
92     private static final String ATTR_ACTION_BAR_SIZE = "actionBarSize";
93     private static final String ATTR_WINDOW_NO_TITLE = "windowNoTitle";
94     private static final String ATTR_WINDOW_TITLE_SIZE = "windowTitleSize";
95     private static final String ATTR_WINDOW_TRANSLUCENT_STATUS = StatusBar.ATTR_TRANSLUCENT;
96     private static final String ATTR_WINDOW_TRANSLUCENT_NAV = NavigationBar.ATTR_TRANSLUCENT;
97     private static final String PREFIX_THEME_APPCOMPAT = "Theme.AppCompat";
98 
99     // Default sizes
100     private static final int DEFAULT_STATUS_BAR_HEIGHT = 25;
101     private static final int DEFAULT_TITLE_BAR_HEIGHT = 25;
102     private static final int DEFAULT_NAV_BAR_SIZE = 48;
103 
104     // Ids assigned to components created. This is so that we can refer to other components in
105     // layout params.
106     private static final String ID_NAV_BAR = "navBar";
107     private static final String ID_STATUS_BAR = "statusBar";
108     private static final String ID_TITLE_BAR = "titleBar";
109     // Prefix used with the above ids in order to make them unique in framework namespace.
110     private static final String ID_PREFIX = "android_layoutlib_";
111 
112     /**
113      * Temporarily store the builder so that it doesn't have to be passed to all methods used
114      * during inflation.
115      */
116     private Builder mBuilder;
117 
118     /**
119      * This holds user's layout.
120      */
121     private FrameLayout mContentRoot;
122 
Layout(@onNull Builder builder)123     public Layout(@NonNull Builder builder) {
124         super(builder.mContext);
125         mBuilder = builder;
126         if (builder.mWindowBackground != null) {
127             Drawable d = ResourceHelper.getDrawable(builder.mWindowBackground, builder.mContext);
128             setBackground(d);
129         }
130 
131         int simulatedPlatformVersion = getParams().getSimulatedPlatformVersion();
132         HardwareConfig hwConfig = getParams().getHardwareConfig();
133         Density density = hwConfig.getDensity();
134         boolean isRtl = Bridge.isLocaleRtl(getParams().getLocale());
135         setLayoutDirection(isRtl? LAYOUT_DIRECTION_RTL : LAYOUT_DIRECTION_LTR);
136 
137         NavigationBar navBar = null;
138         if (mBuilder.hasNavBar()) {
139             navBar = createNavBar(getContext(), density, isRtl, getParams().isRtlSupported(),
140                     simulatedPlatformVersion);
141         }
142 
143         StatusBar statusBar = null;
144         if (builder.mStatusBarSize > 0) {
145             statusBar = createStatusBar(getContext(), density, isRtl, getParams().isRtlSupported(),
146                     simulatedPlatformVersion);
147         }
148 
149         View actionBar = null;
150         TitleBar titleBar = null;
151         if (builder.mActionBarSize > 0) {
152             BridgeActionBar bar = createActionBar(getContext(), getParams());
153             mContentRoot = bar.getContentRoot();
154             actionBar = bar.getRootView();
155         } else if (mBuilder.mTitleBarSize > 0) {
156             titleBar = createTitleBar(getContext(), getParams().getAppLabel(),
157                     simulatedPlatformVersion);
158         }
159 
160         addViews(titleBar, mContentRoot == null ? (mContentRoot = createContentFrame()) : actionBar,
161                 statusBar, navBar);
162         // Done with the builder. Don't hold a reference to it.
163         mBuilder = null;
164      }
165 
166     @NonNull
createContentFrame()167     private FrameLayout createContentFrame() {
168         FrameLayout contentRoot = new FrameLayout(getContext());
169         LayoutParams params = createLayoutParams(MATCH_PARENT, MATCH_PARENT);
170         int rule = mBuilder.isNavBarVertical() ? START_OF : ABOVE;
171         if (mBuilder.hasNavBar() && mBuilder.solidBars()) {
172             params.addRule(rule, getId(ID_NAV_BAR));
173         }
174         int below = -1;
175         if (mBuilder.mActionBarSize <= 0 && mBuilder.mTitleBarSize > 0) {
176             below = getId(ID_TITLE_BAR);
177         } else if (mBuilder.hasStatusBar() && mBuilder.solidBars()) {
178             below = getId(ID_STATUS_BAR);
179         }
180         if (below != -1) {
181             params.addRule(BELOW, below);
182         }
183         contentRoot.setLayoutParams(params);
184         return contentRoot;
185     }
186 
187     @NonNull
createLayoutParams(int width, int height)188     private LayoutParams createLayoutParams(int width, int height) {
189         DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
190         if (width > 0) {
191             width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, width, metrics);
192         }
193         if (height > 0) {
194             height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, height, metrics);
195         }
196         return new LayoutParams(width, height);
197     }
198 
199     @NonNull
getContentRoot()200     public FrameLayout getContentRoot() {
201         return mContentRoot;
202     }
203 
204     @NonNull
getParams()205     private SessionParams getParams() {
206         return mBuilder.mParams;
207     }
208 
209     @NonNull
210     @Override
getContext()211     public BridgeContext getContext(){
212         return (BridgeContext) super.getContext();
213     }
214 
215     /**
216      * @param isRtl    whether the current locale is an RTL locale.
217      * @param isRtlSupported    whether the applications supports RTL (i.e. has supportsRtl=true
218      * in the manifest and targetSdkVersion >= 17.
219      */
220     @NonNull
createStatusBar(BridgeContext context, Density density, boolean isRtl, boolean isRtlSupported, int simulatedPlatformVersion)221     private StatusBar createStatusBar(BridgeContext context, Density density, boolean isRtl,
222             boolean isRtlSupported, int simulatedPlatformVersion) {
223         StatusBar statusBar =
224                 new StatusBar(context, density, isRtl, isRtlSupported, simulatedPlatformVersion);
225         LayoutParams params = createLayoutParams(MATCH_PARENT, mBuilder.mStatusBarSize);
226         if (mBuilder.isNavBarVertical()) {
227             params.addRule(START_OF, getId(ID_NAV_BAR));
228         }
229         statusBar.setLayoutParams(params);
230         statusBar.setId(getId(ID_STATUS_BAR));
231         return statusBar;
232     }
233 
createActionBar(@onNull BridgeContext context, @NonNull SessionParams params)234     private BridgeActionBar createActionBar(@NonNull BridgeContext context,
235             @NonNull SessionParams params) {
236         boolean isMenu = "menu".equals(params.getFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG));
237 
238         BridgeActionBar actionBar;
239         if (mBuilder.isThemeAppCompat() && !isMenu) {
240             actionBar = new AppCompatActionBar(context, params);
241         } else {
242             actionBar = new FrameworkActionBar(context, params);
243         }
244         LayoutParams layoutParams = createLayoutParams(MATCH_PARENT, MATCH_PARENT);
245         int rule = mBuilder.isNavBarVertical() ? START_OF : ABOVE;
246         if (mBuilder.hasNavBar() && mBuilder.solidBars()) {
247             layoutParams.addRule(rule, getId(ID_NAV_BAR));
248         }
249         if (mBuilder.hasStatusBar() && mBuilder.solidBars()) {
250             layoutParams.addRule(BELOW, getId(ID_STATUS_BAR));
251         }
252         actionBar.getRootView().setLayoutParams(layoutParams);
253         actionBar.createMenuPopup();
254         return actionBar;
255     }
256 
257     @NonNull
createTitleBar(BridgeContext context, String title, int simulatedPlatformVersion)258     private TitleBar createTitleBar(BridgeContext context, String title,
259             int simulatedPlatformVersion) {
260         TitleBar titleBar = new TitleBar(context, title, simulatedPlatformVersion);
261         LayoutParams params = createLayoutParams(MATCH_PARENT, mBuilder.mTitleBarSize);
262         if (mBuilder.hasStatusBar() && mBuilder.solidBars()) {
263             params.addRule(BELOW, getId(ID_STATUS_BAR));
264         }
265         if (mBuilder.isNavBarVertical() && mBuilder.solidBars()) {
266             params.addRule(START_OF, getId(ID_NAV_BAR));
267         }
268         titleBar.setLayoutParams(params);
269         titleBar.setId(getId(ID_TITLE_BAR));
270         return titleBar;
271     }
272 
273     /**
274      * @param isRtl    whether the current locale is an RTL locale.
275      * @param isRtlSupported    whether the applications supports RTL (i.e. has supportsRtl=true
276      * in the manifest and targetSdkVersion >= 17.
277      */
278     @NonNull
createNavBar(BridgeContext context, Density density, boolean isRtl, boolean isRtlSupported, int simulatedPlatformVersion)279     private NavigationBar createNavBar(BridgeContext context, Density density, boolean isRtl,
280             boolean isRtlSupported, int simulatedPlatformVersion) {
281         int orientation = mBuilder.mNavBarOrientation;
282         int size = mBuilder.mNavBarSize;
283         NavigationBar navBar = new NavigationBar(context, density, orientation, isRtl,
284                 isRtlSupported, simulatedPlatformVersion);
285         boolean isVertical = mBuilder.isNavBarVertical();
286         int w = isVertical ? size : MATCH_PARENT;
287         int h = isVertical ? MATCH_PARENT : size;
288         LayoutParams params = createLayoutParams(w, h);
289         params.addRule(isVertical ? ALIGN_PARENT_END : ALIGN_PARENT_BOTTOM);
290         navBar.setLayoutParams(params);
291         navBar.setId(getId(ID_NAV_BAR));
292         return navBar;
293     }
294 
addViews(@onNull View... views)295     private void addViews(@NonNull View... views) {
296         for (View view : views) {
297             if (view != null) {
298                 addView(view);
299             }
300         }
301     }
302 
getId(String name)303     private int getId(String name) {
304         return Bridge.getResourceId(ResourceType.ID, ID_PREFIX + name);
305     }
306 
307     /**
308      * A helper class to help initialize the Layout.
309      */
310     static class Builder {
311         @NonNull
312         private final SessionParams mParams;
313         @NonNull
314         private final BridgeContext mContext;
315         private final RenderResources mResources;
316 
317         private final boolean mWindowIsFloating;
318         private ResourceValue mWindowBackground;
319         private int mStatusBarSize;
320         private int mNavBarSize;
321         private int mNavBarOrientation;
322         private int mActionBarSize;
323         private int mTitleBarSize;
324         private boolean mTranslucentStatus;
325         private boolean mTranslucentNav;
326 
327         private Boolean mIsThemeAppCompat;
328 
Builder(@onNull SessionParams params, @NonNull BridgeContext context)329         public Builder(@NonNull SessionParams params, @NonNull BridgeContext context) {
330             mParams = params;
331             mContext = context;
332             mResources = mParams.getResources();
333             mWindowIsFloating = getBooleanThemeValue(mResources, ATTR_WINDOW_FLOATING, true, true);
334 
335             findBackground();
336 
337             if (!mParams.isForceNoDecor()) {
338                 findStatusBar();
339                 findActionBar();
340                 findNavBar();
341             }
342         }
343 
findBackground()344         private void findBackground() {
345             if (!mParams.isBgColorOverridden()) {
346                 mWindowBackground = mResources.findItemInTheme(ATTR_WINDOW_BACKGROUND, true);
347                 mWindowBackground = mResources.resolveResValue(mWindowBackground);
348             }
349         }
350 
findStatusBar()351         private void findStatusBar() {
352             boolean windowFullScreen =
353                     getBooleanThemeValue(mResources, ATTR_WINDOW_FULL_SCREEN, true, false);
354             if (!windowFullScreen && !mWindowIsFloating) {
355                 mStatusBarSize =
356                         getDimension(ATTR_STATUS_BAR_HEIGHT, true, DEFAULT_STATUS_BAR_HEIGHT);
357                 mTranslucentStatus = getBooleanThemeValue(mResources,
358                         ATTR_WINDOW_TRANSLUCENT_STATUS, true, false);
359             }
360         }
361 
findActionBar()362         private void  findActionBar() {
363             if (mWindowIsFloating) {
364                 return;
365             }
366             // Check if an actionbar is needed
367             boolean windowActionBar = getBooleanThemeValue(mResources, ATTR_WINDOW_ACTION_BAR,
368                     !isThemeAppCompat(), true);
369             if (windowActionBar) {
370                 mActionBarSize = getDimension(ATTR_ACTION_BAR_SIZE, true, DEFAULT_TITLE_BAR_HEIGHT);
371             } else {
372                 // Maybe the gingerbread era title bar is needed
373                 boolean windowNoTitle =
374                         getBooleanThemeValue(mResources, ATTR_WINDOW_NO_TITLE, true, false);
375                 if (!windowNoTitle) {
376                     mTitleBarSize =
377                             getDimension(ATTR_WINDOW_TITLE_SIZE, true, DEFAULT_TITLE_BAR_HEIGHT);
378                 }
379             }
380         }
381 
findNavBar()382         private void findNavBar() {
383             if (hasSoftwareButtons() && !mWindowIsFloating) {
384 
385                 // get orientation
386                 HardwareConfig hwConfig = mParams.getHardwareConfig();
387                 boolean barOnBottom = true;
388 
389                 if (hwConfig.getOrientation() == ScreenOrientation.LANDSCAPE) {
390                     int shortSize = hwConfig.getScreenHeight();
391                     int shortSizeDp = shortSize * DisplayMetrics.DENSITY_DEFAULT /
392                             hwConfig.getDensity().getDpiValue();
393 
394                     // 0-599dp: "phone" UI with bar on the side
395                     // 600+dp: "tablet" UI with bar on the bottom
396                     barOnBottom = shortSizeDp >= 600;
397                 }
398 
399                 mNavBarOrientation = barOnBottom ? LinearLayout.HORIZONTAL : VERTICAL;
400                 mNavBarSize = getDimension(barOnBottom ? ATTR_NAV_BAR_HEIGHT : ATTR_NAV_BAR_WIDTH,
401                         true, DEFAULT_NAV_BAR_SIZE);
402                 mTranslucentNav = getBooleanThemeValue(mResources,
403                         ATTR_WINDOW_TRANSLUCENT_NAV, true, false);
404             }
405         }
406 
getDimension(String attr, boolean isFramework, int defaultValue)407         private int getDimension(String attr, boolean isFramework, int defaultValue) {
408             ResourceValue value = mResources.findItemInTheme(attr, isFramework);
409             value = mResources.resolveResValue(value);
410             if (value != null) {
411                 TypedValue typedValue = ResourceHelper.getValue(attr, value.getValue(), true);
412                 if (typedValue != null) {
413                     return (int) typedValue.getDimension(mContext.getMetrics());
414                 }
415             }
416             return defaultValue;
417         }
418 
hasSoftwareButtons()419         private boolean hasSoftwareButtons() {
420             return mParams.getHardwareConfig().hasSoftwareButtons();
421         }
422 
isThemeAppCompat()423         private boolean isThemeAppCompat() {
424             // If a cached value exists, return it.
425             if (mIsThemeAppCompat != null) {
426                 return mIsThemeAppCompat;
427             }
428             // Ideally, we should check if the corresponding activity extends
429             // android.support.v7.app.ActionBarActivity, and not care about the theme name at all.
430             StyleResourceValue defaultTheme = mResources.getDefaultTheme();
431             // We can't simply check for parent using resources.themeIsParentOf() since the
432             // inheritance structure isn't really what one would expect. The first common parent
433             // between Theme.AppCompat.Light and Theme.AppCompat is Theme.Material (for v21).
434             boolean isThemeAppCompat = false;
435             for (int i = 0; i < 50; i++) {
436                 if (defaultTheme == null) {
437                     break;
438                 }
439                 // for loop ensures that we don't run into cyclic theme inheritance.
440                 if (defaultTheme.getName().startsWith(PREFIX_THEME_APPCOMPAT)) {
441                     isThemeAppCompat = true;
442                     break;
443                 }
444                 defaultTheme = mResources.getParent(defaultTheme);
445             }
446             mIsThemeAppCompat = isThemeAppCompat;
447             return isThemeAppCompat;
448         }
449 
450         /**
451          * Return true if the status bar or nav bar are present, they are not translucent (i.e
452          * content doesn't overlap with them).
453          */
solidBars()454         private boolean solidBars() {
455             return !(hasNavBar() && mTranslucentNav) && !(hasStatusBar() && mTranslucentStatus);
456         }
457 
hasNavBar()458         private boolean hasNavBar() {
459             return Config.showOnScreenNavBar(mParams.getSimulatedPlatformVersion()) &&
460                     hasSoftwareButtons() && mNavBarSize > 0;
461         }
462 
hasStatusBar()463         private boolean hasStatusBar() {
464             return mStatusBarSize > 0;
465         }
466 
467         /**
468          * Return true if the nav bar is present and is vertical.
469          */
isNavBarVertical()470         private boolean isNavBarVertical() {
471             return hasNavBar() && mNavBarOrientation == VERTICAL;
472         }
473     }
474 }
475