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