blob: 5fc7c987899f5b66bd49116cfe7a7f0fbdc386e2 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.wm.shell.onehanded;
import static android.os.UserHandle.USER_CURRENT;
import android.content.ComponentName;
import android.content.Context;
import android.content.om.IOverlayManager;
import android.content.om.OverlayInfo;
import android.content.res.Configuration;
import android.database.ContentObserver;
import android.graphics.Rect;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.provider.Settings;
import android.util.Slog;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayChangeController;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TaskStackListenerCallback;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.common.annotations.ExternalThread;
import com.android.wm.shell.onehanded.OneHandedGestureHandler.OneHandedGestureEventCallback;
import java.io.PrintWriter;
/**
* Manages and manipulates the one handed states, transitions, and gesture for phones.
*/
public class OneHandedController {
private static final String TAG = "OneHandedController";
private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE =
"persist.debug.one_handed_offset_percentage";
private static final String ONE_HANDED_MODE_GESTURAL_OVERLAY =
"com.android.internal.systemui.onehanded.gestural";
static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode";
private volatile boolean mIsOneHandedEnabled;
private volatile boolean mIsSwipeToNotificationEnabled;
private boolean mTaskChangeToExit;
private boolean mLockedDisabled;
private float mOffSetFraction;
private final Context mContext;
private final DisplayController mDisplayController;
private final OneHandedGestureHandler mGestureHandler;
private final OneHandedTimeoutHandler mTimeoutHandler;
private final OneHandedTouchHandler mTouchHandler;
private final OneHandedTutorialHandler mTutorialHandler;
private final OneHandedUiEventLogger mOneHandedUiEventLogger;
private final TaskStackListenerImpl mTaskStackListener;
private final IOverlayManager mOverlayManager;
private final ShellExecutor mMainExecutor;
private final Handler mMainHandler;
private final OneHandedImpl mImpl = new OneHandedImpl();
private final WindowManager mWindowManager;
private OneHandedDisplayAreaOrganizer mDisplayAreaOrganizer;
private final AccessibilityManager mAccessibilityManager;
private OneHandedBackgroundPanelOrganizer mBackgroundPanelOrganizer;
/**
* Handle rotation based on OnDisplayChangingListener callback
*/
private final DisplayChangeController.OnDisplayChangingListener mRotationController =
(display, fromRotation, toRotation, wct) -> {
if (mDisplayAreaOrganizer != null) {
mDisplayAreaOrganizer.onRotateDisplay(fromRotation, toRotation, wct);
}
};
private final ContentObserver mEnabledObserver;
private final ContentObserver mTimeoutObserver;
private final ContentObserver mTaskChangeExitObserver;
private final ContentObserver mSwipeToNotificationEnabledObserver;
private AccessibilityManager.AccessibilityStateChangeListener
mAccessibilityStateChangeListener =
new AccessibilityManager.AccessibilityStateChangeListener() {
@Override
public void onAccessibilityStateChanged(boolean enabled) {
if (enabled) {
final int mOneHandedTimeout = OneHandedSettingsUtil
.getSettingsOneHandedModeTimeout(mContext.getContentResolver());
final int timeout = mAccessibilityManager
.getRecommendedTimeoutMillis(mOneHandedTimeout * 1000
/* align with A11y timeout millis */,
AccessibilityManager.FLAG_CONTENT_CONTROLS);
mTimeoutHandler.setTimeout(timeout / 1000);
} else {
mTimeoutHandler.setTimeout(OneHandedSettingsUtil
.getSettingsOneHandedModeTimeout(mContext.getContentResolver()));
}
}
};
private final TaskStackListenerCallback mTaskStackListenerCallback =
new TaskStackListenerCallback() {
@Override
public void onTaskCreated(int taskId, ComponentName componentName) {
stopOneHanded(OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT);
}
@Override
public void onTaskMovedToFront(int taskId) {
stopOneHanded(OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT);
}
};
/**
* Creates {@link OneHandedController}, returns {@code null} if the feature is not supported.
*/
@Nullable
public static OneHandedController create(
Context context, WindowManager windowManager, DisplayController displayController,
TaskStackListenerImpl taskStackListener, UiEventLogger uiEventLogger,
ShellExecutor mainExecutor, Handler mainHandler) {
if (!SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false)) {
Slog.w(TAG, "Device doesn't support OneHanded feature");
return null;
}
OneHandedTimeoutHandler timeoutHandler = new OneHandedTimeoutHandler(mainExecutor);
OneHandedTutorialHandler tutorialHandler = new OneHandedTutorialHandler(context,
windowManager, mainExecutor);
OneHandedAnimationController animationController =
new OneHandedAnimationController(context);
OneHandedTouchHandler touchHandler = new OneHandedTouchHandler(timeoutHandler,
mainExecutor);
OneHandedGestureHandler gestureHandler = new OneHandedGestureHandler(
context, windowManager, displayController, ViewConfiguration.get(context),
mainExecutor);
OneHandedBackgroundPanelOrganizer oneHandedBackgroundPanelOrganizer =
new OneHandedBackgroundPanelOrganizer(context, windowManager, displayController,
mainExecutor);
OneHandedDisplayAreaOrganizer organizer = new OneHandedDisplayAreaOrganizer(
context, windowManager, displayController, animationController, tutorialHandler,
oneHandedBackgroundPanelOrganizer, mainExecutor);
OneHandedUiEventLogger oneHandedUiEventsLogger = new OneHandedUiEventLogger(uiEventLogger);
IOverlayManager overlayManager = IOverlayManager.Stub.asInterface(
ServiceManager.getService(Context.OVERLAY_SERVICE));
return new OneHandedController(context, windowManager, displayController,
oneHandedBackgroundPanelOrganizer, organizer, touchHandler, tutorialHandler,
gestureHandler, timeoutHandler, oneHandedUiEventsLogger, overlayManager,
taskStackListener, mainExecutor, mainHandler);
}
@VisibleForTesting
OneHandedController(Context context,
WindowManager windowManager,
DisplayController displayController,
OneHandedBackgroundPanelOrganizer backgroundPanelOrganizer,
OneHandedDisplayAreaOrganizer displayAreaOrganizer,
OneHandedTouchHandler touchHandler,
OneHandedTutorialHandler tutorialHandler,
OneHandedGestureHandler gestureHandler,
OneHandedTimeoutHandler timeoutHandler,
OneHandedUiEventLogger uiEventsLogger,
IOverlayManager overlayManager,
TaskStackListenerImpl taskStackListener,
ShellExecutor mainExecutor,
Handler mainHandler) {
mContext = context;
mWindowManager = windowManager;
mBackgroundPanelOrganizer = backgroundPanelOrganizer;
mDisplayAreaOrganizer = displayAreaOrganizer;
mDisplayController = displayController;
mTouchHandler = touchHandler;
mTutorialHandler = tutorialHandler;
mGestureHandler = gestureHandler;
mOverlayManager = overlayManager;
mMainExecutor = mainExecutor;
mMainHandler = mainHandler;
mOneHandedUiEventLogger = uiEventsLogger;
mTaskStackListener = taskStackListener;
final float offsetPercentageConfig = context.getResources().getFraction(
R.fraction.config_one_handed_offset, 1, 1);
final int sysPropPercentageConfig = SystemProperties.getInt(
ONE_HANDED_MODE_OFFSET_PERCENTAGE, Math.round(offsetPercentageConfig * 100.0f));
mOffSetFraction = sysPropPercentageConfig / 100.0f;
mIsOneHandedEnabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
context.getContentResolver());
mIsSwipeToNotificationEnabled =
OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
context.getContentResolver());
mTimeoutHandler = timeoutHandler;
mEnabledObserver = getObserver(this::onEnabledSettingChanged);
mTimeoutObserver = getObserver(this::onTimeoutSettingChanged);
mTaskChangeExitObserver = getObserver(this::onTaskChangeExitSettingChanged);
mSwipeToNotificationEnabledObserver =
getObserver(this::onSwipeToNotificationEnabledSettingChanged);
mDisplayController.addDisplayChangingController(mRotationController);
setupCallback();
setupSettingObservers();
setupTimeoutListener();
setupGesturalOverlay();
updateSettings();
mAccessibilityManager = AccessibilityManager.getInstance(context);
mAccessibilityManager.addAccessibilityStateChangeListener(
mAccessibilityStateChangeListener);
}
public OneHanded asOneHanded() {
return mImpl;
}
/**
* Set one handed enabled or disabled when user update settings
*/
void setOneHandedEnabled(boolean enabled) {
mIsOneHandedEnabled = enabled;
updateOneHandedEnabled();
}
/**
* Set one handed enabled or disabled by when user update settings
*/
void setTaskChangeToExit(boolean enabled) {
if (enabled) {
mTaskStackListener.addListener(mTaskStackListenerCallback);
} else {
mTaskStackListener.removeListener(mTaskStackListenerCallback);
}
mTaskChangeToExit = enabled;
}
/**
* Sets whether to enable swipe bottom to notification gesture when user update settings.
*/
void setSwipeToNotificationEnabled(boolean enabled) {
mIsSwipeToNotificationEnabled = enabled;
updateOneHandedEnabled();
}
@VisibleForTesting
void startOneHanded() {
if (isLockedDisabled()) {
Slog.d(TAG, "Temporary lock disabled");
return;
}
if (!mDisplayAreaOrganizer.isInOneHanded()) {
final int yOffSet = Math.round(getDisplaySize().height() * mOffSetFraction);
mDisplayAreaOrganizer.scheduleOffset(0, yOffSet);
mTimeoutHandler.resetTimer();
mOneHandedUiEventLogger.writeEvent(
OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_GESTURE_IN);
}
}
@VisibleForTesting
void stopOneHanded() {
if (mDisplayAreaOrganizer.isInOneHanded()) {
mDisplayAreaOrganizer.scheduleOffset(0, 0);
mTimeoutHandler.removeTimer();
}
}
private void stopOneHanded(int uiEvent) {
if (mDisplayAreaOrganizer.isInOneHanded()) {
mDisplayAreaOrganizer.scheduleOffset(0, 0);
mTimeoutHandler.removeTimer();
mOneHandedUiEventLogger.writeEvent(uiEvent);
}
}
private void setThreeButtonModeEnabled(boolean enabled) {
mGestureHandler.onThreeButtonModeEnabled(enabled);
}
@VisibleForTesting
void registerTransitionCallback(OneHandedTransitionCallback callback) {
mDisplayAreaOrganizer.registerTransitionCallback(callback);
}
private void registerGestureCallback(OneHandedGestureEventCallback callback) {
mGestureHandler.setGestureEventListener(callback);
}
private void setupCallback() {
mTouchHandler.registerTouchEventListener(() ->
stopOneHanded(OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT));
mDisplayAreaOrganizer.registerTransitionCallback(mTouchHandler);
mDisplayAreaOrganizer.registerTransitionCallback(mGestureHandler);
mDisplayAreaOrganizer.registerTransitionCallback(mTutorialHandler);
mDisplayAreaOrganizer.registerTransitionCallback(mBackgroundPanelOrganizer);
if (mTaskChangeToExit) {
mTaskStackListener.addListener(mTaskStackListenerCallback);
}
}
private void setupSettingObservers() {
OneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.ONE_HANDED_MODE_ENABLED,
mContext.getContentResolver(), mEnabledObserver);
OneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.ONE_HANDED_MODE_TIMEOUT,
mContext.getContentResolver(), mTimeoutObserver);
OneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.TAPS_APP_TO_EXIT,
mContext.getContentResolver(), mTaskChangeExitObserver);
OneHandedSettingsUtil.registerSettingsKeyObserver(
Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED,
mContext.getContentResolver(), mSwipeToNotificationEnabledObserver);
}
private void updateSettings() {
setOneHandedEnabled(OneHandedSettingsUtil
.getSettingsOneHandedModeEnabled(mContext.getContentResolver()));
mTimeoutHandler.setTimeout(OneHandedSettingsUtil
.getSettingsOneHandedModeTimeout(mContext.getContentResolver()));
setTaskChangeToExit(OneHandedSettingsUtil
.getSettingsTapsAppToExit(mContext.getContentResolver()));
setSwipeToNotificationEnabled(OneHandedSettingsUtil
.getSettingsSwipeToNotificationEnabled(mContext.getContentResolver()));
}
private ContentObserver getObserver(Runnable onChangeRunnable) {
return new ContentObserver(mMainHandler) {
@Override
public void onChange(boolean selfChange) {
onChangeRunnable.run();
}
};
}
@VisibleForTesting
void onEnabledSettingChanged() {
final boolean enabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
mContext.getContentResolver());
mOneHandedUiEventLogger.writeEvent(enabled
? OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_ENABLED_ON
: OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF);
setOneHandedEnabled(enabled);
// Also checks swipe to notification settings since they all need gesture overlay.
setEnabledGesturalOverlay(
enabled || OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
mContext.getContentResolver()));
}
@VisibleForTesting
void onTimeoutSettingChanged() {
final int newTimeout = OneHandedSettingsUtil.getSettingsOneHandedModeTimeout(
mContext.getContentResolver());
int metricsId = OneHandedUiEventLogger.OneHandedSettingsTogglesEvent.INVALID.getId();
switch (newTimeout) {
case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER:
metricsId = OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER;
break;
case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS:
metricsId = OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4;
break;
case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS:
metricsId = OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8;
break;
case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_LONG_IN_SECONDS:
metricsId = OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12;
break;
default:
// do nothing
break;
}
mOneHandedUiEventLogger.writeEvent(metricsId);
if (mTimeoutHandler != null) {
mTimeoutHandler.setTimeout(newTimeout);
}
}
@VisibleForTesting
void onTaskChangeExitSettingChanged() {
final boolean enabled = OneHandedSettingsUtil.getSettingsTapsAppToExit(
mContext.getContentResolver());
mOneHandedUiEventLogger.writeEvent(enabled
? OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON
: OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF);
setTaskChangeToExit(enabled);
}
@VisibleForTesting
void onSwipeToNotificationEnabledSettingChanged() {
final boolean enabled =
OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
mContext.getContentResolver());
setSwipeToNotificationEnabled(enabled);
// Also checks one handed mode settings since they all need gesture overlay.
setEnabledGesturalOverlay(
enabled || OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
mContext.getContentResolver()));
}
private void setupTimeoutListener() {
mTimeoutHandler.registerTimeoutListener(timeoutTime -> stopOneHanded(
OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT));
}
/**
* Query the current display real size from {@link WindowManager}
*
* @return {@link WindowManager#getCurrentWindowMetrics()#getBounds()}
*/
private Rect getDisplaySize() {
if (mWindowManager == null) {
Slog.e(TAG, "WindowManager instance is null! Can not get display size!");
return new Rect();
}
final Rect displaySize = mWindowManager.getCurrentWindowMetrics().getBounds();
if (displaySize.width() == 0 || displaySize.height() == 0) {
Slog.e(TAG, "Display size error! width = " + displaySize.width()
+ ", height = " + displaySize.height());
}
return displaySize;
}
@VisibleForTesting
boolean isLockedDisabled() {
return mLockedDisabled;
}
private void updateOneHandedEnabled() {
if (mDisplayAreaOrganizer.isInOneHanded()) {
stopOneHanded();
}
mTouchHandler.onOneHandedEnabled(mIsOneHandedEnabled);
mGestureHandler.onOneHandedEnabled(mIsOneHandedEnabled || mIsSwipeToNotificationEnabled);
if (!mIsOneHandedEnabled) {
mDisplayAreaOrganizer.unregisterOrganizer();
mBackgroundPanelOrganizer.unregisterOrganizer();
// Do NOT register + unRegister DA in the same call
return;
}
if (mDisplayAreaOrganizer.getDisplayAreaTokenMap().isEmpty()) {
mDisplayAreaOrganizer.registerOrganizer(
OneHandedDisplayAreaOrganizer.FEATURE_ONE_HANDED);
}
if (mBackgroundPanelOrganizer.getBackgroundSurface() == null) {
mBackgroundPanelOrganizer.registerOrganizer(
OneHandedBackgroundPanelOrganizer.FEATURE_ONE_HANDED_BACKGROUND_PANEL);
}
}
private void setupGesturalOverlay() {
if (!OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(mContext.getContentResolver())) {
return;
}
OverlayInfo info = null;
try {
mOverlayManager.setHighestPriority(ONE_HANDED_MODE_GESTURAL_OVERLAY, USER_CURRENT);
info = mOverlayManager.getOverlayInfo(ONE_HANDED_MODE_GESTURAL_OVERLAY, USER_CURRENT);
} catch (RemoteException e) { /* Do nothing */ }
if (info != null && !info.isEnabled()) {
// Enable the default gestural one handed overlay.
setEnabledGesturalOverlay(true);
}
}
@VisibleForTesting
private void setEnabledGesturalOverlay(boolean enabled) {
try {
mOverlayManager.setEnabled(ONE_HANDED_MODE_GESTURAL_OVERLAY, enabled, USER_CURRENT);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
@VisibleForTesting
void setLockedDisabled(boolean locked, boolean enabled) {
if (enabled == mIsOneHandedEnabled) {
return;
}
mLockedDisabled = locked && !enabled;
}
private void onConfigChanged(Configuration newConfig) {
if (mTutorialHandler != null) {
if (!mIsOneHandedEnabled
|| newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
return;
}
mTutorialHandler.onConfigurationChanged(newConfig);
}
}
public void dump(@NonNull PrintWriter pw) {
final String innerPrefix = " ";
pw.println(TAG + "States: ");
pw.print(innerPrefix + "mOffSetFraction=");
pw.println(mOffSetFraction);
pw.print(innerPrefix + "mLockedDisabled=");
pw.println(mLockedDisabled);
if (mDisplayAreaOrganizer != null) {
mDisplayAreaOrganizer.dump(pw);
}
if (mGestureHandler != null) {
mGestureHandler.dump(pw);
}
if (mTouchHandler != null) {
mTouchHandler.dump(pw);
}
if (mTimeoutHandler != null) {
mTimeoutHandler.dump(pw);
}
if (mTutorialHandler != null) {
mTutorialHandler.dump(pw);
}
OneHandedSettingsUtil.dump(pw, innerPrefix, mContext.getContentResolver());
if (mOverlayManager != null) {
OverlayInfo info = null;
try {
info = mOverlayManager.getOverlayInfo(ONE_HANDED_MODE_GESTURAL_OVERLAY,
USER_CURRENT);
} catch (RemoteException e) { /* Do nothing */ }
if (info != null && !info.isEnabled()) {
pw.print(innerPrefix + "OverlayInfo=");
pw.println(info);
}
}
}
@ExternalThread
private class OneHandedImpl implements OneHanded {
@Override
public boolean isOneHandedEnabled() {
// This is volatile so return directly
return mIsOneHandedEnabled;
}
@Override
public boolean isSwipeToNotificationEnabled() {
// This is volatile so return directly
return mIsSwipeToNotificationEnabled;
}
@Override
public void startOneHanded() {
mMainExecutor.execute(() -> {
OneHandedController.this.startOneHanded();
});
}
@Override
public void stopOneHanded() {
mMainExecutor.execute(() -> {
OneHandedController.this.stopOneHanded();
});
}
@Override
public void stopOneHanded(int event) {
mMainExecutor.execute(() -> {
OneHandedController.this.stopOneHanded(event);
});
}
@Override
public void setThreeButtonModeEnabled(boolean enabled) {
mMainExecutor.execute(() -> {
OneHandedController.this.setThreeButtonModeEnabled(enabled);
});
}
@Override
public void setLockedDisabled(boolean locked, boolean enabled) {
mMainExecutor.execute(() -> {
OneHandedController.this.setLockedDisabled(locked, enabled);
});
}
@Override
public void registerTransitionCallback(OneHandedTransitionCallback callback) {
mMainExecutor.execute(() -> {
OneHandedController.this.registerTransitionCallback(callback);
});
}
@Override
public void registerGestureCallback(OneHandedGestureEventCallback callback) {
mMainExecutor.execute(() -> {
OneHandedController.this.registerGestureCallback(callback);
});
}
@Override
public void onConfigChanged(Configuration newConfig) {
mMainExecutor.execute(() -> {
OneHandedController.this.onConfigChanged(newConfig);
});
}
}
}