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
17package com.android.layoutlib.bridge.impl;
18
19import com.android.ide.common.rendering.api.HardwareConfig;
20import com.android.ide.common.rendering.api.RenderResources;
21import com.android.ide.common.rendering.api.ResourceValue;
22import com.android.ide.common.rendering.api.SessionParams;
23import com.android.ide.common.rendering.api.StyleResourceValue;
24import com.android.layoutlib.bridge.Bridge;
25import com.android.layoutlib.bridge.android.BridgeContext;
26import com.android.layoutlib.bridge.android.RenderParamsFlags;
27import com.android.layoutlib.bridge.bars.AppCompatActionBar;
28import com.android.layoutlib.bridge.bars.BridgeActionBar;
29import com.android.layoutlib.bridge.bars.Config;
30import com.android.layoutlib.bridge.bars.FrameworkActionBar;
31import com.android.layoutlib.bridge.bars.NavigationBar;
32import com.android.layoutlib.bridge.bars.StatusBar;
33import com.android.layoutlib.bridge.bars.TitleBar;
34import com.android.resources.Density;
35import com.android.resources.ResourceType;
36import com.android.resources.ScreenOrientation;
37
38import android.annotation.NonNull;
39import android.graphics.drawable.Drawable;
40import android.util.DisplayMetrics;
41import android.util.TypedValue;
42import android.view.View;
43import android.widget.FrameLayout;
44import android.widget.LinearLayout;
45import android.widget.RelativeLayout;
46
47import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
48import static android.widget.LinearLayout.VERTICAL;
49import 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 */
82class 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
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
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
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
200    public FrameLayout getContentRoot() {
201        return mContentRoot;
202    }
203
204    @NonNull
205    private SessionParams getParams() {
206        return mBuilder.mParams;
207    }
208
209    @NonNull
210    @Override
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
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
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
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
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
295    private void addViews(@NonNull View... views) {
296        for (View view : views) {
297            if (view != null) {
298                addView(view);
299            }
300        }
301    }
302
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
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
344        private void findBackground() {
345            if (!mParams.isBgColorOverridden()) {
346                mWindowBackground = mResources.findItemInTheme(ATTR_WINDOW_BACKGROUND, true);
347                mWindowBackground = mResources.resolveResValue(mWindowBackground);
348            }
349        }
350
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
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
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
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
419        private boolean hasSoftwareButtons() {
420            return mParams.getHardwareConfig().hasSoftwareButtons();
421        }
422
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         */
454        private boolean solidBars() {
455            return !(hasNavBar() && mTranslucentNav) && !(hasStatusBar() && mTranslucentStatus);
456        }
457
458        private boolean hasNavBar() {
459            return Config.showOnScreenNavBar(mParams.getSimulatedPlatformVersion()) &&
460                    hasSoftwareButtons() && mNavBarSize > 0;
461        }
462
463        private boolean hasStatusBar() {
464            return mStatusBarSize > 0;
465        }
466
467        /**
468         * Return true if the nav bar is present and is vertical.
469         */
470        private boolean isNavBarVertical() {
471            return hasNavBar() && mNavBarOrientation == VERTICAL;
472        }
473    }
474}
475