Allow one-handed mode shortcut expand notification

1) Add OneHandedEventCallback for WMShell to register
   callback, and if one handed mode settings set show
   notifications, when user tap one handed mode shortcut
   notify WMShell send KEYCODE_SYSTEM_NAVIGATION_DOWN
   through OneHandedEventCallback with SysUIMainExecutor.

2) Add isReady() flag in OneHandedDisplayAreaOrganizer
   and Simplify the flow of onActivatedActionChanged()
   to prevent potential race problem while auto-enable
   One-handed and immeditately startOneHanded(), however,
   DisplayAreaOrganizer#onDisplayAreaAppeared() may not
   ready for transtion yet, and result mState keep in
   unfinsihed STATE_ENTERING.

3) Adjust the transiton duration from 800ms to 600ms
   800ms is a little bit long to affect timing of
   apply runtime overlay package.

4) DelayExecute setEnabledGesturalOverlay() until
   transition finised and at most delay once.
   - onSwipeToNotificationEnabledChanged() : DelayExecute
   - onEnabledSettingChanged() : DelayExecute
   - Ctor setupGesturalOverlay() : No DelayExecute
   - mState.isTransitioning() : DelayExecute

Test: WMShellUnitTests
Bug: 182425480
Change-Id: I2615c3b30ad95a858510b4bcc73d8f343843fc96
This commit is contained in:
Bill Lin
2021-06-15 04:46:54 +08:00
parent 96d2926fc7
commit ca75e7e466
10 changed files with 186 additions and 21 deletions

View File

@@ -40,7 +40,7 @@
<integer name="long_press_dock_anim_duration">250</integer>
<!-- Animation duration for translating of one handed when trigger / dismiss. -->
<integer name="config_one_handed_translate_animation_duration">800</integer>
<integer name="config_one_handed_translate_animation_duration">600</integer>
<!-- One handed mode default offset % of display size -->
<fraction name="config_one_handed_offset">40%</fraction>

View File

@@ -67,6 +67,11 @@ public interface OneHanded {
*/
void setLockedDisabled(boolean locked, boolean enabled);
/**
* Registers callback to notify WMShell when user tap shortcut to expand notification.
*/
void registerEventCallback(OneHandedEventCallback callback);
/**
* Registers callback to be notified after {@link OneHandedDisplayAreaOrganizer}
* transition start or finish

View File

@@ -43,6 +43,7 @@ import android.util.Slog;
import android.view.Surface;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -72,6 +73,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
private static final String ONE_HANDED_MODE_GESTURAL_OVERLAY =
"com.android.internal.systemui.onehanded.gestural";
private static final int OVERLAY_ENABLED_DELAY_MS = 250;
private static final int DISPLAY_AREA_READY_RETRY_MS = 10;
static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode";
@@ -99,6 +101,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
private final Handler mMainHandler;
private final OneHandedImpl mImpl = new OneHandedImpl();
private OneHandedEventCallback mEventCallback;
private OneHandedDisplayAreaOrganizer mDisplayAreaOrganizer;
private OneHandedBackgroundPanelOrganizer mBackgroundPanelOrganizer;
@@ -288,7 +291,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
mTimeoutObserver = getObserver(this::onTimeoutSettingChanged);
mTaskChangeExitObserver = getObserver(this::onTaskChangeExitSettingChanged);
mSwipeToNotificationEnabledObserver =
getObserver(this::onSwipeToNotificationEnabledSettingChanged);
getObserver(this::onSwipeToNotificationEnabledChanged);
mDisplayController.addDisplayChangingController(mRotationController);
setupCallback();
@@ -358,14 +361,23 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
Slog.d(TAG, "Temporary lock disabled");
return;
}
if (!mDisplayAreaOrganizer.isReady()) {
// Must wait until DisplayAreaOrganizer is ready for transitioning.
mMainExecutor.executeDelayed(this::startOneHanded, DISPLAY_AREA_READY_RETRY_MS);
return;
}
if (mState.isTransitioning() || mState.isInOneHanded()) {
return;
}
final int currentRotation = mDisplayAreaOrganizer.getDisplayLayout().rotation();
if (currentRotation != Surface.ROTATION_0 && currentRotation != Surface.ROTATION_180) {
Slog.w(TAG, "One handed mode only support portrait mode");
return;
}
mState.setState(STATE_ENTERING);
final int yOffSet = Math.round(
mDisplayAreaOrganizer.getDisplayLayout().height() * mOffSetFraction);
@@ -394,6 +406,10 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
mOneHandedUiEventLogger.writeEvent(uiEvent);
}
void registerEventCallback(OneHandedEventCallback callback) {
mEventCallback = callback;
}
@VisibleForTesting
void registerTransitionCallback(OneHandedTransitionCallback callback) {
mDisplayAreaOrganizer.registerTransitionCallback(callback);
@@ -463,9 +479,30 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
};
}
@VisibleForTesting
void notifyExpandNotification() {
mMainExecutor.execute(() -> mEventCallback.notifyExpandNotification());
}
@VisibleForTesting
void notifyUserConfigChanged(boolean success) {
if (!success) {
return;
}
// TODO Check UX if popup Toast to notify user when auto-enabled one-handed is good option.
Toast.makeText(mContext, R.string.one_handed_tutorial_title, Toast.LENGTH_LONG).show();
}
@VisibleForTesting
void onActivatedActionChanged() {
if (mState.isTransitioning() || !isOneHandedEnabled()) {
if (!isOneHandedEnabled()) {
final boolean success = mOneHandedSettingsUtil.setOneHandedModeEnabled(
mContext.getContentResolver(), 1 /* Enabled for shortcut */, mUserId);
notifyUserConfigChanged(success);
}
if (isSwipeToNotificationEnabled()) {
notifyExpandNotification();
return;
}
@@ -494,11 +531,9 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
setOneHandedEnabled(enabled);
// Also checks swipe to notification settings since they all need gesture overlay.
// Enabled overlay package may affect the current animation(e.g:Settings switch),
// so we delay 250ms to enabled overlay after switch animation finish
mMainExecutor.executeDelayed(() -> setEnabledGesturalOverlay(
setEnabledGesturalOverlay(
enabled || mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
mContext.getContentResolver(), mUserId)), OVERLAY_ENABLED_DELAY_MS);
mContext.getContentResolver(), mUserId), true /* DelayExecute */);
}
@VisibleForTesting
@@ -542,7 +577,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
}
@VisibleForTesting
void onSwipeToNotificationEnabledSettingChanged() {
void onSwipeToNotificationEnabledChanged() {
final boolean enabled =
mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
mContext.getContentResolver(), mUserId);
@@ -551,7 +586,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
// Also checks one handed mode settings since they all need gesture overlay.
setEnabledGesturalOverlay(
enabled || mOneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
mContext.getContentResolver(), mUserId));
mContext.getContentResolver(), mUserId), true /* DelayExecute */);
}
private void setupTimeoutListener() {
@@ -569,11 +604,19 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
return mIsOneHandedEnabled;
}
@VisibleForTesting
boolean isSwipeToNotificationEnabled() {
return mIsSwipeToNotificationEnabled;
}
private void updateOneHandedEnabled() {
if (mState.getState() == STATE_ENTERING || mState.getState() == STATE_ACTIVE) {
mMainExecutor.execute(() -> stopOneHanded());
}
// Reset and align shortcut one_handed_mode_activated status with current mState
notifyShortcutState(mState.getState());
mTouchHandler.onOneHandedEnabled(mIsOneHandedEnabled);
if (!mIsOneHandedEnabled) {
@@ -608,12 +651,19 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
if (info != null && !info.isEnabled()) {
// Enable the default gestural one handed overlay.
setEnabledGesturalOverlay(true);
setEnabledGesturalOverlay(true /* enabled */, false /* delayExecute */);
}
}
@VisibleForTesting
private void setEnabledGesturalOverlay(boolean enabled) {
private void setEnabledGesturalOverlay(boolean enabled, boolean delayExecute) {
if (mState.isTransitioning() || delayExecute) {
// Enabled overlay package may affect the current animation(e.g:Settings switch),
// so we delay 250ms to enabled overlay after switch animation finish, only delay once.
mMainExecutor.executeDelayed(() -> setEnabledGesturalOverlay(enabled, false),
OVERLAY_ENABLED_DELAY_MS);
return;
}
try {
mOverlayManager.setEnabled(ONE_HANDED_MODE_GESTURAL_OVERLAY, enabled, USER_CURRENT);
} catch (RemoteException e) {
@@ -628,6 +678,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
if (enabled == isFeatureEnabled) {
return;
}
mLockedDisabled = locked && !enabled;
}
@@ -760,6 +811,13 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
});
}
@Override
public void registerEventCallback(OneHandedEventCallback callback) {
mMainExecutor.execute(() -> {
OneHandedController.this.registerEventCallback(callback);
});
}
@Override
public void registerTransitionCallback(OneHandedTransitionCallback callback) {
mMainExecutor.execute(() -> {

View File

@@ -61,11 +61,12 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer {
private DisplayLayout mDisplayLayout = new DisplayLayout();
private float mLastVisualOffset = 0;
private final Rect mLastVisualDisplayBounds = new Rect();
private final Rect mDefaultDisplayBounds = new Rect();
private final OneHandedSettingsUtil mOneHandedSettingsUtil;
private boolean mIsReady;
private float mLastVisualOffset = 0;
private int mEnterExitAnimationDurationMs;
private ArrayMap<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap = new ArrayMap();
@@ -157,6 +158,7 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer {
final DisplayAreaAppearedInfo info = displayAreaInfos.get(i);
onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash());
}
mIsReady = true;
updateDisplayBounds();
return displayAreaInfos;
}
@@ -164,9 +166,14 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer {
@Override
public void unregisterOrganizer() {
super.unregisterOrganizer();
mIsReady = false;
resetWindowsOffset();
}
boolean isReady() {
return mIsReady;
}
/**
* Handler for display rotation changes by {@link DisplayLayout}
*
@@ -312,6 +319,8 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer {
pw.println(mDisplayAreaTokenMap);
pw.print(innerPrefix + "mDefaultDisplayBounds=");
pw.println(mDefaultDisplayBounds);
pw.print(innerPrefix + "mIsReady=");
pw.println(mIsReady);
pw.print(innerPrefix + "mLastVisualDisplayBounds=");
pw.println(mLastVisualDisplayBounds);
pw.print(innerPrefix + "mLastVisualOffset=");

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2021 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;
/**
* Additional callback interface for OneHanded events.
*/
public interface OneHandedEventCallback {
/**
* Called to notify expand notification shade.
*/
default void notifyExpandNotification() {
}
}

View File

@@ -104,6 +104,17 @@ public final class OneHandedSettingsUtil {
Settings.Secure.ONE_HANDED_MODE_ENABLED, 0 /* Disabled */, userId) == 1;
}
/**
* Sets one handed enable or disable flag from Settings provider.
*
* @return true if the value was set, false on database errors
*/
public boolean setOneHandedModeEnabled(ContentResolver resolver, int enabled, int userId) {
return Settings.Secure.putIntForUser(resolver,
Settings.Secure.ONE_HANDED_MODE_ENABLED, enabled, userId);
}
/**
* Queries taps app to exit config from Settings provider.
*

View File

@@ -74,6 +74,8 @@ public class OneHandedControllerTest extends OneHandedTestCase {
@Mock
OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer;
@Mock
OneHandedEventCallback mMockEventCallback;
@Mock
OneHandedTouchHandler mMockTouchHandler;
@Mock
OneHandedTutorialHandler mMockTutorialHandler;
@@ -106,6 +108,7 @@ public class OneHandedControllerTest extends OneHandedTestCase {
when(mMockDisplayController.getDisplay(anyInt())).thenReturn(mDisplay);
when(mMockDisplayAreaOrganizer.getDisplayAreaTokenMap()).thenReturn(new ArrayMap<>());
when(mMockDisplayAreaOrganizer.isReady()).thenReturn(true);
when(mMockBackgroundOrganizer.getBackgroundSurface()).thenReturn(mMockLeash);
when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn(
mDefaultEnabled);
@@ -241,7 +244,7 @@ public class OneHandedControllerTest extends OneHandedTestCase {
@Test
public void testSettingsObserverUpdateSwipeToNotification() {
mSpiedOneHandedController.onSwipeToNotificationEnabledSettingChanged();
mSpiedOneHandedController.onSwipeToNotificationEnabledChanged();
verify(mSpiedOneHandedController).setSwipeToNotificationEnabled(anyBoolean());
}
@@ -311,6 +314,7 @@ public class OneHandedControllerTest extends OneHandedTestCase {
final DisplayLayout testDisplayLayout = new DisplayLayout(mDisplayLayout);
testDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_180);
mSpiedTransitionState.setState(STATE_NONE);
when(mMockDisplayAreaOrganizer.isReady()).thenReturn(true);
when(mMockDisplayAreaOrganizer.getDisplayLayout()).thenReturn(testDisplayLayout);
mSpiedOneHandedController.setOneHandedEnabled(true);
mSpiedOneHandedController.setLockedDisabled(false /* locked */, false /* enabled */);
@@ -372,8 +376,7 @@ public class OneHandedControllerTest extends OneHandedTestCase {
when(mMockSettingsUitl.getOneHandedModeActivated(any(), anyInt())).thenReturn(true);
mSpiedOneHandedController.onActivatedActionChanged();
verify(mSpiedOneHandedController, never()).startOneHanded();
verify(mSpiedOneHandedController, never()).stopOneHanded();
verify(mSpiedTransitionState, never()).setState(STATE_EXITING);
}
@Test
@@ -383,20 +386,20 @@ public class OneHandedControllerTest extends OneHandedTestCase {
when(mMockSettingsUitl.getOneHandedModeActivated(any(), anyInt())).thenReturn(true);
mSpiedOneHandedController.onActivatedActionChanged();
verify(mSpiedOneHandedController, never()).startOneHanded();
verify(mSpiedOneHandedController, never()).stopOneHanded();
verify(mSpiedTransitionState, never()).setState(STATE_ENTERING);
}
@Test
public void testOneHandedDisabled_shortcutEnabled_skipActions() {
public void testOneHandedDisabled_shortcutTrigger_thenAutoEnabled() {
when(mSpiedOneHandedController.isOneHandedEnabled()).thenReturn(false);
when(mSpiedTransitionState.getState()).thenReturn(STATE_NONE);
when(mSpiedTransitionState.isTransitioning()).thenReturn(false);
when(mMockSettingsUitl.getOneHandedModeActivated(any(), anyInt())).thenReturn(true);
when(mMockSettingsUitl.getOneHandedModeActivated(any(), anyInt())).thenReturn(false);
when(mMockSettingsUitl.setOneHandedModeEnabled(any(), anyInt(), anyInt())).thenReturn(
false);
mSpiedOneHandedController.onActivatedActionChanged();
verify(mSpiedOneHandedController, never()).startOneHanded();
verify(mSpiedOneHandedController, never()).stopOneHanded();
verify(mSpiedOneHandedController).notifyUserConfigChanged(anyBoolean());
}
@Test
@@ -408,4 +411,28 @@ public class OneHandedControllerTest extends OneHandedTestCase {
verify(mSpiedTransitionState).addSListeners(mMockTutorialHandler);
}
@Test
public void testNotifyEventCallbackWithMainExecutor() {
when(mSpiedOneHandedController.isOneHandedEnabled()).thenReturn(true);
when(mSpiedTransitionState.getState()).thenReturn(STATE_NONE);
when(mSpiedTransitionState.isTransitioning()).thenReturn(false);
when(mSpiedOneHandedController.isSwipeToNotificationEnabled()).thenReturn(true);
mSpiedOneHandedController.registerEventCallback(mMockEventCallback);
mSpiedOneHandedController.onActivatedActionChanged();
verify(mMockShellMainExecutor).execute(any());
}
@Test
public void testNotifyShortcutState_whenUpdateOneHandedEnabled() {
when(mSpiedOneHandedController.isOneHandedEnabled()).thenReturn(false);
when(mSpiedTransitionState.getState()).thenReturn(STATE_NONE);
when(mSpiedTransitionState.isTransitioning()).thenReturn(false);
when(mSpiedOneHandedController.isSwipeToNotificationEnabled()).thenReturn(true);
mSpiedOneHandedController.registerEventCallback(mMockEventCallback);
mSpiedOneHandedController.setOneHandedEnabled(true);
verify(mSpiedOneHandedController).notifyShortcutState(anyInt());
}
}

View File

@@ -418,4 +418,18 @@ public class OneHandedDisplayAreaOrganizerTest extends OneHandedTestCase {
verify(mSpiedDisplayAreaOrganizer, never()).resetWindowsOffset();
}
@Test
public void testDisplayArea_notReadyForTransition() {
OneHandedDisplayAreaOrganizer testSpiedDisplayAreaOrganizer = spy(
new OneHandedDisplayAreaOrganizer(mContext,
mDisplayLayout,
mMockSettingsUitl,
mMockAnimationController,
mTutorialHandler,
mMockBackgroundOrganizer,
mMockShellMainExecutor));
assertThat(testSpiedDisplayAreaOrganizer.isReady()).isFalse();
}
}

View File

@@ -34,6 +34,7 @@ import android.graphics.drawable.Drawable;
import android.inputmethodservice.InputMethodService;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.view.KeyEvent;
import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.KeyguardUpdateMonitor;
@@ -57,6 +58,7 @@ import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
import com.android.wm.shell.nano.WmShellTraceProto;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedEventCallback;
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
import com.android.wm.shell.onehanded.OneHandedUiEventLogger;
import com.android.wm.shell.pip.Pip;
@@ -253,6 +255,15 @@ public final class WMShell extends SystemUI
}
});
oneHanded.registerEventCallback(new OneHandedEventCallback() {
@Override
public void notifyExpandNotification() {
mSysUiMainExecutor.execute(
() -> mCommandQueue.handleSystemKey(
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN));
}
});
mOneHandedKeyguardCallback = new KeyguardUpdateMonitorCallback() {
@Override
public void onKeyguardBouncerChanged(boolean bouncer) {

View File

@@ -37,6 +37,7 @@ import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedEventCallback;
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
import com.android.wm.shell.pip.Pip;
@@ -106,6 +107,7 @@ public class WMShellTest extends SysuiTestCase {
verify(mCommandQueue).addCallback(any(CommandQueue.Callbacks.class));
verify(mScreenLifecycle).addObserver(any(ScreenLifecycle.Observer.class));
verify(mOneHanded).registerTransitionCallback(any(OneHandedTransitionCallback.class));
verify(mOneHanded).registerEventCallback(any(OneHandedEventCallback.class));
}
@Test