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