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.wm.shell.onehanded; 18 19 import static android.os.UserHandle.USER_CURRENT; 20 import static android.os.UserHandle.myUserId; 21 import static android.view.Display.DEFAULT_DISPLAY; 22 23 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; 24 import static com.android.wm.shell.onehanded.OneHandedState.STATE_ACTIVE; 25 import static com.android.wm.shell.onehanded.OneHandedState.STATE_ENTERING; 26 import static com.android.wm.shell.onehanded.OneHandedState.STATE_EXITING; 27 import static com.android.wm.shell.onehanded.OneHandedState.STATE_NONE; 28 29 import android.annotation.BinderThread; 30 import android.content.ComponentName; 31 import android.content.Context; 32 import android.content.om.IOverlayManager; 33 import android.content.om.OverlayInfo; 34 import android.content.res.Configuration; 35 import android.database.ContentObserver; 36 import android.graphics.Rect; 37 import android.os.Handler; 38 import android.os.RemoteException; 39 import android.os.ServiceManager; 40 import android.os.SystemProperties; 41 import android.provider.Settings; 42 import android.util.Slog; 43 import android.view.Surface; 44 import android.view.WindowManager; 45 import android.view.accessibility.AccessibilityManager; 46 import android.window.WindowContainerTransaction; 47 48 import androidx.annotation.NonNull; 49 import androidx.annotation.VisibleForTesting; 50 51 import com.android.internal.logging.UiEventLogger; 52 import com.android.wm.shell.R; 53 import com.android.wm.shell.common.DisplayChangeController; 54 import com.android.wm.shell.common.DisplayController; 55 import com.android.wm.shell.common.DisplayLayout; 56 import com.android.wm.shell.common.RemoteCallable; 57 import com.android.wm.shell.common.ShellExecutor; 58 import com.android.wm.shell.common.TaskStackListenerCallback; 59 import com.android.wm.shell.common.TaskStackListenerImpl; 60 import com.android.wm.shell.common.annotations.ExternalThread; 61 62 import java.io.PrintWriter; 63 64 /** 65 * Manages and manipulates the one handed states, transitions, and gesture for phones. 66 */ 67 public class OneHandedController implements RemoteCallable<OneHandedController>, 68 DisplayChangeController.OnDisplayChangingListener { 69 private static final String TAG = "OneHandedController"; 70 71 private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE = 72 "persist.debug.one_handed_offset_percentage"; 73 private static final String ONE_HANDED_MODE_GESTURAL_OVERLAY = 74 "com.android.internal.systemui.onehanded.gestural"; 75 private static final int OVERLAY_ENABLED_DELAY_MS = 250; 76 private static final int DISPLAY_AREA_READY_RETRY_MS = 10; 77 78 public static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode"; 79 80 private volatile boolean mIsOneHandedEnabled; 81 private volatile boolean mIsSwipeToNotificationEnabled; 82 private boolean mIsShortcutEnabled; 83 private boolean mTaskChangeToExit; 84 private boolean mLockedDisabled; 85 private boolean mKeyguardShowing; 86 private int mUserId; 87 private float mOffSetFraction; 88 89 private Context mContext; 90 91 private final AccessibilityManager mAccessibilityManager; 92 private final DisplayController mDisplayController; 93 private final OneHandedSettingsUtil mOneHandedSettingsUtil; 94 private final OneHandedAccessibilityUtil mOneHandedAccessibilityUtil; 95 private final OneHandedTimeoutHandler mTimeoutHandler; 96 private final OneHandedTouchHandler mTouchHandler; 97 private final OneHandedState mState; 98 private final OneHandedTutorialHandler mTutorialHandler; 99 private final TaskStackListenerImpl mTaskStackListener; 100 private final IOverlayManager mOverlayManager; 101 private final ShellExecutor mMainExecutor; 102 private final Handler mMainHandler; 103 private final OneHandedImpl mImpl = new OneHandedImpl(); 104 105 private OneHandedEventCallback mEventCallback; 106 private OneHandedDisplayAreaOrganizer mDisplayAreaOrganizer; 107 private OneHandedBackgroundPanelOrganizer mBackgroundPanelOrganizer; 108 private OneHandedUiEventLogger mOneHandedUiEventLogger; 109 110 private final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener = 111 new DisplayController.OnDisplaysChangedListener() { 112 @Override 113 public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { 114 if (displayId != DEFAULT_DISPLAY || !isInitialized()) { 115 return; 116 } 117 updateDisplayLayout(displayId); 118 } 119 120 @Override 121 public void onDisplayAdded(int displayId) { 122 if (displayId != DEFAULT_DISPLAY || !isInitialized()) { 123 return; 124 } 125 updateDisplayLayout(displayId); 126 } 127 }; 128 129 private final ContentObserver mActivatedObserver; 130 private final ContentObserver mEnabledObserver; 131 private final ContentObserver mSwipeToNotificationEnabledObserver; 132 private final ContentObserver mShortcutEnabledObserver; 133 134 private AccessibilityManager.AccessibilityStateChangeListener 135 mAccessibilityStateChangeListener = 136 new AccessibilityManager.AccessibilityStateChangeListener() { 137 @Override 138 public void onAccessibilityStateChanged(boolean enabled) { 139 if (!isInitialized()) { 140 return; 141 } 142 if (enabled) { 143 final int mOneHandedTimeout = mOneHandedSettingsUtil 144 .getSettingsOneHandedModeTimeout( 145 mContext.getContentResolver(), mUserId); 146 final int timeout = mAccessibilityManager 147 .getRecommendedTimeoutMillis(mOneHandedTimeout * 1000 148 /* align with A11y timeout millis */, 149 AccessibilityManager.FLAG_CONTENT_CONTROLS); 150 mTimeoutHandler.setTimeout(timeout / 1000); 151 } else { 152 mTimeoutHandler.setTimeout(mOneHandedSettingsUtil 153 .getSettingsOneHandedModeTimeout( 154 mContext.getContentResolver(), mUserId)); 155 } 156 } 157 }; 158 159 private final OneHandedTransitionCallback mTransitionCallBack = 160 new OneHandedTransitionCallback() { 161 @Override 162 public void onStartFinished(Rect bounds) { 163 mState.setState(STATE_ACTIVE); 164 notifyShortcutStateChanged(STATE_ACTIVE); 165 } 166 167 @Override 168 public void onStopFinished(Rect bounds) { 169 mState.setState(STATE_NONE); 170 notifyShortcutStateChanged(STATE_NONE); 171 mBackgroundPanelOrganizer.onStopFinished(); 172 } 173 }; 174 175 private final TaskStackListenerCallback mTaskStackListenerCallback = 176 new TaskStackListenerCallback() { 177 @Override 178 public void onTaskCreated(int taskId, ComponentName componentName) { 179 stopOneHanded(OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT); 180 } 181 182 @Override 183 public void onTaskMovedToFront(int taskId) { 184 stopOneHanded(OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT); 185 } 186 }; 187 isInitialized()188 private boolean isInitialized() { 189 if (mDisplayAreaOrganizer == null || mDisplayController == null 190 || mOneHandedSettingsUtil == null) { 191 Slog.w(TAG, "Components may not initialized yet!"); 192 return false; 193 } 194 return true; 195 } 196 197 /** 198 * Creates {@link OneHandedController}, returns {@code null} if the feature is not supported. 199 */ create( Context context, WindowManager windowManager, DisplayController displayController, DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener, UiEventLogger uiEventLogger, ShellExecutor mainExecutor, Handler mainHandler)200 public static OneHandedController create( 201 Context context, WindowManager windowManager, DisplayController displayController, 202 DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener, 203 UiEventLogger uiEventLogger, ShellExecutor mainExecutor, Handler mainHandler) { 204 OneHandedSettingsUtil settingsUtil = new OneHandedSettingsUtil(); 205 OneHandedAccessibilityUtil accessibilityUtil = new OneHandedAccessibilityUtil(context); 206 OneHandedTimeoutHandler timeoutHandler = new OneHandedTimeoutHandler(mainExecutor); 207 OneHandedState transitionState = new OneHandedState(); 208 OneHandedTutorialHandler tutorialHandler = new OneHandedTutorialHandler(context, 209 settingsUtil, windowManager); 210 OneHandedAnimationController animationController = 211 new OneHandedAnimationController(context); 212 OneHandedTouchHandler touchHandler = new OneHandedTouchHandler(timeoutHandler, 213 mainExecutor); 214 OneHandedBackgroundPanelOrganizer oneHandedBackgroundPanelOrganizer = 215 new OneHandedBackgroundPanelOrganizer(context, displayLayout, settingsUtil, 216 mainExecutor); 217 OneHandedDisplayAreaOrganizer organizer = new OneHandedDisplayAreaOrganizer( 218 context, displayLayout, settingsUtil, animationController, tutorialHandler, 219 oneHandedBackgroundPanelOrganizer, mainExecutor); 220 OneHandedUiEventLogger oneHandedUiEventsLogger = new OneHandedUiEventLogger(uiEventLogger); 221 IOverlayManager overlayManager = IOverlayManager.Stub.asInterface( 222 ServiceManager.getService(Context.OVERLAY_SERVICE)); 223 return new OneHandedController(context, displayController, 224 oneHandedBackgroundPanelOrganizer, organizer, touchHandler, tutorialHandler, 225 settingsUtil, accessibilityUtil, timeoutHandler, transitionState, 226 oneHandedUiEventsLogger, overlayManager, taskStackListener, mainExecutor, 227 mainHandler); 228 } 229 230 @VisibleForTesting OneHandedController(Context context, DisplayController displayController, OneHandedBackgroundPanelOrganizer backgroundPanelOrganizer, OneHandedDisplayAreaOrganizer displayAreaOrganizer, OneHandedTouchHandler touchHandler, OneHandedTutorialHandler tutorialHandler, OneHandedSettingsUtil settingsUtil, OneHandedAccessibilityUtil oneHandedAccessibilityUtil, OneHandedTimeoutHandler timeoutHandler, OneHandedState state, OneHandedUiEventLogger uiEventsLogger, IOverlayManager overlayManager, TaskStackListenerImpl taskStackListener, ShellExecutor mainExecutor, Handler mainHandler)231 OneHandedController(Context context, 232 DisplayController displayController, 233 OneHandedBackgroundPanelOrganizer backgroundPanelOrganizer, 234 OneHandedDisplayAreaOrganizer displayAreaOrganizer, 235 OneHandedTouchHandler touchHandler, 236 OneHandedTutorialHandler tutorialHandler, 237 OneHandedSettingsUtil settingsUtil, 238 OneHandedAccessibilityUtil oneHandedAccessibilityUtil, 239 OneHandedTimeoutHandler timeoutHandler, 240 OneHandedState state, 241 OneHandedUiEventLogger uiEventsLogger, 242 IOverlayManager overlayManager, 243 TaskStackListenerImpl taskStackListener, 244 ShellExecutor mainExecutor, 245 Handler mainHandler) { 246 mContext = context; 247 mOneHandedSettingsUtil = settingsUtil; 248 mOneHandedAccessibilityUtil = oneHandedAccessibilityUtil; 249 mBackgroundPanelOrganizer = backgroundPanelOrganizer; 250 mDisplayAreaOrganizer = displayAreaOrganizer; 251 mDisplayController = displayController; 252 mTouchHandler = touchHandler; 253 mState = state; 254 mTutorialHandler = tutorialHandler; 255 mOverlayManager = overlayManager; 256 mMainExecutor = mainExecutor; 257 mMainHandler = mainHandler; 258 mOneHandedUiEventLogger = uiEventsLogger; 259 mTaskStackListener = taskStackListener; 260 261 mDisplayController.addDisplayWindowListener(mDisplaysChangedListener); 262 final float offsetPercentageConfig = context.getResources().getFraction( 263 R.fraction.config_one_handed_offset, 1, 1); 264 final int sysPropPercentageConfig = SystemProperties.getInt( 265 ONE_HANDED_MODE_OFFSET_PERCENTAGE, Math.round(offsetPercentageConfig * 100.0f)); 266 mUserId = myUserId(); 267 mOffSetFraction = sysPropPercentageConfig / 100.0f; 268 mIsOneHandedEnabled = mOneHandedSettingsUtil.getSettingsOneHandedModeEnabled( 269 context.getContentResolver(), mUserId); 270 mIsSwipeToNotificationEnabled = 271 mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( 272 context.getContentResolver(), mUserId); 273 mTimeoutHandler = timeoutHandler; 274 275 mActivatedObserver = getObserver(this::onActivatedActionChanged); 276 mEnabledObserver = getObserver(this::onEnabledSettingChanged); 277 mSwipeToNotificationEnabledObserver = 278 getObserver(this::onSwipeToNotificationEnabledChanged); 279 mShortcutEnabledObserver = getObserver(this::onShortcutEnabledChanged); 280 281 mDisplayController.addDisplayChangingController(this); 282 setupCallback(); 283 registerSettingObservers(mUserId); 284 setupTimeoutListener(); 285 setupGesturalOverlay(); 286 updateSettings(); 287 288 mAccessibilityManager = AccessibilityManager.getInstance(context); 289 mAccessibilityManager.addAccessibilityStateChangeListener( 290 mAccessibilityStateChangeListener); 291 292 mState.addSListeners(mBackgroundPanelOrganizer); 293 mState.addSListeners(mTutorialHandler); 294 } 295 asOneHanded()296 public OneHanded asOneHanded() { 297 return mImpl; 298 } 299 300 @Override getContext()301 public Context getContext() { 302 return mContext; 303 } 304 305 @Override getRemoteCallExecutor()306 public ShellExecutor getRemoteCallExecutor() { 307 return mMainExecutor; 308 } 309 310 /** 311 * Set one handed enabled or disabled when user update settings 312 */ setOneHandedEnabled(boolean enabled)313 void setOneHandedEnabled(boolean enabled) { 314 mIsOneHandedEnabled = enabled; 315 updateOneHandedEnabled(); 316 } 317 318 /** 319 * Set one handed enabled or disabled by when user update settings 320 */ setTaskChangeToExit(boolean enabled)321 void setTaskChangeToExit(boolean enabled) { 322 if (enabled) { 323 mTaskStackListener.addListener(mTaskStackListenerCallback); 324 } else { 325 mTaskStackListener.removeListener(mTaskStackListenerCallback); 326 } 327 mTaskChangeToExit = enabled; 328 } 329 330 /** 331 * Sets whether to enable swipe bottom to notification gesture when user update settings. 332 */ setSwipeToNotificationEnabled(boolean enabled)333 void setSwipeToNotificationEnabled(boolean enabled) { 334 mIsSwipeToNotificationEnabled = enabled; 335 } 336 337 @VisibleForTesting notifyShortcutStateChanged(@neHandedState.State int state)338 void notifyShortcutStateChanged(@OneHandedState.State int state) { 339 if (!isShortcutEnabled()) { 340 return; 341 } 342 mOneHandedSettingsUtil.setOneHandedModeActivated( 343 mContext.getContentResolver(), state == STATE_ACTIVE ? 1 : 0, mUserId); 344 } 345 346 @VisibleForTesting startOneHanded()347 void startOneHanded() { 348 if (isLockedDisabled() || mKeyguardShowing) { 349 Slog.d(TAG, "Temporary lock disabled"); 350 return; 351 } 352 353 if (!mDisplayAreaOrganizer.isReady()) { 354 // Must wait until DisplayAreaOrganizer is ready for transitioning. 355 mMainExecutor.executeDelayed(this::startOneHanded, DISPLAY_AREA_READY_RETRY_MS); 356 return; 357 } 358 359 if (mState.isTransitioning() || mState.isInOneHanded()) { 360 return; 361 } 362 363 final int currentRotation = mDisplayAreaOrganizer.getDisplayLayout().rotation(); 364 if (currentRotation != Surface.ROTATION_0 && currentRotation != Surface.ROTATION_180) { 365 Slog.w(TAG, "One handed mode only support portrait mode"); 366 return; 367 } 368 369 mState.setState(STATE_ENTERING); 370 final int yOffSet = Math.round( 371 mDisplayAreaOrganizer.getDisplayLayout().height() * mOffSetFraction); 372 mOneHandedAccessibilityUtil.announcementForScreenReader( 373 mOneHandedAccessibilityUtil.getOneHandedStartDescription()); 374 mBackgroundPanelOrganizer.onStart(); 375 mDisplayAreaOrganizer.scheduleOffset(0, yOffSet); 376 mTimeoutHandler.resetTimer(); 377 mOneHandedUiEventLogger.writeEvent( 378 OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_GESTURE_IN); 379 } 380 381 @VisibleForTesting stopOneHanded()382 void stopOneHanded() { 383 stopOneHanded(OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_GESTURE_OUT); 384 } 385 stopOneHanded(int uiEvent)386 private void stopOneHanded(int uiEvent) { 387 if (mState.isTransitioning() || mState.getState() == STATE_NONE) { 388 return; 389 } 390 mState.setState(STATE_EXITING); 391 mOneHandedAccessibilityUtil.announcementForScreenReader( 392 mOneHandedAccessibilityUtil.getOneHandedStopDescription()); 393 mDisplayAreaOrganizer.scheduleOffset(0, 0); 394 mTimeoutHandler.removeTimer(); 395 mOneHandedUiEventLogger.writeEvent(uiEvent); 396 } 397 registerEventCallback(OneHandedEventCallback callback)398 void registerEventCallback(OneHandedEventCallback callback) { 399 mEventCallback = callback; 400 } 401 402 @VisibleForTesting registerTransitionCallback(OneHandedTransitionCallback callback)403 void registerTransitionCallback(OneHandedTransitionCallback callback) { 404 mDisplayAreaOrganizer.registerTransitionCallback(callback); 405 } 406 setupCallback()407 private void setupCallback() { 408 mTouchHandler.registerTouchEventListener(() -> 409 stopOneHanded(OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT)); 410 mDisplayAreaOrganizer.registerTransitionCallback(mTouchHandler); 411 mDisplayAreaOrganizer.registerTransitionCallback(mTutorialHandler); 412 mDisplayAreaOrganizer.registerTransitionCallback(mTransitionCallBack); 413 if (mTaskChangeToExit) { 414 mTaskStackListener.addListener(mTaskStackListenerCallback); 415 } 416 } 417 registerSettingObservers(int newUserId)418 private void registerSettingObservers(int newUserId) { 419 mOneHandedSettingsUtil.registerSettingsKeyObserver( 420 Settings.Secure.ONE_HANDED_MODE_ACTIVATED, 421 mContext.getContentResolver(), mActivatedObserver, newUserId); 422 mOneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.ONE_HANDED_MODE_ENABLED, 423 mContext.getContentResolver(), mEnabledObserver, newUserId); 424 mOneHandedSettingsUtil.registerSettingsKeyObserver( 425 Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 426 mContext.getContentResolver(), mSwipeToNotificationEnabledObserver, newUserId); 427 mOneHandedSettingsUtil.registerSettingsKeyObserver( 428 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, 429 mContext.getContentResolver(), mShortcutEnabledObserver, newUserId); 430 mOneHandedSettingsUtil.registerSettingsKeyObserver( 431 Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, 432 mContext.getContentResolver(), mShortcutEnabledObserver, newUserId); 433 } 434 unregisterSettingObservers()435 private void unregisterSettingObservers() { 436 mOneHandedSettingsUtil.unregisterSettingsKeyObserver(mContext.getContentResolver(), 437 mEnabledObserver); 438 mOneHandedSettingsUtil.unregisterSettingsKeyObserver(mContext.getContentResolver(), 439 mSwipeToNotificationEnabledObserver); 440 mOneHandedSettingsUtil.unregisterSettingsKeyObserver(mContext.getContentResolver(), 441 mShortcutEnabledObserver); 442 } 443 updateSettings()444 private void updateSettings() { 445 setOneHandedEnabled(mOneHandedSettingsUtil 446 .getSettingsOneHandedModeEnabled(mContext.getContentResolver(), mUserId)); 447 mTimeoutHandler.setTimeout(mOneHandedSettingsUtil 448 .getSettingsOneHandedModeTimeout(mContext.getContentResolver(), mUserId)); 449 setTaskChangeToExit(mOneHandedSettingsUtil 450 .getSettingsTapsAppToExit(mContext.getContentResolver(), mUserId)); 451 setSwipeToNotificationEnabled(mOneHandedSettingsUtil 452 .getSettingsSwipeToNotificationEnabled(mContext.getContentResolver(), mUserId)); 453 onShortcutEnabledChanged(); 454 } 455 updateDisplayLayout(int displayId)456 private void updateDisplayLayout(int displayId) { 457 final DisplayLayout newDisplayLayout = mDisplayController.getDisplayLayout(displayId); 458 mDisplayAreaOrganizer.setDisplayLayout(newDisplayLayout); 459 mTutorialHandler.onDisplayChanged(newDisplayLayout); 460 mBackgroundPanelOrganizer.onDisplayChanged(newDisplayLayout); 461 } 462 getObserver(Runnable onChangeRunnable)463 private ContentObserver getObserver(Runnable onChangeRunnable) { 464 return new ContentObserver(mMainHandler) { 465 @Override 466 public void onChange(boolean selfChange) { 467 onChangeRunnable.run(); 468 } 469 }; 470 } 471 472 @VisibleForTesting 473 void notifyExpandNotification() { 474 if (mEventCallback != null) { 475 mMainExecutor.execute(() -> mEventCallback.notifyExpandNotification()); 476 } 477 } 478 479 @VisibleForTesting 480 void onActivatedActionChanged() { 481 if (!isShortcutEnabled()) { 482 Slog.w(TAG, "Shortcut not enabled, skip onActivatedActionChanged()"); 483 return; 484 } 485 486 if (!isOneHandedEnabled()) { 487 final boolean success = mOneHandedSettingsUtil.setOneHandedModeEnabled( 488 mContext.getContentResolver(), 1 /* Enabled for shortcut */, mUserId); 489 Slog.d(TAG, "Auto enabled One-handed mode by shortcut trigger, success=" + success); 490 } 491 492 if (isSwipeToNotificationEnabled()) { 493 notifyExpandNotification(); 494 return; 495 } 496 497 final boolean isActivated = mState.getState() == STATE_ACTIVE; 498 final boolean requestActivated = mOneHandedSettingsUtil.getOneHandedModeActivated( 499 mContext.getContentResolver(), mUserId); 500 // When gesture trigger action, we will update settings and introduce observer callback 501 // again, then the following logic will just ignore the second redundant callback. 502 if (isActivated ^ requestActivated) { 503 if (requestActivated) { 504 startOneHanded(); 505 } else { 506 stopOneHanded(); 507 } 508 } 509 } 510 511 @VisibleForTesting 512 void onEnabledSettingChanged() { 513 final boolean enabled = mOneHandedSettingsUtil.getSettingsOneHandedModeEnabled( 514 mContext.getContentResolver(), mUserId); 515 mOneHandedUiEventLogger.writeEvent(enabled 516 ? OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_ENABLED_ON 517 : OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF); 518 519 setOneHandedEnabled(enabled); 520 521 // Also checks swipe to notification settings since they all need gesture overlay. 522 setEnabledGesturalOverlay( 523 enabled || mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( 524 mContext.getContentResolver(), mUserId), true /* DelayExecute */); 525 } 526 527 @VisibleForTesting 528 void onSwipeToNotificationEnabledChanged() { 529 final boolean enabled = 530 mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( 531 mContext.getContentResolver(), mUserId); 532 setSwipeToNotificationEnabled(enabled); 533 notifyShortcutStateChanged(mState.getState()); 534 535 mOneHandedUiEventLogger.writeEvent(enabled 536 ? OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_SHOW_NOTIFICATION_ENABLED_ON 537 : OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_SHOW_NOTIFICATION_ENABLED_OFF); 538 } 539 540 void onShortcutEnabledChanged() { 541 mIsShortcutEnabled = mOneHandedSettingsUtil.getShortcutEnabled( 542 mContext.getContentResolver(), mUserId); 543 544 mOneHandedUiEventLogger.writeEvent(mIsShortcutEnabled 545 ? OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_SHORTCUT_ENABLED_ON 546 : OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_SHORTCUT_ENABLED_OFF); 547 } 548 549 private void setupTimeoutListener() { 550 mTimeoutHandler.registerTimeoutListener(timeoutTime -> stopOneHanded( 551 OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT)); 552 } 553 554 @VisibleForTesting 555 boolean isLockedDisabled() { 556 return mLockedDisabled; 557 } 558 559 @VisibleForTesting 560 boolean isOneHandedEnabled() { 561 return mIsOneHandedEnabled; 562 } 563 564 @VisibleForTesting 565 boolean isShortcutEnabled() { 566 return mIsShortcutEnabled; 567 } 568 569 @VisibleForTesting 570 boolean isSwipeToNotificationEnabled() { 571 return mIsSwipeToNotificationEnabled; 572 } 573 574 private void updateOneHandedEnabled() { 575 if (mState.getState() == STATE_ENTERING || mState.getState() == STATE_ACTIVE) { 576 mMainExecutor.execute(() -> stopOneHanded()); 577 } 578 579 // If setting is pull screen, notify shortcut one_handed_mode_activated to reset 580 // and align status with current mState when one-handed gesture enabled. 581 if (isOneHandedEnabled() && !isSwipeToNotificationEnabled()) { 582 notifyShortcutStateChanged(mState.getState()); 583 } 584 585 mTouchHandler.onOneHandedEnabled(mIsOneHandedEnabled); 586 587 if (!mIsOneHandedEnabled) { 588 mDisplayAreaOrganizer.unregisterOrganizer(); 589 mBackgroundPanelOrganizer.unregisterOrganizer(); 590 // Do NOT register + unRegister DA in the same call 591 return; 592 } 593 594 if (mDisplayAreaOrganizer.getDisplayAreaTokenMap().isEmpty()) { 595 mDisplayAreaOrganizer.registerOrganizer( 596 OneHandedDisplayAreaOrganizer.FEATURE_ONE_HANDED); 597 } 598 599 if (!mBackgroundPanelOrganizer.isRegistered()) { 600 mBackgroundPanelOrganizer.registerOrganizer( 601 OneHandedBackgroundPanelOrganizer.FEATURE_ONE_HANDED_BACKGROUND_PANEL); 602 } 603 } 604 605 private void setupGesturalOverlay() { 606 if (!mOneHandedSettingsUtil.getSettingsOneHandedModeEnabled( 607 mContext.getContentResolver(), mUserId)) { 608 return; 609 } 610 611 OverlayInfo info = null; 612 try { 613 mOverlayManager.setHighestPriority(ONE_HANDED_MODE_GESTURAL_OVERLAY, USER_CURRENT); 614 info = mOverlayManager.getOverlayInfo(ONE_HANDED_MODE_GESTURAL_OVERLAY, USER_CURRENT); 615 } catch (RemoteException e) { /* Do nothing */ } 616 617 if (info != null && !info.isEnabled()) { 618 // Enable the default gestural one handed overlay. 619 setEnabledGesturalOverlay(true /* enabled */, false /* delayExecute */); 620 } 621 } 622 623 @VisibleForTesting 624 private void setEnabledGesturalOverlay(boolean enabled, boolean delayExecute) { 625 if (mState.isTransitioning() || delayExecute) { 626 // Enabled overlay package may affect the current animation(e.g:Settings switch), 627 // so we delay 250ms to enabled overlay after switch animation finish, only delay once. 628 mMainExecutor.executeDelayed(() -> setEnabledGesturalOverlay(enabled, false), 629 OVERLAY_ENABLED_DELAY_MS); 630 return; 631 } 632 try { 633 mOverlayManager.setEnabled(ONE_HANDED_MODE_GESTURAL_OVERLAY, enabled, USER_CURRENT); 634 } catch (RemoteException e) { 635 throw e.rethrowFromSystemServer(); 636 } 637 } 638 639 @VisibleForTesting 640 void setLockedDisabled(boolean locked, boolean enabled) { 641 final boolean isFeatureEnabled = mIsOneHandedEnabled || mIsSwipeToNotificationEnabled; 642 643 if (enabled == isFeatureEnabled) { 644 return; 645 } 646 647 mLockedDisabled = locked && !enabled; 648 } 649 650 private void onConfigChanged(Configuration newConfig) { 651 if (mTutorialHandler == null || mBackgroundPanelOrganizer == null) { 652 return; 653 } 654 if (!mIsOneHandedEnabled || newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { 655 return; 656 } 657 mBackgroundPanelOrganizer.onConfigurationChanged(); 658 mTutorialHandler.onConfigurationChanged(); 659 } 660 661 private void onKeyguardVisibilityChanged(boolean showing) { 662 mKeyguardShowing = showing; 663 } 664 665 private void onUserSwitch(int newUserId) { 666 unregisterSettingObservers(); 667 mUserId = newUserId; 668 registerSettingObservers(newUserId); 669 updateSettings(); 670 updateOneHandedEnabled(); 671 } 672 673 public void dump(@NonNull PrintWriter pw) { 674 final String innerPrefix = " "; 675 pw.println(); 676 pw.println(TAG); 677 pw.print(innerPrefix + "mOffSetFraction="); 678 pw.println(mOffSetFraction); 679 pw.print(innerPrefix + "mLockedDisabled="); 680 pw.println(mLockedDisabled); 681 pw.print(innerPrefix + "mUserId="); 682 pw.println(mUserId); 683 pw.print(innerPrefix + "isShortcutEnabled="); 684 pw.println(isShortcutEnabled()); 685 pw.print(innerPrefix + "mIsSwipeToNotificationEnabled="); 686 pw.println(mIsSwipeToNotificationEnabled); 687 688 if (mBackgroundPanelOrganizer != null) { 689 mBackgroundPanelOrganizer.dump(pw); 690 } 691 692 if (mDisplayAreaOrganizer != null) { 693 mDisplayAreaOrganizer.dump(pw); 694 } 695 696 if (mTouchHandler != null) { 697 mTouchHandler.dump(pw); 698 } 699 700 if (mTimeoutHandler != null) { 701 mTimeoutHandler.dump(pw); 702 } 703 704 if (mState != null) { 705 mState.dump(pw); 706 } 707 708 if (mTutorialHandler != null) { 709 mTutorialHandler.dump(pw); 710 } 711 712 if (mOneHandedAccessibilityUtil != null) { 713 mOneHandedAccessibilityUtil.dump(pw); 714 } 715 716 mOneHandedSettingsUtil.dump(pw, innerPrefix, mContext.getContentResolver(), mUserId); 717 718 if (mOverlayManager != null) { 719 OverlayInfo info = null; 720 try { 721 info = mOverlayManager.getOverlayInfo(ONE_HANDED_MODE_GESTURAL_OVERLAY, 722 USER_CURRENT); 723 } catch (RemoteException e) { /* Do nothing */ } 724 725 if (info != null && !info.isEnabled()) { 726 pw.print(innerPrefix + "OverlayInfo="); 727 pw.println(info); 728 } 729 } 730 } 731 732 /** 733 * Handles rotation based on OnDisplayChangingListener callback 734 */ 735 @Override 736 public void onRotateDisplay(int displayId, int fromRotation, int toRotation, 737 WindowContainerTransaction wct) { 738 if (!isInitialized()) { 739 return; 740 } 741 742 if (!mOneHandedSettingsUtil.getSettingsOneHandedModeEnabled(mContext.getContentResolver(), 743 mUserId) || mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( 744 mContext.getContentResolver(), mUserId)) { 745 return; 746 } 747 748 mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation, wct); 749 mOneHandedUiEventLogger.writeEvent( 750 OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT); 751 } 752 753 /** 754 * The interface for calls from outside the Shell, within the host process. 755 */ 756 @ExternalThread 757 private class OneHandedImpl implements OneHanded { 758 private IOneHandedImpl mIOneHanded; 759 760 @Override 761 public IOneHanded createExternalInterface() { 762 if (mIOneHanded != null) { 763 mIOneHanded.invalidate(); 764 } 765 mIOneHanded = new IOneHandedImpl(OneHandedController.this); 766 return mIOneHanded; 767 } 768 769 @Override 770 public boolean isOneHandedEnabled() { 771 // This is volatile so return directly 772 return mIsOneHandedEnabled; 773 } 774 775 @Override 776 public boolean isSwipeToNotificationEnabled() { 777 // This is volatile so return directly 778 return mIsSwipeToNotificationEnabled; 779 } 780 781 @Override 782 public void startOneHanded() { 783 mMainExecutor.execute(() -> { 784 OneHandedController.this.startOneHanded(); 785 }); 786 } 787 788 @Override 789 public void stopOneHanded() { 790 mMainExecutor.execute(() -> { 791 OneHandedController.this.stopOneHanded(); 792 }); 793 } 794 795 @Override 796 public void stopOneHanded(int event) { 797 mMainExecutor.execute(() -> { 798 OneHandedController.this.stopOneHanded(event); 799 }); 800 } 801 802 @Override 803 public void setLockedDisabled(boolean locked, boolean enabled) { 804 mMainExecutor.execute(() -> { 805 OneHandedController.this.setLockedDisabled(locked, enabled); 806 }); 807 } 808 809 @Override 810 public void registerEventCallback(OneHandedEventCallback callback) { 811 mMainExecutor.execute(() -> { 812 OneHandedController.this.registerEventCallback(callback); 813 }); 814 } 815 816 @Override 817 public void registerTransitionCallback(OneHandedTransitionCallback callback) { 818 mMainExecutor.execute(() -> { 819 OneHandedController.this.registerTransitionCallback(callback); 820 }); 821 } 822 823 @Override 824 public void onConfigChanged(Configuration newConfig) { 825 mMainExecutor.execute(() -> { 826 OneHandedController.this.onConfigChanged(newConfig); 827 }); 828 } 829 830 @Override 831 public void onUserSwitch(int userId) { 832 mMainExecutor.execute(() -> { 833 OneHandedController.this.onUserSwitch(userId); 834 }); 835 } 836 837 @Override 838 public void onKeyguardVisibilityChanged(boolean showing) { 839 mMainExecutor.execute(() -> { 840 OneHandedController.this.onKeyguardVisibilityChanged(showing); 841 }); 842 } 843 } 844 845 /** 846 * The interface for calls from outside the host process. 847 */ 848 @BinderThread 849 private static class IOneHandedImpl extends IOneHanded.Stub { 850 private OneHandedController mController; 851 852 IOneHandedImpl(OneHandedController controller) { 853 mController = controller; 854 } 855 856 /** 857 * Invalidates this instance, preventing future calls from updating the controller. 858 */ 859 void invalidate() { 860 mController = null; 861 } 862 863 @Override 864 public void startOneHanded() { 865 executeRemoteCallWithTaskPermission(mController, "startOneHanded", 866 (controller) -> { 867 controller.startOneHanded(); 868 }); 869 } 870 871 @Override 872 public void stopOneHanded() { 873 executeRemoteCallWithTaskPermission(mController, "stopOneHanded", 874 (controller) -> { 875 controller.stopOneHanded(); 876 }); 877 } 878 } 879 } 880