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 23 import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG; 24 import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen; 25 import static com.android.wm.shell.Flags.enableTaskbarNavbarUnification; 26 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.RemoteException; 33 import android.os.Trace; 34 import android.os.UserHandle; 35 import android.provider.Settings; 36 import android.util.Log; 37 import android.util.SparseArray; 38 import android.util.SparseBooleanArray; 39 import android.view.Display; 40 import android.view.IWindowManager; 41 import android.view.View; 42 import android.view.WindowManagerGlobal; 43 44 import androidx.annotation.NonNull; 45 import androidx.annotation.Nullable; 46 47 import com.android.internal.R; 48 import com.android.internal.annotations.VisibleForTesting; 49 import com.android.internal.statusbar.RegisterStatusBarResult; 50 import com.android.settingslib.applications.InterestingConfigChanges; 51 import com.android.systemui.Dumpable; 52 import com.android.systemui.dagger.SysUISingleton; 53 import com.android.systemui.dagger.qualifiers.Main; 54 import com.android.systemui.dump.DumpManager; 55 import com.android.systemui.model.SysUiState; 56 import com.android.systemui.recents.OverviewProxyService; 57 import com.android.systemui.settings.DisplayTracker; 58 import com.android.systemui.shared.system.QuickStepContract; 59 import com.android.systemui.shared.system.TaskStackChangeListeners; 60 import com.android.systemui.statusbar.CommandQueue; 61 import com.android.systemui.statusbar.phone.AutoHideController; 62 import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode; 63 import com.android.systemui.statusbar.phone.LightBarController; 64 import com.android.systemui.statusbar.policy.ConfigurationController; 65 import com.android.systemui.util.settings.SecureSettings; 66 import com.android.wm.shell.back.BackAnimation; 67 import com.android.wm.shell.pip.Pip; 68 69 import dalvik.annotation.optimization.NeverCompile; 70 71 import java.io.PrintWriter; 72 import java.util.Optional; 73 import java.util.concurrent.Executor; 74 75 import javax.inject.Inject; 76 77 @SysUISingleton 78 public class NavigationBarControllerImpl implements 79 ConfigurationController.ConfigurationListener, 80 NavigationModeController.ModeChangedListener, 81 Dumpable, NavigationBarController { 82 83 private static final String TAG = NavigationBarControllerImpl.class.getSimpleName(); 84 85 private final Context mContext; 86 private final Executor mExecutor; 87 private final NavigationBarComponent.Factory mNavigationBarComponentFactory; 88 private final SecureSettings mSecureSettings; 89 private final DisplayTracker mDisplayTracker; 90 private final DisplayManager mDisplayManager; 91 private final TaskbarDelegate mTaskbarDelegate; 92 private final NavBarHelper mNavBarHelper; 93 private int mNavMode; 94 /** 95 * Indicates whether the active display is a large screen, e.g. tablets, foldable devices in 96 * the unfolded state. 97 */ 98 @VisibleForTesting boolean mIsLargeScreen; 99 /** 100 * Indicates whether the device is a phone, rather than everything else (e.g. foldables, 101 * tablets) is considered not a handheld device. 102 */ 103 @VisibleForTesting boolean mIsPhone; 104 105 /** A displayId - nav bar maps. */ 106 @VisibleForTesting 107 SparseArray<NavigationBar> mNavigationBars = new SparseArray<>(); 108 109 /** Local cache for {@link IWindowManager#hasNavigationBar(int)}. */ 110 private SparseBooleanArray mHasNavBar = new SparseBooleanArray(); 111 112 // Tracks config changes that will actually recreate the nav bar 113 private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges( 114 ActivityInfo.CONFIG_FONT_SCALE 115 | ActivityInfo.CONFIG_UI_MODE); 116 117 @Inject NavigationBarControllerImpl(Context context, OverviewProxyService overviewProxyService, NavigationModeController navigationModeController, SysUiState sysUiFlagsContainer, CommandQueue commandQueue, @Main Executor mainExecutor, ConfigurationController configurationController, NavBarHelper navBarHelper, TaskbarDelegate taskbarDelegate, NavigationBarComponent.Factory navigationBarComponentFactory, DumpManager dumpManager, AutoHideController autoHideController, LightBarController lightBarController, TaskStackChangeListeners taskStackChangeListeners, Optional<Pip> pipOptional, Optional<BackAnimation> backAnimation, SecureSettings secureSettings, DisplayTracker displayTracker)118 public NavigationBarControllerImpl(Context context, 119 OverviewProxyService overviewProxyService, 120 NavigationModeController navigationModeController, 121 SysUiState sysUiFlagsContainer, 122 CommandQueue commandQueue, 123 @Main Executor mainExecutor, 124 ConfigurationController configurationController, 125 NavBarHelper navBarHelper, 126 TaskbarDelegate taskbarDelegate, 127 NavigationBarComponent.Factory navigationBarComponentFactory, 128 DumpManager dumpManager, 129 AutoHideController autoHideController, 130 LightBarController lightBarController, 131 TaskStackChangeListeners taskStackChangeListeners, 132 Optional<Pip> pipOptional, 133 Optional<BackAnimation> backAnimation, 134 SecureSettings secureSettings, 135 DisplayTracker displayTracker) { 136 mContext = context; 137 mExecutor = mainExecutor; 138 mNavigationBarComponentFactory = navigationBarComponentFactory; 139 mSecureSettings = secureSettings; 140 mDisplayTracker = displayTracker; 141 mDisplayManager = mContext.getSystemService(DisplayManager.class); 142 commandQueue.addCallback(mCommandQueueCallbacks); 143 configurationController.addCallback(this); 144 mConfigChanges.applyNewConfig(mContext.getResources()); 145 mNavMode = navigationModeController.addListener(this); 146 mNavBarHelper = navBarHelper; 147 mTaskbarDelegate = taskbarDelegate; 148 mTaskbarDelegate.setDependencies(commandQueue, overviewProxyService, 149 navBarHelper, navigationModeController, sysUiFlagsContainer, 150 dumpManager, autoHideController, lightBarController, pipOptional, 151 backAnimation.orElse(null), taskStackChangeListeners); 152 mIsLargeScreen = isLargeScreen(mContext); 153 mIsPhone = 154 mContext.getResources().getIntArray(R.array.config_foldedDeviceStates).length == 0; 155 dumpManager.registerDumpable(this); 156 } 157 158 @Override onConfigChanged(Configuration newConfig)159 public void onConfigChanged(Configuration newConfig) { 160 boolean isOldConfigLargeScreen = mIsLargeScreen; 161 mIsLargeScreen = isLargeScreen(mContext); 162 boolean willApplyConfig = mConfigChanges.applyNewConfig(mContext.getResources()); 163 boolean largeScreenChanged = mIsLargeScreen != isOldConfigLargeScreen; 164 // TODO(b/332635834): Disable this logging once b/332635834 is fixed. 165 Log.i(DEBUG_MISSING_GESTURE_TAG, "NavbarController: newConfig=" + newConfig 166 + " mTaskbarDelegate initialized=" + mTaskbarDelegate.isInitialized() 167 + " willApplyConfigToNavbars=" + willApplyConfig 168 + " navBarCount=" + mNavigationBars.size()); 169 // If we folded/unfolded while in 3 button, show navbar in folded state, hide in unfolded 170 if (largeScreenChanged && updateNavbarForTaskbar()) { 171 return; 172 } 173 174 if (willApplyConfig) { 175 for (int i = 0; i < mNavigationBars.size(); i++) { 176 recreateNavigationBar(mNavigationBars.keyAt(i)); 177 } 178 } else { 179 for (int i = 0; i < mNavigationBars.size(); i++) { 180 mNavigationBars.valueAt(i).onConfigurationChanged(newConfig); 181 } 182 } 183 } 184 185 @Override onNavigationModeChanged(int mode)186 public void onNavigationModeChanged(int mode) { 187 if (mNavMode == mode) { 188 return; 189 } 190 final int oldMode = mNavMode; 191 mNavMode = mode; 192 updateAccessibilityButtonModeIfNeeded(); 193 194 mExecutor.execute(() -> { 195 // create/destroy nav bar based on nav mode only in unfolded state 196 if (oldMode != mNavMode) { 197 updateNavbarForTaskbar(); 198 } 199 for (int i = 0; i < mNavigationBars.size(); i++) { 200 NavigationBar navBar = mNavigationBars.valueAt(i); 201 if (navBar == null) { 202 continue; 203 } 204 navBar.getView().updateStates(); 205 } 206 }); 207 } 208 updateAccessibilityButtonModeIfNeeded()209 private void updateAccessibilityButtonModeIfNeeded() { 210 final int mode = mSecureSettings.getIntForUser( 211 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, 212 ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT); 213 214 // ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU is compatible under gestural or non-gestural 215 // mode, so we don't need to update it. 216 if (mode == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU) { 217 return; 218 } 219 220 // ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR is incompatible under gestural mode. Need to 221 // force update to ACCESSIBILITY_BUTTON_MODE_GESTURE. 222 if (QuickStepContract.isGesturalMode(mNavMode) 223 && mode == ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR) { 224 mSecureSettings.putIntForUser( 225 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_GESTURE, 226 UserHandle.USER_CURRENT); 227 // ACCESSIBILITY_BUTTON_MODE_GESTURE is incompatible under non gestural mode. Need to 228 // force update to ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR. 229 } else if (!QuickStepContract.isGesturalMode(mNavMode) 230 && mode == ACCESSIBILITY_BUTTON_MODE_GESTURE) { 231 mSecureSettings.putIntForUser( 232 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, 233 ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT); 234 } 235 } 236 shouldCreateNavBarAndTaskBar(int displayId)237 private boolean shouldCreateNavBarAndTaskBar(int displayId) { 238 if (mHasNavBar.indexOfKey(displayId) > -1) { 239 return mHasNavBar.get(displayId); 240 } 241 242 final IWindowManager wms = WindowManagerGlobal.getWindowManagerService(); 243 244 try { 245 boolean hasNavigationBar = wms.hasNavigationBar(displayId); 246 mHasNavBar.put(displayId, hasNavigationBar); 247 return hasNavigationBar; 248 } catch (RemoteException e) { 249 // Cannot get wms, just return false with warning message. 250 Log.w(TAG, "Cannot get WindowManager."); 251 return false; 252 } 253 } 254 255 /** @see #initializeTaskbarIfNecessary() */ updateNavbarForTaskbar()256 private boolean updateNavbarForTaskbar() { 257 boolean taskbarShown = initializeTaskbarIfNecessary(); 258 if (!taskbarShown && mNavigationBars.get(mContext.getDisplayId()) == null) { 259 createNavigationBar(mContext.getDisplay(), null, null); 260 } 261 return taskbarShown; 262 } 263 264 /** @return {@code true} if taskbar is enabled, false otherwise */ initializeTaskbarIfNecessary()265 private boolean initializeTaskbarIfNecessary() { 266 boolean taskbarEnabled = supportsTaskbar() && shouldCreateNavBarAndTaskBar( 267 mContext.getDisplayId()); 268 269 if (taskbarEnabled) { 270 Trace.beginSection("NavigationBarController#initializeTaskbarIfNecessary"); 271 final int displayId = mContext.getDisplayId(); 272 // Hint to NavBarHelper if we are replacing an existing bar to skip extra work 273 mNavBarHelper.setTogglingNavbarTaskbar(mNavigationBars.contains(displayId)); 274 // Remove navigation bar when taskbar is showing 275 removeNavigationBar(displayId); 276 mTaskbarDelegate.init(displayId); 277 mNavBarHelper.setTogglingNavbarTaskbar(false); 278 Trace.endSection(); 279 280 } else { 281 mTaskbarDelegate.destroy(); 282 } 283 return taskbarEnabled; 284 } 285 286 @VisibleForTesting supportsTaskbar()287 boolean supportsTaskbar() { 288 // Enable for tablets, unfolded state on a foldable device or (non handheld AND flag is set) 289 return mIsLargeScreen || (!mIsPhone && enableTaskbarNavbarUnification()); 290 } 291 292 private final CommandQueue.Callbacks mCommandQueueCallbacks = new CommandQueue.Callbacks() { 293 @Override 294 public void onDisplayRemoved(int displayId) { 295 removeNavigationBar(displayId); 296 mHasNavBar.delete(displayId); 297 } 298 299 @Override 300 public void onDisplayReady(int displayId) { 301 Display display = mDisplayManager.getDisplay(displayId); 302 mIsLargeScreen = isLargeScreen(mContext); 303 createNavigationBar(display, null /* savedState */, null /* result */); 304 } 305 306 @Override 307 public void setNavigationBarLumaSamplingEnabled(int displayId, boolean enable) { 308 final NavigationBar navigationBar = getNavigationBar(displayId); 309 if (navigationBar != null) { 310 navigationBar.setNavigationBarLumaSamplingEnabled(enable); 311 } 312 } 313 314 @Override 315 public void showPinningEnterExitToast(boolean entering) { 316 int displayId = mContext.getDisplayId(); 317 final NavigationBarView navBarView = getNavigationBarView(displayId); 318 if (navBarView != null) { 319 navBarView.showPinningEnterExitToast(entering); 320 } 321 } 322 323 @Override 324 public void showPinningEscapeToast() { 325 int displayId = mContext.getDisplayId(); 326 final NavigationBarView navBarView = getNavigationBarView(displayId); 327 if (navBarView != null) { 328 navBarView.showPinningEscapeToast(); 329 } 330 } 331 }; 332 333 /** 334 * Recreates the navigation bar for the given display. 335 */ recreateNavigationBar(int displayId)336 private void recreateNavigationBar(int displayId) { 337 // TODO: Improve this flow so that we don't need to create a new nav bar but just 338 // the view 339 Bundle savedState = new Bundle(); 340 NavigationBar bar = mNavigationBars.get(displayId); 341 if (bar != null) { 342 bar.onSaveInstanceState(savedState); 343 } 344 removeNavigationBar(displayId); 345 createNavigationBar(mDisplayManager.getDisplay(displayId), savedState, null /* result */); 346 } 347 348 @Override createNavigationBars(final boolean includeDefaultDisplay, RegisterStatusBarResult result)349 public void createNavigationBars(final boolean includeDefaultDisplay, 350 RegisterStatusBarResult result) { 351 updateAccessibilityButtonModeIfNeeded(); 352 353 // Don't need to create nav bar on the default display if we initialize TaskBar. 354 final boolean shouldCreateDefaultNavbar = includeDefaultDisplay 355 && !initializeTaskbarIfNecessary(); 356 Display[] displays = mDisplayTracker.getAllDisplays(); 357 for (Display display : displays) { 358 if (shouldCreateDefaultNavbar 359 || display.getDisplayId() != mDisplayTracker.getDefaultDisplayId()) { 360 createNavigationBar(display, null /* savedState */, result); 361 } 362 } 363 } 364 365 /** 366 * Adds a navigation bar on default display or an external display if the display supports 367 * system decorations. 368 * 369 * @param display the display to add navigation bar on. 370 */ 371 @VisibleForTesting createNavigationBar(Display display, Bundle savedState, RegisterStatusBarResult result)372 void createNavigationBar(Display display, Bundle savedState, 373 RegisterStatusBarResult result) { 374 if (display == null) { 375 return; 376 } 377 378 final int displayId = display.getDisplayId(); 379 final boolean isOnDefaultDisplay = displayId == mDisplayTracker.getDefaultDisplayId(); 380 381 if (!shouldCreateNavBarAndTaskBar(displayId)) { 382 return; 383 } 384 385 // We may show TaskBar on the default display for large screen device. Don't need to create 386 // navigation bar for this case. 387 if (isOnDefaultDisplay && initializeTaskbarIfNecessary()) { 388 return; 389 } 390 391 final Context context = isOnDefaultDisplay 392 ? mContext 393 : mContext.createDisplayContext(display); 394 NavigationBarComponent component = mNavigationBarComponentFactory.create( 395 context, savedState); 396 NavigationBar navBar = component.getNavigationBar(); 397 navBar.init(); 398 mNavigationBars.put(displayId, navBar); 399 400 navBar.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { 401 @Override 402 public void onViewAttachedToWindow(View v) { 403 if (result != null) { 404 navBar.setImeWindowStatus(display.getDisplayId(), result.mImeToken, 405 result.mImeWindowVis, result.mImeBackDisposition, 406 result.mShowImeSwitcher); 407 } 408 } 409 410 @Override 411 public void onViewDetachedFromWindow(View v) { 412 v.removeOnAttachStateChangeListener(this); 413 } 414 }); 415 } 416 417 @Override removeNavigationBar(int displayId)418 public void removeNavigationBar(int displayId) { 419 NavigationBar navBar = mNavigationBars.get(displayId); 420 if (navBar != null) { 421 navBar.destroyView(); 422 mNavigationBars.remove(displayId); 423 } 424 } 425 426 @Override checkNavBarModes(int displayId)427 public void checkNavBarModes(int displayId) { 428 NavigationBar navBar = mNavigationBars.get(displayId); 429 if (navBar != null) { 430 navBar.checkNavBarModes(); 431 } 432 } 433 434 @Override finishBarAnimations(int displayId)435 public void finishBarAnimations(int displayId) { 436 NavigationBar navBar = mNavigationBars.get(displayId); 437 if (navBar != null) { 438 navBar.finishBarAnimations(); 439 } 440 } 441 442 @Override touchAutoDim(int displayId)443 public void touchAutoDim(int displayId) { 444 NavigationBar navBar = mNavigationBars.get(displayId); 445 if (navBar != null) { 446 navBar.touchAutoDim(); 447 } 448 } 449 450 @Override transitionTo(int displayId, @TransitionMode int barMode, boolean animate)451 public void transitionTo(int displayId, @TransitionMode int barMode, boolean animate) { 452 NavigationBar navBar = mNavigationBars.get(displayId); 453 if (navBar != null) { 454 navBar.transitionTo(barMode, animate); 455 } 456 } 457 458 @Override disableAnimationsDuringHide(int displayId, long delay)459 public void disableAnimationsDuringHide(int displayId, long delay) { 460 NavigationBar navBar = mNavigationBars.get(displayId); 461 if (navBar != null) { 462 navBar.disableAnimationsDuringHide(delay); 463 } 464 } 465 466 @Override getDefaultNavigationBarView()467 public @Nullable NavigationBarView getDefaultNavigationBarView() { 468 return getNavigationBarView(mDisplayTracker.getDefaultDisplayId()); 469 } 470 471 @Override getNavigationBarView(int displayId)472 public @Nullable NavigationBarView getNavigationBarView(int displayId) { 473 NavigationBar navBar = getNavigationBar(displayId); 474 return (navBar == null) ? null : navBar.getView(); 475 } 476 getNavigationBar(int displayId)477 private @Nullable NavigationBar getNavigationBar(int displayId) { 478 return mNavigationBars.get(displayId); 479 } 480 481 @Override isOverviewEnabled(int displayId)482 public boolean isOverviewEnabled(int displayId) { 483 final NavigationBarView navBarView = getNavigationBarView(displayId); 484 if (navBarView != null) { 485 return navBarView.isOverviewEnabled(); 486 } else { 487 return mTaskbarDelegate.isOverviewEnabled(); 488 } 489 } 490 491 @Override 492 @Nullable getDefaultNavigationBar()493 public NavigationBar getDefaultNavigationBar() { 494 return mNavigationBars.get(mDisplayTracker.getDefaultDisplayId()); 495 } 496 497 @NeverCompile 498 @Override dump(@onNull PrintWriter pw, @NonNull String[] args)499 public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { 500 pw.println("mIsLargeScreen=" + mIsLargeScreen); 501 pw.println("mNavMode=" + mNavMode); 502 for (int i = 0; i < mNavigationBars.size(); i++) { 503 if (i > 0) { 504 pw.println(); 505 } 506 mNavigationBars.valueAt(i).dump(pw); 507 } 508 } 509 } 510