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