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.setupwizardlib.util;
18 
19 import android.annotation.SuppressLint;
20 import android.annotation.TargetApi;
21 import android.app.Dialog;
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.os.Build.VERSION;
25 import android.os.Build.VERSION_CODES;
26 import android.os.Handler;
27 import android.util.Log;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.view.Window;
31 import android.view.WindowInsets;
32 import android.view.WindowManager;
33 
34 import 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  */
45 public 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      */
hideSystemBars(final Dialog dialog)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      */
hideSystemBars(final Window window)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      */
showSystemBars(final Dialog dialog, final Context context)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      */
showSystemBars(final Window window, final Context context)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      */
addVisibilityFlag(final View view, final int flag)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      */
addVisibilityFlag(final Window window, final int flag)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      */
removeVisibilityFlag(final View view, final int flag)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      */
removeVisibilityFlag(final Window window, final int flag)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 
setBackButtonVisible(final Window window, final boolean visible)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      */
setImeInsetView(final View view)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)
addImmersiveFlagsToDecorView(final Window window, final int vis)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)
removeImmersiveFlagsFromDecorView(final Window window, final int vis)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 
getDecorView(Window window, OnDecorViewInstalledListener callback)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 
getDecorView(Window window, OnDecorViewInstalledListener callback, int retries)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 
onDecorViewInstalled(View decorView)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      */
temporarilyDisableDialogFocus(final Window window)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
onApplyWindowInsets(View view, WindowInsets insets)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 
getBottomDistance(View view)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