Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1d19a4e4 authored by Nigel Wang's avatar Nigel Wang
Browse files

Add magnification edge haptic feature.

This change add logic to fire a haptic when user panning the screen
horizontally and reaches the left and right of the screen.

1. Added FullScreenMagnificationVibrationHelper to hold all the logic to fire the haptic.
2. Added VibrationEffectSupportedProvider in FullScreenMagnificationVibrationHelper. Because VibrationEffectSupportedProvider is calling some final function to determine if the device is able to play vibration effect or not. But those final methods cannot be mocked. So adding VibrationEffectSupportedProvider, so we change inject different implementation.
3. Added the new tests for FullScreenMagnificationVibrationHelper and FullScreenMagnificationGestureHandler

Test Video: https://drive.google.com/file/d/1Gd28dfzaws-67jNKWvtiiL-tPJudayIX/view?usp=sharing

Bug: 280315691
Defer-CP-To-Master: 279498927
Test: atest FullScreenMagnificationGestureHandlerTest
Change-Id: I0a07909bf6ea88042ab05aad7e8b792cc33f9a3b
Defer-CP-To-Maste:283170514
(cherry picked from commit 4fc0416f5d822a470d413781b8226f78e2033653)
parent 46d2d95b
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import android.view.accessibility.AccessibilityEvent;
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 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo
        } 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;
    }
+21 −3
Original line number Diff line number Diff line
@@ -155,7 +155,8 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            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 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
        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 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
    }

    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 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            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{"
+78 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
+51 −1
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import static org.mockito.Matchers.anyInt;
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 @@ public class FullScreenMagnificationGestureHandlerTest {
    WindowMagnificationPromptController mWindowMagnificationPromptController;
    @Mock
    AccessibilityTraceManager mMockTraceManager;
    @Mock
    FullScreenMagnificationVibrationHelper mMockFullScreenMagnificationVibrationHelper;

    @Rule
    public final TestableContext mContext = new TestableContext(getInstrumentation().getContext());
@@ -247,7 +250,8 @@ public class FullScreenMagnificationGestureHandlerTest {
        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
@@ -633,6 +637,52 @@ public class FullScreenMagnificationGestureHandlerTest {
        assertTrue(isZoomed());
    }

    @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);
+111 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}