Merge "Add magnification edge haptic feature." into main
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index 8e7d277..f3a540b 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -40,6 +40,7 @@
 import com.android.server.LocalServices;
 import com.android.server.accessibility.gestures.TouchExplorer;
 import com.android.server.accessibility.magnification.FullScreenMagnificationGestureHandler;
+import com.android.server.accessibility.magnification.FullScreenMagnificationVibrationHelper;
 import com.android.server.accessibility.magnification.MagnificationGestureHandler;
 import com.android.server.accessibility.magnification.WindowMagnificationGestureHandler;
 import com.android.server.accessibility.magnification.WindowMagnificationPromptController;
@@ -654,11 +655,14 @@
         } else {
             final Context uiContext = displayContext.createWindowContext(
                     TYPE_MAGNIFICATION_OVERLAY, null /* options */);
+            FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper =
+                    new FullScreenMagnificationVibrationHelper(uiContext);
             magnificationGestureHandler = new FullScreenMagnificationGestureHandler(uiContext,
                     mAms.getMagnificationController().getFullScreenMagnificationController(),
                     mAms.getTraceManager(),
                     mAms.getMagnificationController(), detectControlGestures, triggerable,
-                    new WindowMagnificationPromptController(displayContext, mUserId), displayId);
+                    new WindowMagnificationPromptController(displayContext, mUserId), displayId,
+                    fullScreenMagnificationVibrationHelper);
         }
         return magnificationGestureHandler;
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
index fd8babb..58b61b3 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
@@ -155,7 +155,8 @@
             boolean detectTripleTap,
             boolean detectShortcutTrigger,
             @NonNull WindowMagnificationPromptController promptController,
-            int displayId) {
+            int displayId,
+            FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper) {
         super(displayId, detectTripleTap, detectShortcutTrigger, trace, callback);
         if (DEBUG_ALL) {
             Log.i(mLogTag,
@@ -203,7 +204,8 @@
         mDetectingState = new DetectingState(context);
         mViewportDraggingState = new ViewportDraggingState();
         mPanningScalingState = new PanningScalingState(context);
-        mSinglePanningState = new SinglePanningState(context);
+        mSinglePanningState = new SinglePanningState(context,
+                fullScreenMagnificationVibrationHelper);
         setSinglePanningEnabled(
                 context.getResources()
                         .getBoolean(R.bool.config_enable_a11y_magnification_single_panning));
@@ -1334,11 +1336,17 @@
     }
 
     final class SinglePanningState extends SimpleOnGestureListener implements State {
+
+
         private final GestureDetector mScrollGestureDetector;
         private MotionEventInfo mEvent;
+        private final FullScreenMagnificationVibrationHelper
+                mFullScreenMagnificationVibrationHelper;
 
-        SinglePanningState(Context context) {
+        SinglePanningState(Context context, FullScreenMagnificationVibrationHelper
+                fullScreenMagnificationVibrationHelper) {
             mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
+            mFullScreenMagnificationVibrationHelper = fullScreenMagnificationVibrationHelper;
         }
 
         @Override
@@ -1378,10 +1386,20 @@
             if (mFullScreenMagnificationController.isAtEdge(mDisplayId)) {
                 clear();
                 transitionTo(mDelegatingState);
+                vibrateIfNeeded();
             }
             return /* event consumed: */ true;
         }
 
+        private void vibrateIfNeeded() {
+            if ((mFullScreenMagnificationController.isAtLeftEdge(mDisplayId)
+                    || mFullScreenMagnificationController.isAtRightEdge(mDisplayId))) {
+                mFullScreenMagnificationVibrationHelper.vibrateIfSettingEnabled();
+            }
+        }
+
+
+
         @Override
         public String toString() {
             return "SinglePanningState{"
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationVibrationHelper.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationVibrationHelper.java
new file mode 100644
index 0000000..37a2eb5
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationVibrationHelper.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 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.server.accessibility.magnification;
+
+import android.annotation.Nullable;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.UserHandle;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Class to encapsulate all the logic to fire a vibration when user reaches the screen's left or
+ * right edge, when it's in magnification mode.
+ */
+public class FullScreenMagnificationVibrationHelper {
+    private static final long VIBRATION_DURATION_MS = 10L;
+    private static final int VIBRATION_AMPLITUDE = VibrationEffect.MAX_AMPLITUDE / 2;
+
+    @Nullable
+    private final Vibrator mVibrator;
+    private final ContentResolver mContentResolver;
+    private final VibrationEffect mVibrationEffect = VibrationEffect.get(
+            VibrationEffect.EFFECT_CLICK);
+    @VisibleForTesting
+    VibrationEffectSupportedProvider mIsVibrationEffectSupportedProvider;
+
+    public FullScreenMagnificationVibrationHelper(Context context) {
+        mContentResolver = context.getContentResolver();
+        mVibrator = context.getSystemService(Vibrator.class);
+        mIsVibrationEffectSupportedProvider =
+                () -> mVibrator != null && mVibrator.areAllEffectsSupported(
+                        VibrationEffect.EFFECT_CLICK) == Vibrator.VIBRATION_EFFECT_SUPPORT_YES;
+    }
+
+
+    void vibrateIfSettingEnabled() {
+        if (mVibrator != null && mVibrator.hasVibrator() && isEdgeHapticSettingEnabled()) {
+            if (mIsVibrationEffectSupportedProvider.isVibrationEffectSupported()) {
+                mVibrator.vibrate(mVibrationEffect);
+            } else {
+                mVibrator.vibrate(VibrationEffect.createOneShot(VIBRATION_DURATION_MS,
+                        VIBRATION_AMPLITUDE));
+            }
+        }
+    }
+
+    private boolean isEdgeHapticSettingEnabled() {
+        return Settings.Secure.getIntForUser(
+                mContentResolver,
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_EDGE_HAPTIC_ENABLED,
+                0, UserHandle.USER_CURRENT)
+                == 1;
+    }
+
+    @VisibleForTesting
+    interface VibrationEffectSupportedProvider {
+        boolean isVibrationEffectSupported();
+    }
+}
+
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
index 68bb5a4..4d3bd92 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
@@ -36,6 +36,7 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -165,6 +166,8 @@
     WindowMagnificationPromptController mWindowMagnificationPromptController;
     @Mock
     AccessibilityTraceManager mMockTraceManager;
+    @Mock
+    FullScreenMagnificationVibrationHelper mMockFullScreenMagnificationVibrationHelper;
 
     @Rule
     public final TestableContext mContext = new TestableContext(getInstrumentation().getContext());
@@ -247,7 +250,8 @@
         FullScreenMagnificationGestureHandler h = new FullScreenMagnificationGestureHandler(
                 mContext, mFullScreenMagnificationController, mMockTraceManager, mMockCallback,
                 detectTripleTap, detectShortcutTrigger,
-                mWindowMagnificationPromptController, DISPLAY_0);
+                mWindowMagnificationPromptController, DISPLAY_0,
+                mMockFullScreenMagnificationVibrationHelper);
         h.setSinglePanningEnabled(true);
         mHandler = new TestHandler(h.mDetectingState, mClock) {
             @Override
@@ -634,6 +638,52 @@
     }
 
     @Test
+    public void testScroll_singleHorizontalPanningAndAtEdge_vibrate() {
+        goFromStateIdleTo(STATE_SINGLE_PANNING);
+        mFullScreenMagnificationController.setCenter(
+                DISPLAY_0,
+                INITIAL_MAGNIFICATION_BOUNDS.left,
+                INITIAL_MAGNIFICATION_BOUNDS.top / 2,
+                false,
+                1);
+        final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1;
+        PointF initCoords =
+                new PointF(
+                        mFullScreenMagnificationController.getCenterX(DISPLAY_0),
+                        mFullScreenMagnificationController.getCenterY(DISPLAY_0));
+        PointF endCoords = new PointF(initCoords.x, initCoords.y);
+        endCoords.offset(swipeMinDistance, 0);
+        allowEventDelegation();
+
+        swipeAndHold(initCoords, endCoords);
+
+        verify(mMockFullScreenMagnificationVibrationHelper).vibrateIfSettingEnabled();
+    }
+
+    @Test
+    public void testScroll_singleVerticalPanningAndAtEdge_doNotVibrate() {
+        goFromStateIdleTo(STATE_SINGLE_PANNING);
+        mFullScreenMagnificationController.setCenter(
+                DISPLAY_0,
+                INITIAL_MAGNIFICATION_BOUNDS.left,
+                INITIAL_MAGNIFICATION_BOUNDS.top,
+                false,
+                1);
+        final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1;
+        PointF initCoords =
+                new PointF(
+                        mFullScreenMagnificationController.getCenterX(DISPLAY_0),
+                        mFullScreenMagnificationController.getCenterY(DISPLAY_0));
+        PointF endCoords = new PointF(initCoords.x, initCoords.y);
+        endCoords.offset(0, swipeMinDistance);
+        allowEventDelegation();
+
+        swipeAndHold(initCoords, endCoords);
+
+        verify(mMockFullScreenMagnificationVibrationHelper, never()).vibrateIfSettingEnabled();
+    }
+
+    @Test
     public void testShortcutTriggered_invokeShowWindowPromptAction() {
         goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED);
 
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationVibrationHelperTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationVibrationHelperTest.java
new file mode 100644
index 0000000..8563881
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationVibrationHelperTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 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.server.accessibility.magnification;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.testing.TestableContext;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for {@link FullScreenMagnificationVibrationHelper}.
+ */
+public class FullScreenMagnificationVibrationHelperTest {
+    private static final long VIBRATION_DURATION_MS = 10L;
+    private static final int VIBRATION_AMPLITUDE = VibrationEffect.MAX_AMPLITUDE / 2;
+
+
+    @Rule
+    public final TestableContext mContext = new TestableContext(getInstrumentation().getContext());
+    @Mock
+    Vibrator mMockVibrator;
+
+    private FullScreenMagnificationVibrationHelper mFullScreenMagnificationVibrationHelper;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext.addMockSystemService(Vibrator.class, mMockVibrator);
+        mFullScreenMagnificationVibrationHelper = new FullScreenMagnificationVibrationHelper(
+                mContext);
+        mFullScreenMagnificationVibrationHelper.mIsVibrationEffectSupportedProvider = () -> true;
+    }
+
+    @Test
+    public void edgeHapticSettingEnabled_vibrate() {
+        setEdgeHapticSettingEnabled(true);
+        when(mMockVibrator.hasVibrator()).thenReturn(true);
+
+        mFullScreenMagnificationVibrationHelper.vibrateIfSettingEnabled();
+
+        verify(mMockVibrator).vibrate(any());
+    }
+
+    @Test
+    public void edgeHapticSettingDisabled_doNotVibrate() {
+        setEdgeHapticSettingEnabled(false);
+        when(mMockVibrator.hasVibrator()).thenReturn(true);
+
+        mFullScreenMagnificationVibrationHelper.vibrateIfSettingEnabled();
+
+        verify(mMockVibrator, never()).vibrate(any());
+    }
+
+    @Test
+    public void hasNoVibrator_doNotVibrate() {
+        setEdgeHapticSettingEnabled(true);
+        when(mMockVibrator.hasVibrator()).thenReturn(false);
+
+        mFullScreenMagnificationVibrationHelper.vibrateIfSettingEnabled();
+
+        verify(mMockVibrator, never()).vibrate(any());
+    }
+
+    @Test
+    public void notSupportVibrationEffect_vibrateOneShotEffect() {
+        setEdgeHapticSettingEnabled(true);
+        when(mMockVibrator.hasVibrator()).thenReturn(true);
+        mFullScreenMagnificationVibrationHelper.mIsVibrationEffectSupportedProvider = () -> false;
+
+        mFullScreenMagnificationVibrationHelper.vibrateIfSettingEnabled();
+
+        verify(mMockVibrator).vibrate(eq(VibrationEffect.createOneShot(VIBRATION_DURATION_MS,
+                VIBRATION_AMPLITUDE)));
+    }
+
+
+    private boolean setEdgeHapticSettingEnabled(boolean enabled) {
+        return Settings.Secure.putInt(
+                mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_EDGE_HAPTIC_ENABLED,
+                enabled ? 1 : 0);
+    }
+}