Home | History | Annotate | Download | only in util
      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