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