1 /* 2 * Copyright (C) 2020 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.android.systemui.navigationbar; 18 19 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; 20 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE; 21 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR; 22 import static android.view.Display.DEFAULT_DISPLAY; 23 24 import static com.android.systemui.shared.recents.utilities.Utilities.isTablet; 25 26 import android.content.ContentResolver; 27 import android.content.Context; 28 import android.content.pm.ActivityInfo; 29 import android.content.res.Configuration; 30 import android.hardware.display.DisplayManager; 31 import android.os.Bundle; 32 import android.os.Handler; 33 import android.os.RemoteException; 34 import android.os.UserHandle; 35 import android.provider.Settings; 36 import android.util.Log; 37 import android.util.SparseArray; 38 import android.view.Display; 39 import android.view.IWindowManager; 40 import android.view.View; 41 import android.view.WindowManagerGlobal; 42 43 import androidx.annotation.NonNull; 44 import androidx.annotation.Nullable; 45 46 import com.android.internal.annotations.VisibleForTesting; 47 import com.android.internal.statusbar.RegisterStatusBarResult; 48 import com.android.settingslib.applications.InterestingConfigChanges; 49 import com.android.systemui.Dumpable; 50 import com.android.systemui.dagger.SysUISingleton; 51 import com.android.systemui.dagger.qualifiers.Main; 52 import com.android.systemui.dump.DumpManager; 53 import com.android.systemui.model.SysUiState; 54 import com.android.systemui.recents.OverviewProxyService; 55 import com.android.systemui.shared.system.QuickStepContract; 56 import com.android.systemui.statusbar.CommandQueue; 57 import com.android.systemui.statusbar.CommandQueue.Callbacks; 58 import com.android.systemui.statusbar.phone.AutoHideController; 59 import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode; 60 import com.android.systemui.statusbar.phone.LightBarController; 61 import com.android.systemui.statusbar.policy.ConfigurationController; 62 import com.android.wm.shell.pip.Pip; 63 64 import java.io.FileDescriptor; 65 import java.io.PrintWriter; 66 import java.util.Optional; 67 68 import javax.inject.Inject; 69 70 71 /** A controller to handle navigation bars. */ 72 @SysUISingleton 73 public class NavigationBarController implements 74 Callbacks, 75 ConfigurationController.ConfigurationListener, 76 NavigationModeController.ModeChangedListener, 77 Dumpable { 78 79 private static final String TAG = NavigationBarController.class.getSimpleName(); 80 81 private final Context mContext; 82 private final Handler mHandler; 83 private final NavigationBar.Factory mNavigationBarFactory; 84 private final DisplayManager mDisplayManager; 85 private final TaskbarDelegate mTaskbarDelegate; 86 private int mNavMode; 87 @VisibleForTesting boolean mIsTablet; 88 89 /** A displayId - nav bar maps. */ 90 @VisibleForTesting 91 SparseArray<NavigationBar> mNavigationBars = new SparseArray<>(); 92 93 // Tracks config changes that will actually recreate the nav bar 94 private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges( 95 ActivityInfo.CONFIG_FONT_SCALE | ActivityInfo.CONFIG_SCREEN_LAYOUT 96 | ActivityInfo.CONFIG_UI_MODE); 97 98 @Inject NavigationBarController(Context context, OverviewProxyService overviewProxyService, NavigationModeController navigationModeController, SysUiState sysUiFlagsContainer, CommandQueue commandQueue, @Main Handler mainHandler, ConfigurationController configurationController, NavBarHelper navBarHelper, TaskbarDelegate taskbarDelegate, NavigationBar.Factory navigationBarFactory, DumpManager dumpManager, AutoHideController autoHideController, LightBarController lightBarController, Optional<Pip> pipOptional)99 public NavigationBarController(Context context, 100 OverviewProxyService overviewProxyService, 101 NavigationModeController navigationModeController, 102 SysUiState sysUiFlagsContainer, 103 CommandQueue commandQueue, 104 @Main Handler mainHandler, 105 ConfigurationController configurationController, 106 NavBarHelper navBarHelper, 107 TaskbarDelegate taskbarDelegate, 108 NavigationBar.Factory navigationBarFactory, 109 DumpManager dumpManager, 110 AutoHideController autoHideController, 111 LightBarController lightBarController, 112 Optional<Pip> pipOptional) { 113 mContext = context; 114 mHandler = mainHandler; 115 mNavigationBarFactory = navigationBarFactory; 116 mDisplayManager = mContext.getSystemService(DisplayManager.class); 117 commandQueue.addCallback(this); 118 configurationController.addCallback(this); 119 mConfigChanges.applyNewConfig(mContext.getResources()); 120 mNavMode = navigationModeController.addListener(this); 121 mTaskbarDelegate = taskbarDelegate; 122 mTaskbarDelegate.setDependencies(commandQueue, overviewProxyService, 123 navBarHelper, navigationModeController, sysUiFlagsContainer, 124 dumpManager, autoHideController, lightBarController, pipOptional); 125 mIsTablet = isTablet(mContext); 126 dumpManager.registerDumpable(this); 127 } 128 129 @Override onConfigChanged(Configuration newConfig)130 public void onConfigChanged(Configuration newConfig) { 131 boolean isOldConfigTablet = mIsTablet; 132 mIsTablet = isTablet(mContext); 133 boolean largeScreenChanged = mIsTablet != isOldConfigTablet; 134 // If we folded/unfolded while in 3 button, show navbar in folded state, hide in unfolded 135 if (largeScreenChanged && updateNavbarForTaskbar()) { 136 return; 137 } 138 139 if (mConfigChanges.applyNewConfig(mContext.getResources())) { 140 for (int i = 0; i < mNavigationBars.size(); i++) { 141 recreateNavigationBar(mNavigationBars.keyAt(i)); 142 } 143 } else { 144 for (int i = 0; i < mNavigationBars.size(); i++) { 145 mNavigationBars.valueAt(i).onConfigurationChanged(newConfig); 146 } 147 } 148 } 149 150 @Override onNavigationModeChanged(int mode)151 public void onNavigationModeChanged(int mode) { 152 if (mNavMode == mode) { 153 return; 154 } 155 final int oldMode = mNavMode; 156 mNavMode = mode; 157 updateAccessibilityButtonModeIfNeeded(); 158 159 mHandler.post(() -> { 160 // create/destroy nav bar based on nav mode only in unfolded state 161 if (oldMode != mNavMode) { 162 updateNavbarForTaskbar(); 163 } 164 for (int i = 0; i < mNavigationBars.size(); i++) { 165 NavigationBar navBar = mNavigationBars.valueAt(i); 166 if (navBar == null) { 167 continue; 168 } 169 navBar.getView().updateStates(); 170 } 171 }); 172 } 173 updateAccessibilityButtonModeIfNeeded()174 private void updateAccessibilityButtonModeIfNeeded() { 175 ContentResolver contentResolver = mContext.getContentResolver(); 176 final int mode = Settings.Secure.getIntForUser(contentResolver, 177 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, 178 ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT); 179 180 // ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU is compatible under gestural or non-gestural 181 // mode, so we don't need to update it. 182 if (mode == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU) { 183 return; 184 } 185 186 // ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR is incompatible under gestural mode. Need to 187 // force update to ACCESSIBILITY_BUTTON_MODE_GESTURE. 188 if (QuickStepContract.isGesturalMode(mNavMode) 189 && mode == ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR) { 190 Settings.Secure.putIntForUser(contentResolver, 191 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_GESTURE, 192 UserHandle.USER_CURRENT); 193 // ACCESSIBILITY_BUTTON_MODE_GESTURE is incompatible under non gestural mode. Need to 194 // force update to ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR. 195 } else if (!QuickStepContract.isGesturalMode(mNavMode) 196 && mode == ACCESSIBILITY_BUTTON_MODE_GESTURE) { 197 Settings.Secure.putIntForUser(contentResolver, 198 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, 199 ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT); 200 } 201 } 202 203 /** @see #initializeTaskbarIfNecessary() */ updateNavbarForTaskbar()204 private boolean updateNavbarForTaskbar() { 205 boolean taskbarShown = initializeTaskbarIfNecessary(); 206 if (!taskbarShown && mNavigationBars.get(mContext.getDisplayId()) == null) { 207 createNavigationBar(mContext.getDisplay(), null, null); 208 } 209 return taskbarShown; 210 } 211 212 /** @return {@code true} if taskbar is enabled, false otherwise */ initializeTaskbarIfNecessary()213 private boolean initializeTaskbarIfNecessary() { 214 if (mIsTablet) { 215 // Remove navigation bar when taskbar is showing 216 removeNavigationBar(mContext.getDisplayId()); 217 mTaskbarDelegate.init(mContext.getDisplayId()); 218 } else { 219 mTaskbarDelegate.destroy(); 220 } 221 return mIsTablet; 222 } 223 224 @Override onDisplayRemoved(int displayId)225 public void onDisplayRemoved(int displayId) { 226 removeNavigationBar(displayId); 227 } 228 229 @Override onDisplayReady(int displayId)230 public void onDisplayReady(int displayId) { 231 Display display = mDisplayManager.getDisplay(displayId); 232 mIsTablet = isTablet(mContext); 233 createNavigationBar(display, null /* savedState */, null /* result */); 234 } 235 236 @Override setNavigationBarLumaSamplingEnabled(int displayId, boolean enable)237 public void setNavigationBarLumaSamplingEnabled(int displayId, boolean enable) { 238 final NavigationBarView navigationBarView = getNavigationBarView(displayId); 239 if (navigationBarView != null) { 240 navigationBarView.setNavigationBarLumaSamplingEnabled(enable); 241 } 242 } 243 244 /** 245 * Recreates the navigation bar for the given display. 246 */ recreateNavigationBar(int displayId)247 private void recreateNavigationBar(int displayId) { 248 // TODO: Improve this flow so that we don't need to create a new nav bar but just 249 // the view 250 Bundle savedState = new Bundle(); 251 NavigationBar bar = mNavigationBars.get(displayId); 252 if (bar != null) { 253 bar.onSaveInstanceState(savedState); 254 } 255 removeNavigationBar(displayId); 256 createNavigationBar(mDisplayManager.getDisplay(displayId), savedState, null /* result */); 257 } 258 259 // TODO(b/117478341): I use {@code includeDefaultDisplay} to make this method compatible to 260 // CarStatusBar because they have their own nav bar. Think about a better way for it. 261 /** 262 * Creates navigation bars when car/status bar initializes. 263 * 264 * @param includeDefaultDisplay {@code true} to create navigation bar on default display. 265 */ createNavigationBars(final boolean includeDefaultDisplay, RegisterStatusBarResult result)266 public void createNavigationBars(final boolean includeDefaultDisplay, 267 RegisterStatusBarResult result) { 268 updateAccessibilityButtonModeIfNeeded(); 269 270 // Don't need to create nav bar on the default display if we initialize TaskBar. 271 final boolean shouldCreateDefaultNavbar = includeDefaultDisplay 272 && !initializeTaskbarIfNecessary(); 273 Display[] displays = mDisplayManager.getDisplays(); 274 for (Display display : displays) { 275 if (shouldCreateDefaultNavbar || display.getDisplayId() != DEFAULT_DISPLAY) { 276 createNavigationBar(display, null /* savedState */, result); 277 } 278 } 279 } 280 281 /** 282 * Adds a navigation bar on default display or an external display if the display supports 283 * system decorations. 284 * 285 * @param display the display to add navigation bar on. 286 */ 287 @VisibleForTesting createNavigationBar(Display display, Bundle savedState, RegisterStatusBarResult result)288 void createNavigationBar(Display display, Bundle savedState, RegisterStatusBarResult result) { 289 if (display == null) { 290 return; 291 } 292 293 final int displayId = display.getDisplayId(); 294 final boolean isOnDefaultDisplay = displayId == DEFAULT_DISPLAY; 295 296 // We may show TaskBar on the default display for large screen device. Don't need to create 297 // navigation bar for this case. 298 if (mIsTablet && isOnDefaultDisplay) { 299 return; 300 } 301 302 final IWindowManager wms = WindowManagerGlobal.getWindowManagerService(); 303 304 try { 305 if (!wms.hasNavigationBar(displayId)) { 306 return; 307 } 308 } catch (RemoteException e) { 309 // Cannot get wms, just return with warning message. 310 Log.w(TAG, "Cannot get WindowManager."); 311 return; 312 } 313 final Context context = isOnDefaultDisplay 314 ? mContext 315 : mContext.createDisplayContext(display); 316 NavigationBar navBar = mNavigationBarFactory.create(context); 317 318 mNavigationBars.put(displayId, navBar); 319 320 View navigationBarView = navBar.createView(savedState); 321 navigationBarView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { 322 @Override 323 public void onViewAttachedToWindow(View v) { 324 if (result != null) { 325 navBar.setImeWindowStatus(display.getDisplayId(), result.mImeToken, 326 result.mImeWindowVis, result.mImeBackDisposition, 327 result.mShowImeSwitcher); 328 } 329 } 330 331 @Override 332 public void onViewDetachedFromWindow(View v) { 333 v.removeOnAttachStateChangeListener(this); 334 } 335 }); 336 } 337 removeNavigationBar(int displayId)338 void removeNavigationBar(int displayId) { 339 NavigationBar navBar = mNavigationBars.get(displayId); 340 if (navBar != null) { 341 navBar.destroyView(); 342 mNavigationBars.remove(displayId); 343 } 344 } 345 346 /** @see NavigationBar#checkNavBarModes() */ checkNavBarModes(int displayId)347 public void checkNavBarModes(int displayId) { 348 NavigationBar navBar = mNavigationBars.get(displayId); 349 if (navBar != null) { 350 navBar.checkNavBarModes(); 351 } 352 } 353 354 /** @see NavigationBar#finishBarAnimations() */ finishBarAnimations(int displayId)355 public void finishBarAnimations(int displayId) { 356 NavigationBar navBar = mNavigationBars.get(displayId); 357 if (navBar != null) { 358 navBar.finishBarAnimations(); 359 } 360 } 361 362 /** @see NavigationBar#touchAutoDim() */ touchAutoDim(int displayId)363 public void touchAutoDim(int displayId) { 364 NavigationBar navBar = mNavigationBars.get(displayId); 365 if (navBar != null) { 366 navBar.touchAutoDim(); 367 } 368 } 369 370 /** @see NavigationBar#transitionTo(int, boolean) */ transitionTo(int displayId, @TransitionMode int barMode, boolean animate)371 public void transitionTo(int displayId, @TransitionMode int barMode, boolean animate) { 372 NavigationBar navBar = mNavigationBars.get(displayId); 373 if (navBar != null) { 374 navBar.transitionTo(barMode, animate); 375 } 376 } 377 378 /** @see NavigationBar#disableAnimationsDuringHide(long) */ disableAnimationsDuringHide(int displayId, long delay)379 public void disableAnimationsDuringHide(int displayId, long delay) { 380 NavigationBar navBar = mNavigationBars.get(displayId); 381 if (navBar != null) { 382 navBar.disableAnimationsDuringHide(delay); 383 } 384 } 385 386 /** @return {@link NavigationBarView} on the default display. */ getDefaultNavigationBarView()387 public @Nullable NavigationBarView getDefaultNavigationBarView() { 388 return getNavigationBarView(DEFAULT_DISPLAY); 389 } 390 391 /** 392 * @param displayId the ID of display which Navigation bar is on 393 * @return {@link NavigationBarView} on the display with {@code displayId}. 394 * {@code null} if no navigation bar on that display. 395 */ getNavigationBarView(int displayId)396 public @Nullable NavigationBarView getNavigationBarView(int displayId) { 397 NavigationBar navBar = mNavigationBars.get(displayId); 398 return (navBar == null) ? null : navBar.getView(); 399 } 400 showPinningEnterExitToast(int displayId, boolean entering)401 public void showPinningEnterExitToast(int displayId, boolean entering) { 402 final NavigationBarView navBarView = getNavigationBarView(displayId); 403 if (navBarView != null) { 404 navBarView.showPinningEnterExitToast(entering); 405 } else if (displayId == DEFAULT_DISPLAY && mTaskbarDelegate.isInitialized()) { 406 mTaskbarDelegate.showPinningEnterExitToast(entering); 407 } 408 } 409 showPinningEscapeToast(int displayId)410 public void showPinningEscapeToast(int displayId) { 411 final NavigationBarView navBarView = getNavigationBarView(displayId); 412 if (navBarView != null) { 413 navBarView.showPinningEscapeToast(); 414 } else if (displayId == DEFAULT_DISPLAY && mTaskbarDelegate.isInitialized()) { 415 mTaskbarDelegate.showPinningEscapeToast(); 416 } 417 } 418 419 /** @return {@link NavigationBar} on the default display. */ 420 @Nullable getDefaultNavigationBar()421 public NavigationBar getDefaultNavigationBar() { 422 return mNavigationBars.get(DEFAULT_DISPLAY); 423 } 424 425 @Override dump(@onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)426 public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { 427 for (int i = 0; i < mNavigationBars.size(); i++) { 428 if (i > 0) { 429 pw.println(); 430 } 431 mNavigationBars.valueAt(i).dump(pw); 432 } 433 } 434 } 435