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.setupwizardlib.util;
18
19import android.annotation.SuppressLint;
20import android.annotation.TargetApi;
21import android.app.Dialog;
22import android.content.Context;
23import android.content.res.TypedArray;
24import android.os.Build.VERSION;
25import android.os.Build.VERSION_CODES;
26import android.os.Handler;
27import android.util.Log;
28import android.view.View;
29import android.view.ViewGroup;
30import android.view.Window;
31import android.view.WindowInsets;
32import android.view.WindowManager;
33
34import com.android.setupwizardlib.R;
35
36/**
37 * A helper class to manage the system navigation bar and status bar. This will add various
38 * systemUiVisibility flags to the given Window or View to make them follow the Setup Wizard style.
39 *
40 * When the useImmersiveMode intent extra is true, a screen in Setup Wizard should hide the system
41 * bars using methods from this class. For Lollipop, {@link #hideSystemBars(android.view.Window)}
42 * will completely hide the system navigation bar and change the status bar to transparent, and
43 * layout the screen contents (usually the illustration) behind it.
44 */
45public class SystemBarHelper {
46
47    private static final String TAG = "SystemBarHelper";
48
49    @SuppressLint("InlinedApi")
50    private static final int DEFAULT_IMMERSIVE_FLAGS =
51            View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
52            | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
53            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
54            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
55
56    @SuppressLint("InlinedApi")
57    private static final int DIALOG_IMMERSIVE_FLAGS =
58            View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
59            | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
60
61    /**
62     * Needs to be equal to View.STATUS_BAR_DISABLE_BACK
63     */
64    private static final int STATUS_BAR_DISABLE_BACK = 0x00400000;
65
66    /**
67     * The maximum number of retries when peeking the decor view. When polling for the decor view,
68     * waiting it to be installed, set a maximum number of retries.
69     */
70    private static final int PEEK_DECOR_VIEW_RETRIES = 3;
71
72    /**
73     * Hide the navigation bar for a dialog.
74     *
75     * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op.
76     */
77    public static void hideSystemBars(final Dialog dialog) {
78        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
79            final Window window = dialog.getWindow();
80            temporarilyDisableDialogFocus(window);
81            addVisibilityFlag(window, DIALOG_IMMERSIVE_FLAGS);
82            addImmersiveFlagsToDecorView(window, DIALOG_IMMERSIVE_FLAGS);
83
84            // Also set the navigation bar and status bar to transparent color. Note that this
85            // doesn't work if android.R.boolean.config_enableTranslucentDecor is false.
86            window.setNavigationBarColor(0);
87            window.setStatusBarColor(0);
88        }
89    }
90
91    /**
92     * Hide the navigation bar, make the color of the status and navigation bars transparent, and
93     * specify {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} flag so that the content is laid-out
94     * behind the transparent status bar. This is commonly used with
95     * {@link android.app.Activity#getWindow()} to make the navigation and status bars follow the
96     * Setup Wizard style.
97     *
98     * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op.
99     */
100    public static void hideSystemBars(final Window window) {
101        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
102            addVisibilityFlag(window, DEFAULT_IMMERSIVE_FLAGS);
103            addImmersiveFlagsToDecorView(window, DEFAULT_IMMERSIVE_FLAGS);
104
105            // Also set the navigation bar and status bar to transparent color. Note that this
106            // doesn't work if android.R.boolean.config_enableTranslucentDecor is false.
107            window.setNavigationBarColor(0);
108            window.setStatusBarColor(0);
109        }
110    }
111
112    /**
113     * Revert the actions of hideSystemBars. Note that this will remove the system UI visibility
114     * flags regardless of whether it is originally present. You should also manually reset the
115     * navigation bar and status bar colors, as this method doesn't know what value to revert it to.
116     */
117    public static void showSystemBars(final Dialog dialog, final Context context) {
118        showSystemBars(dialog.getWindow(), context);
119    }
120
121    /**
122     * Revert the actions of hideSystemBars. Note that this will remove the system UI visibility
123     * flags regardless of whether it is originally present. You should also manually reset the
124     * navigation bar and status bar colors, as this method doesn't know what value to revert it to.
125     */
126    public static void showSystemBars(final Window window, final Context context) {
127        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
128            removeVisibilityFlag(window, DEFAULT_IMMERSIVE_FLAGS);
129            removeImmersiveFlagsFromDecorView(window, DEFAULT_IMMERSIVE_FLAGS);
130
131            if (context != null) {
132                //noinspection AndroidLintInlinedApi
133                final TypedArray typedArray = context.obtainStyledAttributes(new int[]{
134                        android.R.attr.statusBarColor, android.R.attr.navigationBarColor});
135                final int statusBarColor = typedArray.getColor(0, 0);
136                final int navigationBarColor = typedArray.getColor(1, 0);
137                window.setStatusBarColor(statusBarColor);
138                window.setNavigationBarColor(navigationBarColor);
139                typedArray.recycle();
140            }
141        }
142    }
143
144    /**
145     * Convenience method to add a visibility flag in addition to the existing ones.
146     */
147    public static void addVisibilityFlag(final View view, final int flag) {
148        if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
149            final int vis = view.getSystemUiVisibility();
150            view.setSystemUiVisibility(vis | flag);
151        }
152    }
153
154    /**
155     * Convenience method to add a visibility flag in addition to the existing ones.
156     */
157    public static void addVisibilityFlag(final Window window, final int flag) {
158        if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
159            WindowManager.LayoutParams attrs = window.getAttributes();
160            attrs.systemUiVisibility |= flag;
161            window.setAttributes(attrs);
162        }
163    }
164
165    /**
166     * Convenience method to remove a visibility flag from the view, leaving other flags that are
167     * not specified intact.
168     */
169    public static void removeVisibilityFlag(final View view, final int flag) {
170        if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
171            final int vis = view.getSystemUiVisibility();
172            view.setSystemUiVisibility(vis & ~flag);
173        }
174    }
175
176    /**
177     * Convenience method to remove a visibility flag from the window, leaving other flags that are
178     * not specified intact.
179     */
180    public static void removeVisibilityFlag(final Window window, final int flag) {
181        if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
182            WindowManager.LayoutParams attrs = window.getAttributes();
183            attrs.systemUiVisibility &= ~flag;
184            window.setAttributes(attrs);
185        }
186    }
187
188    public static void setBackButtonVisible(final Window window, final boolean visible) {
189        if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
190            if (visible) {
191                removeVisibilityFlag(window, STATUS_BAR_DISABLE_BACK);
192            } else {
193                addVisibilityFlag(window, STATUS_BAR_DISABLE_BACK);
194            }
195        }
196    }
197
198    /**
199     * Set a view to be resized when the keyboard is shown. This will set the bottom margin of the
200     * view to be immediately above the keyboard, and assumes that the view sits immediately above
201     * the navigation bar.
202     *
203     * <p>Note that you must set {@link android.R.attr#windowSoftInputMode} to {@code adjustResize}
204     * for this class to work. Otherwise window insets are not dispatched and this method will have
205     * no effect.
206     *
207     * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op.
208     *
209     * @param view The view to be resized when the keyboard is shown.
210     */
211    public static void setImeInsetView(final View view) {
212        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
213            view.setOnApplyWindowInsetsListener(new WindowInsetsListener());
214        }
215    }
216
217    /**
218     * Add the specified immersive flags to the decor view of the window, because
219     * {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} only takes effect when it is added to a view
220     * instead of the window.
221     */
222    @TargetApi(VERSION_CODES.LOLLIPOP)
223    private static void addImmersiveFlagsToDecorView(final Window window, final int vis) {
224        getDecorView(window, new OnDecorViewInstalledListener() {
225            @Override
226            public void onDecorViewInstalled(View decorView) {
227                addVisibilityFlag(decorView, vis);
228            }
229        });
230    }
231
232    @TargetApi(VERSION_CODES.LOLLIPOP)
233    private static void removeImmersiveFlagsFromDecorView(final Window window, final int vis) {
234        getDecorView(window, new OnDecorViewInstalledListener() {
235            @Override
236            public void onDecorViewInstalled(View decorView) {
237                removeVisibilityFlag(decorView, vis);
238            }
239        });
240    }
241
242    private static void getDecorView(Window window, OnDecorViewInstalledListener callback) {
243        new DecorViewFinder().getDecorView(window, callback, PEEK_DECOR_VIEW_RETRIES);
244    }
245
246    private static class DecorViewFinder {
247
248        private final Handler mHandler = new Handler();
249        private Window mWindow;
250        private int mRetries;
251        private OnDecorViewInstalledListener mCallback;
252
253        private Runnable mCheckDecorViewRunnable = new Runnable() {
254            @Override
255            public void run() {
256                // Use peekDecorView instead of getDecorView so that clients can still set window
257                // features after calling this method.
258                final View decorView = mWindow.peekDecorView();
259                if (decorView != null) {
260                    mCallback.onDecorViewInstalled(decorView);
261                } else {
262                    mRetries--;
263                    if (mRetries >= 0) {
264                        // If the decor view is not installed yet, try again in the next loop.
265                        mHandler.post(mCheckDecorViewRunnable);
266                    } else {
267                        Log.w(TAG, "Cannot get decor view of window: " + mWindow);
268                    }
269                }
270            }
271        };
272
273        public void getDecorView(Window window, OnDecorViewInstalledListener callback,
274                int retries) {
275            mWindow = window;
276            mRetries = retries;
277            mCallback = callback;
278            mCheckDecorViewRunnable.run();
279        }
280    }
281
282    private interface OnDecorViewInstalledListener {
283
284        void onDecorViewInstalled(View decorView);
285    }
286
287    /**
288     * Apply a hack to temporarily set the window to not focusable, so that the navigation bar
289     * will not show up during the transition.
290     */
291    private static void temporarilyDisableDialogFocus(final Window window) {
292        window.setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
293                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
294        // Add the SOFT_INPUT_IS_FORWARD_NAVIGATION_FLAG. This is normally done by the system when
295        // FLAG_NOT_FOCUSABLE is not set. Setting this flag allows IME to be shown automatically
296        // if the dialog has editable text fields.
297        window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION);
298        new Handler().post(new Runnable() {
299            @Override
300            public void run() {
301                window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
302            }
303        });
304    }
305
306    @TargetApi(VERSION_CODES.LOLLIPOP)
307    private static class WindowInsetsListener implements View.OnApplyWindowInsetsListener {
308        private int mBottomOffset;
309        private boolean mHasCalculatedBottomOffset = false;
310
311        @Override
312        public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) {
313            if (!mHasCalculatedBottomOffset) {
314                mBottomOffset = getBottomDistance(view);
315                mHasCalculatedBottomOffset = true;
316            }
317
318            int bottomInset = insets.getSystemWindowInsetBottom();
319
320            final int bottomMargin = Math.max(
321                    insets.getSystemWindowInsetBottom() - mBottomOffset, 0);
322
323            final ViewGroup.MarginLayoutParams lp =
324                    (ViewGroup.MarginLayoutParams) view.getLayoutParams();
325            // Check that we have enough space to apply the bottom margins before applying it.
326            // Otherwise the framework may think that the view is empty and exclude it from layout.
327            if (bottomMargin < lp.bottomMargin + view.getHeight()) {
328                lp.setMargins(lp.leftMargin, lp.topMargin, lp.rightMargin, bottomMargin);
329                view.setLayoutParams(lp);
330                bottomInset = 0;
331            }
332
333
334            return insets.replaceSystemWindowInsets(
335                    insets.getSystemWindowInsetLeft(),
336                    insets.getSystemWindowInsetTop(),
337                    insets.getSystemWindowInsetRight(),
338                    bottomInset
339            );
340        }
341    }
342
343    private static int getBottomDistance(View view) {
344        int[] coords = new int[2];
345        view.getLocationInWindow(coords);
346        return view.getRootView().getHeight() - coords[1] - view.getHeight();
347    }
348}
349