• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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