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