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)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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{"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user