| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.google.android.setupcompat.util; |
| |
| import android.annotation.SuppressLint; |
| import android.annotation.TargetApi; |
| import android.app.Dialog; |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.os.Build.VERSION; |
| import android.os.Build.VERSION_CODES; |
| import android.os.Handler; |
| import androidx.annotation.RequiresPermission; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.Window; |
| import android.view.WindowInsets; |
| import android.view.WindowManager; |
| |
| /** |
| * A helper class to manage the system navigation bar and status bar. This will add various |
| * systemUiVisibility flags to the given Window or View to make them follow the Setup Wizard style. |
| * |
| * <p>When the useImmersiveMode intent extra is true, a screen in Setup Wizard should hide the |
| * system bars using methods from this class. For Lollipop, {@link |
| * #hideSystemBars(android.view.Window)} will completely hide the system navigation bar and change |
| * the status bar to transparent, and layout the screen contents (usually the illustration) behind |
| * it. |
| */ |
| public final class SystemBarHelper { |
| |
| private static final String TAG = "SystemBarHelper"; |
| |
| /** Needs to be equal to View.STATUS_BAR_DISABLE_BACK */ |
| private static final int STATUS_BAR_DISABLE_BACK = 0x00400000; |
| |
| @SuppressLint("InlinedApi") |
| public static final int DEFAULT_IMMERSIVE_FLAGS = |
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
| | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
| | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
| | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; |
| |
| @SuppressLint("InlinedApi") |
| public static final int DIALOG_IMMERSIVE_FLAGS = |
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; |
| |
| /** |
| * The maximum number of retries when peeking the decor view. When polling for the decor view, |
| * waiting it to be installed, set a maximum number of retries. |
| */ |
| private static final int PEEK_DECOR_VIEW_RETRIES = 3; |
| |
| /** Convenience method to add a visibility flag in addition to the existing ones. */ |
| public static void addVisibilityFlag(final View view, final int flag) { |
| final int vis = view.getSystemUiVisibility(); |
| view.setSystemUiVisibility(vis | flag); |
| } |
| |
| /** Convenience method to add a visibility flag in addition to the existing ones. */ |
| public static void addVisibilityFlag(final Window window, final int flag) { |
| WindowManager.LayoutParams attrs = window.getAttributes(); |
| attrs.systemUiVisibility |= flag; |
| window.setAttributes(attrs); |
| } |
| |
| /** |
| * Convenience method to remove a visibility flag from the view, leaving other flags that are not |
| * specified intact. |
| */ |
| public static void removeVisibilityFlag(final View view, final int flag) { |
| final int vis = view.getSystemUiVisibility(); |
| view.setSystemUiVisibility(vis & ~flag); |
| } |
| |
| /** |
| * Convenience method to remove a visibility flag from the window, leaving other flags that are |
| * not specified intact. |
| */ |
| public static void removeVisibilityFlag(final Window window, final int flag) { |
| WindowManager.LayoutParams attrs = window.getAttributes(); |
| attrs.systemUiVisibility &= ~flag; |
| window.setAttributes(attrs); |
| } |
| |
| /** |
| * Add the specified immersive flags to the decor view of the window, because {@link |
| * View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} only takes effect when it is added to a view instead of |
| * the window. |
| */ |
| public static void addImmersiveFlagsToDecorView(final Window window, final int vis) { |
| getDecorView( |
| window, |
| new OnDecorViewInstalledListener() { |
| @Override |
| public void onDecorViewInstalled(View decorView) { |
| addVisibilityFlag(decorView, vis); |
| } |
| }); |
| } |
| |
| public static void removeImmersiveFlagsFromDecorView(final Window window, final int vis) { |
| getDecorView( |
| window, |
| new OnDecorViewInstalledListener() { |
| @Override |
| public void onDecorViewInstalled(View decorView) { |
| removeVisibilityFlag(decorView, vis); |
| } |
| }); |
| } |
| |
| /** |
| * Hide the navigation bar for a dialog. |
| * |
| * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op. |
| * |
| * @deprecated If the layout is instance of TemplateLayout, please use |
| * SystemNavBarMixin.hideSystemBars. |
| */ |
| @Deprecated |
| public static void hideSystemBars(final Dialog dialog) { |
| if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { |
| final Window window = dialog.getWindow(); |
| temporarilyDisableDialogFocus(window); |
| SystemBarHelper.addVisibilityFlag(window, SystemBarHelper.DIALOG_IMMERSIVE_FLAGS); |
| SystemBarHelper.addImmersiveFlagsToDecorView(window, SystemBarHelper.DIALOG_IMMERSIVE_FLAGS); |
| |
| // Also set the navigation bar and status bar to transparent color. Note that this |
| // doesn't work if android.R.boolean.config_enableTranslucentDecor is false. |
| window.setNavigationBarColor(0); |
| window.setStatusBarColor(0); |
| } |
| } |
| |
| /** |
| * Hide the navigation bar, make the color of the status and navigation bars transparent, and |
| * specify {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} flag so that the content is laid-out |
| * behind the transparent status bar. This is commonly used with {@link |
| * android.app.Activity#getWindow()} to make the navigation and status bars follow the Setup |
| * Wizard style. |
| * |
| * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op. |
| * |
| * @deprecated If the layout instance of TemplateLayout, please use |
| * SystemNavBarMixin.hideSystemBars. |
| */ |
| @Deprecated |
| public static void hideSystemBars(final Window window) { |
| if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { |
| SystemBarHelper.addVisibilityFlag(window, SystemBarHelper.DEFAULT_IMMERSIVE_FLAGS); |
| SystemBarHelper.addImmersiveFlagsToDecorView(window, SystemBarHelper.DEFAULT_IMMERSIVE_FLAGS); |
| |
| // Also set the navigation bar and status bar to transparent color. Note that this |
| // doesn't work if android.R.boolean.config_enableTranslucentDecor is false. |
| window.setNavigationBarColor(0); |
| window.setStatusBarColor(0); |
| } |
| } |
| |
| /** |
| * Revert the actions of hideSystemBars. Note that this will remove the system UI visibility flags |
| * regardless of whether it is originally present. You should also manually reset the navigation |
| * bar and status bar colors, as this method doesn't know what value to revert it to. |
| * |
| * @deprecated If the layout is instance of TemplateLayout, please use |
| * SystemNavBarMixin.showSystemBars. |
| */ |
| @Deprecated |
| public static void showSystemBars(final Window window, final Context context) { |
| if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { |
| SystemBarHelper.removeVisibilityFlag(window, SystemBarHelper.DEFAULT_IMMERSIVE_FLAGS); |
| SystemBarHelper.removeImmersiveFlagsFromDecorView( |
| window, SystemBarHelper.DEFAULT_IMMERSIVE_FLAGS); |
| |
| if (context != null) { |
| //noinspection AndroidLintInlinedApi |
| final TypedArray typedArray = |
| context.obtainStyledAttributes( |
| new int[] {android.R.attr.statusBarColor, android.R.attr.navigationBarColor}); |
| final int statusBarColor = typedArray.getColor(0, 0); |
| final int navigationBarColor = typedArray.getColor(1, 0); |
| window.setStatusBarColor(statusBarColor); |
| window.setNavigationBarColor(navigationBarColor); |
| typedArray.recycle(); |
| } |
| } |
| } |
| |
| /** |
| * Sets whether the back button on the software navigation bar is visible. This only works if you |
| * have the STATUS_BAR permission. Otherwise framework will filter out this flag and this method |
| * call will not have any effect. |
| * |
| * <p>IMPORTANT: Do not assume that users have no way to go back when the back button is hidden. |
| * Many devices have physical back buttons, and accessibility services like TalkBack may have |
| * gestures mapped to back. Please use onBackPressed, onKeyDown, or other similar ways to make |
| * sure back button events are still handled (or ignored) properly. |
| */ |
| @RequiresPermission("android.permission.STATUS_BAR") |
| public static void setBackButtonVisible(final Window window, final boolean visible) { |
| if (visible) { |
| SystemBarHelper.removeVisibilityFlag(window, STATUS_BAR_DISABLE_BACK); |
| SystemBarHelper.removeImmersiveFlagsFromDecorView(window, STATUS_BAR_DISABLE_BACK); |
| } else { |
| SystemBarHelper.addVisibilityFlag(window, STATUS_BAR_DISABLE_BACK); |
| SystemBarHelper.addImmersiveFlagsToDecorView(window, STATUS_BAR_DISABLE_BACK); |
| } |
| } |
| |
| /** |
| * Set a view to be resized when the keyboard is shown. This will set the bottom margin of the |
| * view to be immediately above the keyboard, and assumes that the view sits immediately above the |
| * navigation bar. |
| * |
| * <p>Note that you must set {@link android.R.attr#windowSoftInputMode} to {@code adjustResize} |
| * for this class to work. Otherwise window insets are not dispatched and this method will have no |
| * effect. |
| * |
| * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op. |
| * |
| * @param view The view to be resized when the keyboard is shown. |
| */ |
| public static void setImeInsetView(final View view) { |
| if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { |
| view.setOnApplyWindowInsetsListener(new WindowInsetsListener()); |
| } |
| } |
| |
| /** |
| * Apply a hack to temporarily set the window to not focusable, so that the navigation bar will |
| * not show up during the transition. |
| */ |
| private static void temporarilyDisableDialogFocus(final Window window) { |
| window.setFlags( |
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, |
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); |
| // Add the SOFT_INPUT_IS_FORWARD_NAVIGATION_FLAG. This is normally done by the system when |
| // FLAG_NOT_FOCUSABLE is not set. Setting this flag allows IME to be shown automatically |
| // if the dialog has editable text fields. |
| window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION); |
| new Handler() |
| .post( |
| new Runnable() { |
| @Override |
| public void run() { |
| window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); |
| } |
| }); |
| } |
| |
| @TargetApi(VERSION_CODES.LOLLIPOP) |
| private static class WindowInsetsListener implements View.OnApplyWindowInsetsListener { |
| private int bottomOffset; |
| private boolean hasCalculatedBottomOffset = false; |
| |
| @Override |
| public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) { |
| if (!hasCalculatedBottomOffset) { |
| bottomOffset = getBottomDistance(view); |
| hasCalculatedBottomOffset = true; |
| } |
| |
| int bottomInset = insets.getSystemWindowInsetBottom(); |
| |
| final int bottomMargin = Math.max(insets.getSystemWindowInsetBottom() - bottomOffset, 0); |
| |
| final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); |
| // Check that we have enough space to apply the bottom margins before applying it. |
| // Otherwise the framework may think that the view is empty and exclude it from layout. |
| if (bottomMargin < lp.bottomMargin + view.getHeight()) { |
| lp.setMargins(lp.leftMargin, lp.topMargin, lp.rightMargin, bottomMargin); |
| view.setLayoutParams(lp); |
| bottomInset = 0; |
| } |
| |
| return insets.replaceSystemWindowInsets( |
| insets.getSystemWindowInsetLeft(), |
| insets.getSystemWindowInsetTop(), |
| insets.getSystemWindowInsetRight(), |
| bottomInset); |
| } |
| } |
| |
| private static int getBottomDistance(View view) { |
| int[] coords = new int[2]; |
| view.getLocationInWindow(coords); |
| return view.getRootView().getHeight() - coords[1] - view.getHeight(); |
| } |
| |
| private static void getDecorView(Window window, OnDecorViewInstalledListener callback) { |
| new DecorViewFinder().getDecorView(window, callback, PEEK_DECOR_VIEW_RETRIES); |
| } |
| |
| private static class DecorViewFinder { |
| |
| private final Handler handler = new Handler(); |
| private Window window; |
| private int retries; |
| private OnDecorViewInstalledListener callback; |
| |
| private final Runnable checkDecorViewRunnable = |
| new Runnable() { |
| @Override |
| public void run() { |
| // Use peekDecorView instead of getDecorView so that clients can still set window |
| // features after calling this method. |
| final View decorView = window.peekDecorView(); |
| if (decorView != null) { |
| callback.onDecorViewInstalled(decorView); |
| } else { |
| retries--; |
| if (retries >= 0) { |
| // If the decor view is not installed yet, try again in the next loop. |
| handler.post(checkDecorViewRunnable); |
| } else { |
| Log.w(TAG, "Cannot get decor view of window: " + window); |
| } |
| } |
| } |
| }; |
| |
| public void getDecorView(Window window, OnDecorViewInstalledListener callback, int retries) { |
| this.window = window; |
| this.retries = retries; |
| this.callback = callback; |
| checkDecorViewRunnable.run(); |
| } |
| } |
| |
| private interface OnDecorViewInstalledListener { |
| |
| void onDecorViewInstalled(View decorView); |
| } |
| |
| private SystemBarHelper() {} |
| } |