diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 4c2863e4f5947..8cea869aea349 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -40,7 +40,7 @@ 250 - 800 + 600 40% diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java index 8dc05de9bb8f9..a525c2c0219ce 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java @@ -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 diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java index c275d50a5d56e..b43daa0da2c09 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -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 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 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 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 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 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 }; } + @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 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 } @VisibleForTesting - void onSwipeToNotificationEnabledSettingChanged() { + void onSwipeToNotificationEnabledChanged() { final boolean enabled = mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( mContext.getContentResolver(), mUserId); @@ -551,7 +586,7 @@ public class OneHandedController implements RemoteCallable // 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 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 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 if (enabled == isFeatureEnabled) { return; } + mLockedDisabled = locked && !enabled; } @@ -760,6 +811,13 @@ public class OneHandedController implements RemoteCallable }); } + @Override + public void registerEventCallback(OneHandedEventCallback callback) { + mMainExecutor.execute(() -> { + OneHandedController.this.registerEventCallback(callback); + }); + } + @Override public void registerTransitionCallback(OneHandedTransitionCallback callback) { mMainExecutor.execute(() -> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java index b8da37fd0c25d..d749c320bf948 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java @@ -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 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="); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedEventCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedEventCallback.java new file mode 100644 index 0000000000000..d07eea271eacd --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedEventCallback.java @@ -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() { + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java index 90fc823fb574d..da53b359a304a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java @@ -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. * diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java index 1852279ee96c8..47789b7490ee2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java @@ -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()); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java index a27ed114de708..ef16fd391235c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java @@ -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(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 92ef8504d1232..3af82f91af1cb 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -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) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java index 1dd0b21bda30f..ff15d0151ea44 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java @@ -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