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