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