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.layoutlib.bridge.Bridge;
24import com.android.layoutlib.bridge.android.BridgeContext;
25import com.android.layoutlib.bridge.android.RenderParamsFlags;
26import com.android.layoutlib.bridge.bars.AppCompatActionBar;
27import com.android.layoutlib.bridge.bars.BridgeActionBar;
28import com.android.layoutlib.bridge.bars.Config;
29import com.android.layoutlib.bridge.bars.FrameworkActionBar;
30import com.android.layoutlib.bridge.bars.NavigationBar;
31import com.android.layoutlib.bridge.bars.StatusBar;
32import com.android.layoutlib.bridge.bars.TitleBar;
33import com.android.resources.Density;
34import com.android.resources.ResourceType;
35import com.android.resources.ScreenOrientation;
36
37import android.annotation.NonNull;
38import android.graphics.drawable.Drawable;
39import android.util.DisplayMetrics;
40import android.util.TypedValue;
41import android.view.AttachInfo_Accessor;
42import android.view.View;
43import android.view.ViewRootImpl;
44import android.view.ViewRootImpl_Accessor;
45import android.widget.FrameLayout;
46import android.widget.LinearLayout;
47import android.widget.RelativeLayout;
48
49import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
50import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
51import static android.widget.LinearLayout.VERTICAL;
52import static com.android.layoutlib.bridge.impl.ResourceHelper.getBooleanThemeValue;
53
54/**
55 * The Layout used to create the system decor.
56 * <p>
57 * The layout inflated will contain a content frame where the user's layout can be inflated.
58 * <pre>
59 *  +-------------------------------------------------+---+
60 *  | Status bar                                      | N |
61 *  +-------------------------------------------------+ a |
62 *  | Title/Framework Action bar (optional)           | v |
63 *  +-------------------------------------------------+   |
64 *  | AppCompat Action bar (optional)                 |   |
65 *  +-------------------------------------------------+   |
66 *  | Content, vertical extending                     | b |
67 *  |                                                 | a |
68 *  |                                                 | r |
69 *  +-------------------------------------------------+---+
70 * </pre>
71 * or
72 * <pre>
73 *  +--------------------------------------+
74 *  | Status bar                           |
75 *  +--------------------------------------+
76 *  | Title/Framework Action bar (optional)|
77 *  +--------------------------------------+
78 *  | AppCompat Action bar (optional)      |
79 *  +--------------------------------------+
80 *  | Content, vertical extending          |
81 *  |                                      |
82 *  |                                      |
83 *  +--------------------------------------+
84 *  | Nav bar                              |
85 *  +--------------------------------------+
86 * </pre>
87 */
88class Layout extends RelativeLayout {
89
90    // Theme attributes used for configuring appearance of the system decor.
91    private static final String ATTR_WINDOW_FLOATING = "windowIsFloating";
92    private static final String ATTR_WINDOW_BACKGROUND = "windowBackground";
93    private static final String ATTR_WINDOW_FULL_SCREEN = "windowFullscreen";
94    private static final String ATTR_NAV_BAR_HEIGHT = "navigation_bar_height";
95    private static final String ATTR_NAV_BAR_WIDTH = "navigation_bar_width";
96    private static final String ATTR_STATUS_BAR_HEIGHT = "status_bar_height";
97    private static final String ATTR_WINDOW_ACTION_BAR = "windowActionBar";
98    private static final String ATTR_ACTION_BAR_SIZE = "actionBarSize";
99    private static final String ATTR_WINDOW_NO_TITLE = "windowNoTitle";
100    private static final String ATTR_WINDOW_TITLE_SIZE = "windowTitleSize";
101    private static final String ATTR_WINDOW_TRANSLUCENT_STATUS = StatusBar.ATTR_TRANSLUCENT;
102    private static final String ATTR_WINDOW_TRANSLUCENT_NAV = NavigationBar.ATTR_TRANSLUCENT;
103
104    // Default sizes
105    private static final int DEFAULT_STATUS_BAR_HEIGHT = 25;
106    private static final int DEFAULT_TITLE_BAR_HEIGHT = 25;
107    private static final int DEFAULT_NAV_BAR_SIZE = 48;
108
109    // Ids assigned to components created. This is so that we can refer to other components in
110    // layout params.
111    private static final String ID_NAV_BAR = "navBar";
112    private static final String ID_STATUS_BAR = "statusBar";
113    private static final String ID_APP_COMPAT_ACTION_BAR = "appCompatActionBar";
114    private static final String ID_FRAMEWORK_BAR = "frameworkBar";
115    // Prefix used with the above ids in order to make them unique in framework namespace.
116    private static final String ID_PREFIX = "android_layoutlib_";
117
118    /**
119     * Temporarily store the builder so that it doesn't have to be passed to all methods used
120     * during inflation.
121     */
122    private Builder mBuilder;
123
124    /**
125     * This holds user's layout.
126     */
127    private FrameLayout mContentRoot;
128
129    public Layout(@NonNull Builder builder) {
130        super(builder.mContext);
131
132        mBuilder = builder;
133        View frameworkActionBar = null;
134        View appCompatActionBar = null;
135        TitleBar titleBar = null;
136        StatusBar statusBar = null;
137        NavigationBar navBar = null;
138
139        if (builder.mWindowBackground != null) {
140            Drawable d = ResourceHelper.getDrawable(builder.mWindowBackground, builder.mContext);
141            setBackground(d);
142        }
143
144        int simulatedPlatformVersion = getParams().getSimulatedPlatformVersion();
145        HardwareConfig hwConfig = getParams().getHardwareConfig();
146        Density density = hwConfig.getDensity();
147        boolean isRtl = Bridge.isLocaleRtl(getParams().getLocale());
148        setLayoutDirection(isRtl ? LAYOUT_DIRECTION_RTL : LAYOUT_DIRECTION_LTR);
149
150        if (mBuilder.hasNavBar()) {
151            navBar = createNavBar(getContext(), density, isRtl, getParams().isRtlSupported(),
152                    simulatedPlatformVersion);
153        }
154
155        if (builder.hasStatusBar()) {
156            statusBar = createStatusBar(getContext(), density, isRtl, getParams().isRtlSupported(),
157                    simulatedPlatformVersion);
158        }
159
160        if (mBuilder.hasAppCompatActionBar()) {
161            BridgeActionBar bar = createActionBar(getContext(), getParams(), true);
162            mContentRoot = bar.getContentRoot();
163            appCompatActionBar = bar.getRootView();
164        }
165
166        // Title bar must appear on top of the Action bar
167        if (mBuilder.hasTitleBar()) {
168            titleBar = createTitleBar(getContext(), getParams().getAppLabel(),
169                    simulatedPlatformVersion);
170        } else if (mBuilder.hasFrameworkActionBar()) {
171            BridgeActionBar bar = createActionBar(getContext(), getParams(), false);
172            if(mContentRoot == null) {
173                // We only set the content root if the AppCompat action bar did not already
174                // provide it
175                mContentRoot = bar.getContentRoot();
176            }
177            frameworkActionBar = bar.getRootView();
178        }
179
180        addViews(titleBar, mContentRoot == null ? (mContentRoot = createContentFrame()) : frameworkActionBar,
181                statusBar, navBar, appCompatActionBar);
182        // Done with the builder. Don't hold a reference to it.
183        mBuilder = null;
184    }
185
186    @NonNull
187    private FrameLayout createContentFrame() {
188        FrameLayout contentRoot = new FrameLayout(getContext());
189        LayoutParams params = createLayoutParams(MATCH_PARENT, MATCH_PARENT);
190        int rule = mBuilder.isNavBarVertical() ? START_OF : ABOVE;
191        if (mBuilder.hasSolidNavBar()) {
192            params.addRule(rule, getId(ID_NAV_BAR));
193        }
194        int below = -1;
195        if (mBuilder.mAppCompatActionBarSize > 0) {
196            below = getId(ID_APP_COMPAT_ACTION_BAR);
197        } else if (mBuilder.hasFrameworkActionBar() || mBuilder.hasTitleBar()) {
198            below = getId(ID_FRAMEWORK_BAR);
199        } else if (mBuilder.hasSolidStatusBar()) {
200            below = getId(ID_STATUS_BAR);
201        }
202        if (below != -1) {
203            params.addRule(BELOW, below);
204        }
205        contentRoot.setLayoutParams(params);
206        return contentRoot;
207    }
208
209    @NonNull
210    private LayoutParams createLayoutParams(int width, int height) {
211        DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
212        if (width > 0) {
213            width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, width, metrics);
214        }
215        if (height > 0) {
216            height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, height, metrics);
217        }
218        return new LayoutParams(width, height);
219    }
220
221    @NonNull
222    public FrameLayout getContentRoot() {
223        return mContentRoot;
224    }
225
226    @NonNull
227    private SessionParams getParams() {
228        return mBuilder.mParams;
229    }
230
231    @NonNull
232    @Override
233    public BridgeContext getContext() {
234        return (BridgeContext) super.getContext();
235    }
236
237    /**
238     * @param isRtl whether the current locale is an RTL locale.
239     * @param isRtlSupported whether the applications supports RTL (i.e. has supportsRtl=true in the
240     * manifest and targetSdkVersion >= 17.
241     */
242    @NonNull
243    private StatusBar createStatusBar(BridgeContext context, Density density, boolean isRtl,
244            boolean isRtlSupported, int simulatedPlatformVersion) {
245        StatusBar statusBar =
246                new StatusBar(context, density, isRtl, isRtlSupported, simulatedPlatformVersion);
247        LayoutParams params = createLayoutParams(MATCH_PARENT, mBuilder.mStatusBarSize);
248        if (mBuilder.isNavBarVertical()) {
249            params.addRule(START_OF, getId(ID_NAV_BAR));
250        }
251        statusBar.setLayoutParams(params);
252        statusBar.setId(getId(ID_STATUS_BAR));
253        return statusBar;
254    }
255
256    private BridgeActionBar createActionBar(@NonNull BridgeContext context,
257            @NonNull SessionParams params, boolean appCompatActionBar) {
258        boolean isMenu = "menu".equals(params.getFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG));
259        String id;
260
261        // For the framework action bar, we set the height to MATCH_PARENT only if there is no
262        // AppCompat ActionBar below it
263        int heightRule = appCompatActionBar || !mBuilder.hasAppCompatActionBar() ? MATCH_PARENT :
264          WRAP_CONTENT;
265        LayoutParams layoutParams = createLayoutParams(MATCH_PARENT, heightRule);
266        int rule = mBuilder.isNavBarVertical() ? START_OF : ABOVE;
267        if (mBuilder.hasSolidNavBar()) {
268            // If there
269            if(rule == START_OF || appCompatActionBar || !mBuilder.hasAppCompatActionBar()) {
270                layoutParams.addRule(rule, getId(ID_NAV_BAR));
271            }
272        }
273
274
275        BridgeActionBar actionBar;
276        if (appCompatActionBar && !isMenu) {
277            actionBar = new AppCompatActionBar(context, params);
278            id = ID_APP_COMPAT_ACTION_BAR;
279
280            if (mBuilder.hasTitleBar() || mBuilder.hasFrameworkActionBar()) {
281                layoutParams.addRule(BELOW, getId(ID_FRAMEWORK_BAR));
282            } else if (mBuilder.hasSolidStatusBar()) {
283                layoutParams.addRule(BELOW, getId(ID_STATUS_BAR));
284            }
285        } else {
286            actionBar = new FrameworkActionBar(context, params);
287            id = ID_FRAMEWORK_BAR;
288            if (mBuilder.hasSolidStatusBar()) {
289                layoutParams.addRule(BELOW, getId(ID_STATUS_BAR));
290            }
291        }
292
293        actionBar.getRootView().setLayoutParams(layoutParams);
294        actionBar.getRootView().setId(getId(id));
295        actionBar.createMenuPopup();
296        return actionBar;
297    }
298
299    @NonNull
300    private TitleBar createTitleBar(BridgeContext context, String title,
301            int simulatedPlatformVersion) {
302        TitleBar titleBar = new TitleBar(context, title, simulatedPlatformVersion);
303        LayoutParams params = createLayoutParams(MATCH_PARENT, mBuilder.mTitleBarSize);
304        if (mBuilder.hasSolidStatusBar()) {
305            params.addRule(BELOW, getId(ID_STATUS_BAR));
306        }
307        if (mBuilder.isNavBarVertical() && mBuilder.hasSolidNavBar()) {
308            params.addRule(START_OF, getId(ID_NAV_BAR));
309        }
310        titleBar.setLayoutParams(params);
311        titleBar.setId(getId(ID_FRAMEWORK_BAR));
312        return titleBar;
313    }
314
315    /**
316     * @param isRtl whether the current locale is an RTL locale.
317     * @param isRtlSupported whether the applications supports RTL (i.e. has supportsRtl=true in the
318     * manifest and targetSdkVersion >= 17.
319     */
320    @NonNull
321    private NavigationBar createNavBar(BridgeContext context, Density density, boolean isRtl,
322            boolean isRtlSupported, int simulatedPlatformVersion) {
323        int orientation = mBuilder.mNavBarOrientation;
324        int size = mBuilder.mNavBarSize;
325        NavigationBar navBar =
326                new NavigationBar(context, density, orientation, isRtl, isRtlSupported,
327                        simulatedPlatformVersion);
328        boolean isVertical = mBuilder.isNavBarVertical();
329        int w = isVertical ? size : MATCH_PARENT;
330        int h = isVertical ? MATCH_PARENT : size;
331        LayoutParams params = createLayoutParams(w, h);
332        params.addRule(isVertical ? ALIGN_PARENT_END : ALIGN_PARENT_BOTTOM);
333        navBar.setLayoutParams(params);
334        navBar.setId(getId(ID_NAV_BAR));
335        return navBar;
336    }
337
338    private void addViews(@NonNull View... views) {
339        for (View view : views) {
340            if (view != null) {
341                addView(view);
342            }
343        }
344    }
345
346    private int getId(String name) {
347        return Bridge.getResourceId(ResourceType.ID, ID_PREFIX + name);
348    }
349
350    @SuppressWarnings("deprecation")
351    @Override
352    public void requestFitSystemWindows() {
353        // The framework call would usually bubble up to ViewRootImpl but, in layoutlib, Layout will
354        // act as view root for most purposes. That way, we can also save going through the Handler
355        // to dispatch the new applied insets.
356        ViewRootImpl root = AttachInfo_Accessor.getRootView(this);
357        if (root != null) {
358            ViewRootImpl_Accessor.dispatchApplyInsets(root, this);
359        }
360    }
361
362    /**
363     * A helper class to help initialize the Layout.
364     */
365    static class Builder {
366        @NonNull
367        private final SessionParams mParams;
368        @NonNull
369        private final BridgeContext mContext;
370        private final RenderResources mResources;
371
372        private final boolean mWindowIsFloating;
373        private ResourceValue mWindowBackground;
374        private int mStatusBarSize;
375        private int mNavBarSize;
376        private int mNavBarOrientation;
377        private int mAppCompatActionBarSize;
378        private int mFrameworkActionBarSize;
379        private int mTitleBarSize;
380        private boolean mTranslucentStatus;
381        private boolean mTranslucentNav;
382
383        public Builder(@NonNull SessionParams params, @NonNull BridgeContext context) {
384            mParams = params;
385            mContext = context;
386            mResources = mParams.getResources();
387            mWindowIsFloating = getBooleanThemeValue(mResources, ATTR_WINDOW_FLOATING, true, true);
388
389            findBackground();
390
391            if (!mParams.isForceNoDecor()) {
392                findStatusBar();
393                findFrameworkBar();
394                findAppCompatActionBar();
395                findNavBar();
396            }
397        }
398
399        private void findBackground() {
400            if (!mParams.isBgColorOverridden()) {
401                mWindowBackground = mResources.findItemInTheme(ATTR_WINDOW_BACKGROUND, true);
402                mWindowBackground = mResources.resolveResValue(mWindowBackground);
403            }
404        }
405
406        private void findStatusBar() {
407            boolean windowFullScreen =
408                    getBooleanThemeValue(mResources, ATTR_WINDOW_FULL_SCREEN, true, false);
409            if (!windowFullScreen && !mWindowIsFloating) {
410                mStatusBarSize =
411                        getDimension(ATTR_STATUS_BAR_HEIGHT, true, DEFAULT_STATUS_BAR_HEIGHT);
412                mTranslucentStatus =
413                        getBooleanThemeValue(mResources, ATTR_WINDOW_TRANSLUCENT_STATUS, true,
414                                false);
415            }
416        }
417
418        /**
419         * The behavior is different wether the App is using AppCompat or not.
420         * <h1>With App compat :</h1>
421         * <li> framework ("android:") attributes have to effect
422         * <li> windowNoTile=true hides the AppCompatActionBar
423         * <li> windowActionBar=false throws an exception
424         */
425        private void findAppCompatActionBar() {
426            if (mWindowIsFloating || !mContext.isAppCompatTheme()) {
427                return;
428            }
429
430            boolean windowNoTitle =
431                    getBooleanThemeValue(mResources, ATTR_WINDOW_NO_TITLE, false, false);
432
433            boolean windowActionBar =
434                    getBooleanThemeValue(mResources, ATTR_WINDOW_ACTION_BAR, false, true);
435
436            if (!windowNoTitle && windowActionBar) {
437                mAppCompatActionBarSize =
438                        getDimension(ATTR_ACTION_BAR_SIZE, false, DEFAULT_TITLE_BAR_HEIGHT);
439            }
440        }
441
442        /**
443         * Find if we should show either the titleBar or the framework ActionBar
444         * <p>
445         * <h1> Without App compat :</h1>
446         * <li> windowNoTitle has no effect
447         * <li> android:windowNoTile=true hides the <b>ActionBar</b>
448         * <li> android:windowActionBar=true/false toggles between ActionBar/TitleBar
449         * </ul>
450         * <pre>
451         * +------------------------------------------------------------+
452         * |               |         android:windowNoTitle              |
453         * |android:       |    TRUE             |      FALSE           |
454         * |windowActionBar|---------------------+----------------------+
455         * |    TRUE       | Nothing             | ActionBar (Default)  |
456         * |    FALSE      | Nothing             | TitleBar             |
457         * +---------------+--------------------------------------------+
458         * </pre>
459         *
460         * @see #findAppCompatActionBar()
461         */
462        private void findFrameworkBar() {
463            if (mWindowIsFloating) {
464                return;
465            }
466            boolean frameworkWindowNoTitle =
467                    getBooleanThemeValue(mResources, ATTR_WINDOW_NO_TITLE, true, false);
468
469            // Check if an actionbar is needed
470            boolean isMenu = "menu".equals(mParams.getFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG));
471
472
473            boolean windowActionBar =
474                    getBooleanThemeValue(mResources, ATTR_WINDOW_ACTION_BAR, true, true);
475
476            if (!frameworkWindowNoTitle || isMenu) {
477                if (isMenu || windowActionBar) {
478                    mFrameworkActionBarSize =
479                            getDimension(ATTR_ACTION_BAR_SIZE, true, DEFAULT_TITLE_BAR_HEIGHT);
480                } else {
481                    mTitleBarSize =
482                            getDimension(ATTR_WINDOW_TITLE_SIZE, false, DEFAULT_TITLE_BAR_HEIGHT);
483                }
484            }
485        }
486
487        private void findNavBar() {
488            if (hasSoftwareButtons() && !mWindowIsFloating) {
489
490                // get orientation
491                HardwareConfig hwConfig = mParams.getHardwareConfig();
492                boolean barOnBottom = true;
493
494                if (hwConfig.getOrientation() == ScreenOrientation.LANDSCAPE) {
495                    int shortSize = hwConfig.getScreenHeight();
496                    int shortSizeDp = shortSize * DisplayMetrics.DENSITY_DEFAULT /
497                            hwConfig.getDensity().getDpiValue();
498
499                    // 0-599dp: "phone" UI with bar on the side
500                    // 600+dp: "tablet" UI with bar on the bottom
501                    barOnBottom = shortSizeDp >= 600;
502                }
503
504                mNavBarOrientation = barOnBottom ? LinearLayout.HORIZONTAL : VERTICAL;
505                mNavBarSize =
506                        getDimension(barOnBottom ? ATTR_NAV_BAR_HEIGHT : ATTR_NAV_BAR_WIDTH, true,
507                                DEFAULT_NAV_BAR_SIZE);
508                mTranslucentNav =
509                        getBooleanThemeValue(mResources, ATTR_WINDOW_TRANSLUCENT_NAV, true, false);
510            }
511        }
512
513        @SuppressWarnings("SameParameterValue")
514        private int getDimension(String attr, boolean isFramework, int defaultValue) {
515            ResourceValue value = mResources.findItemInTheme(attr, isFramework);
516            value = mResources.resolveResValue(value);
517            if (value != null) {
518                TypedValue typedValue = ResourceHelper.getValue(attr, value.getValue(), true);
519                if (typedValue != null) {
520                    return (int) typedValue.getDimension(mContext.getMetrics());
521                }
522            }
523            return defaultValue;
524        }
525
526        private boolean hasSoftwareButtons() {
527            return mParams.getHardwareConfig().hasSoftwareButtons();
528        }
529
530        /**
531         * Return true if the nav bar is present and not translucent
532         */
533        private boolean hasSolidNavBar() {
534            return hasNavBar() && !mTranslucentNav;
535        }
536
537        /**
538         * Return true if the status bar is present and not translucent
539         */
540        private boolean hasSolidStatusBar() {
541            return hasStatusBar() && !mTranslucentStatus;
542        }
543
544        private boolean hasNavBar() {
545            return Config.showOnScreenNavBar(mParams.getSimulatedPlatformVersion()) &&
546                    hasSoftwareButtons() && mNavBarSize > 0;
547        }
548
549        private boolean hasTitleBar() {
550            return mTitleBarSize > 0;
551        }
552
553        private boolean hasStatusBar() {
554            return mStatusBarSize > 0;
555        }
556
557        private boolean hasAppCompatActionBar() {
558            return mAppCompatActionBarSize > 0;
559        }
560
561        /**
562         * Return true if the nav bar is present and is vertical.
563         */
564        private boolean isNavBarVertical() {
565            return hasNavBar() && mNavBarOrientation == VERTICAL;
566        }
567
568        private boolean hasFrameworkActionBar() {
569            return mFrameworkActionBarSize > 0;
570        }
571    }
572}
573