Merge "Add magnification edge haptic feature." into main
This commit is contained in:
committed by
Android (Google) Code Review
commit
080276b44c
@@ -40,6 +40,7 @@ import android.view.accessibility.AccessibilityEvent;
|
|||||||
import com.android.server.LocalServices;
|
import com.android.server.LocalServices;
|
||||||
import com.android.server.accessibility.gestures.TouchExplorer;
|
import com.android.server.accessibility.gestures.TouchExplorer;
|
||||||
import com.android.server.accessibility.magnification.FullScreenMagnificationGestureHandler;
|
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.MagnificationGestureHandler;
|
||||||
import com.android.server.accessibility.magnification.WindowMagnificationGestureHandler;
|
import com.android.server.accessibility.magnification.WindowMagnificationGestureHandler;
|
||||||
import com.android.server.accessibility.magnification.WindowMagnificationPromptController;
|
import com.android.server.accessibility.magnification.WindowMagnificationPromptController;
|
||||||
@@ -654,11 +655,14 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo
|
|||||||
} else {
|
} else {
|
||||||
final Context uiContext = displayContext.createWindowContext(
|
final Context uiContext = displayContext.createWindowContext(
|
||||||
TYPE_MAGNIFICATION_OVERLAY, null /* options */);
|
TYPE_MAGNIFICATION_OVERLAY, null /* options */);
|
||||||
|
FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper =
|
||||||
|
new FullScreenMagnificationVibrationHelper(uiContext);
|
||||||
magnificationGestureHandler = new FullScreenMagnificationGestureHandler(uiContext,
|
magnificationGestureHandler = new FullScreenMagnificationGestureHandler(uiContext,
|
||||||
mAms.getMagnificationController().getFullScreenMagnificationController(),
|
mAms.getMagnificationController().getFullScreenMagnificationController(),
|
||||||
mAms.getTraceManager(),
|
mAms.getTraceManager(),
|
||||||
mAms.getMagnificationController(), detectControlGestures, triggerable,
|
mAms.getMagnificationController(), detectControlGestures, triggerable,
|
||||||
new WindowMagnificationPromptController(displayContext, mUserId), displayId);
|
new WindowMagnificationPromptController(displayContext, mUserId), displayId,
|
||||||
|
fullScreenMagnificationVibrationHelper);
|
||||||
}
|
}
|
||||||
return magnificationGestureHandler;
|
return magnificationGestureHandler;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,7 +155,8 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
|
|||||||
boolean detectTripleTap,
|
boolean detectTripleTap,
|
||||||
boolean detectShortcutTrigger,
|
boolean detectShortcutTrigger,
|
||||||
@NonNull WindowMagnificationPromptController promptController,
|
@NonNull WindowMagnificationPromptController promptController,
|
||||||
int displayId) {
|
int displayId,
|
||||||
|
FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper) {
|
||||||
super(displayId, detectTripleTap, detectShortcutTrigger, trace, callback);
|
super(displayId, detectTripleTap, detectShortcutTrigger, trace, callback);
|
||||||
if (DEBUG_ALL) {
|
if (DEBUG_ALL) {
|
||||||
Log.i(mLogTag,
|
Log.i(mLogTag,
|
||||||
@@ -203,7 +204,8 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
|
|||||||
mDetectingState = new DetectingState(context);
|
mDetectingState = new DetectingState(context);
|
||||||
mViewportDraggingState = new ViewportDraggingState();
|
mViewportDraggingState = new ViewportDraggingState();
|
||||||
mPanningScalingState = new PanningScalingState(context);
|
mPanningScalingState = new PanningScalingState(context);
|
||||||
mSinglePanningState = new SinglePanningState(context);
|
mSinglePanningState = new SinglePanningState(context,
|
||||||
|
fullScreenMagnificationVibrationHelper);
|
||||||
setSinglePanningEnabled(
|
setSinglePanningEnabled(
|
||||||
context.getResources()
|
context.getResources()
|
||||||
.getBoolean(R.bool.config_enable_a11y_magnification_single_panning));
|
.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 {
|
final class SinglePanningState extends SimpleOnGestureListener implements State {
|
||||||
|
|
||||||
|
|
||||||
private final GestureDetector mScrollGestureDetector;
|
private final GestureDetector mScrollGestureDetector;
|
||||||
private MotionEventInfo mEvent;
|
private MotionEventInfo mEvent;
|
||||||
|
private final FullScreenMagnificationVibrationHelper
|
||||||
|
mFullScreenMagnificationVibrationHelper;
|
||||||
|
|
||||||
SinglePanningState(Context context) {
|
SinglePanningState(Context context, FullScreenMagnificationVibrationHelper
|
||||||
|
fullScreenMagnificationVibrationHelper) {
|
||||||
mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
|
mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
|
||||||
|
mFullScreenMagnificationVibrationHelper = fullScreenMagnificationVibrationHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1378,10 +1386,20 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
|
|||||||
if (mFullScreenMagnificationController.isAtEdge(mDisplayId)) {
|
if (mFullScreenMagnificationController.isAtEdge(mDisplayId)) {
|
||||||
clear();
|
clear();
|
||||||
transitionTo(mDelegatingState);
|
transitionTo(mDelegatingState);
|
||||||
|
vibrateIfNeeded();
|
||||||
}
|
}
|
||||||
return /* event consumed: */ true;
|
return /* event consumed: */ true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void vibrateIfNeeded() {
|
||||||
|
if ((mFullScreenMagnificationController.isAtLeftEdge(mDisplayId)
|
||||||
|
|| mFullScreenMagnificationController.isAtRightEdge(mDisplayId))) {
|
||||||
|
mFullScreenMagnificationVibrationHelper.vibrateIfSettingEnabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "SinglePanningState{"
|
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.doAnswer;
|
||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.doNothing;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -165,6 +166,8 @@ public class FullScreenMagnificationGestureHandlerTest {
|
|||||||
WindowMagnificationPromptController mWindowMagnificationPromptController;
|
WindowMagnificationPromptController mWindowMagnificationPromptController;
|
||||||
@Mock
|
@Mock
|
||||||
AccessibilityTraceManager mMockTraceManager;
|
AccessibilityTraceManager mMockTraceManager;
|
||||||
|
@Mock
|
||||||
|
FullScreenMagnificationVibrationHelper mMockFullScreenMagnificationVibrationHelper;
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public final TestableContext mContext = new TestableContext(getInstrumentation().getContext());
|
public final TestableContext mContext = new TestableContext(getInstrumentation().getContext());
|
||||||
@@ -247,7 +250,8 @@ public class FullScreenMagnificationGestureHandlerTest {
|
|||||||
FullScreenMagnificationGestureHandler h = new FullScreenMagnificationGestureHandler(
|
FullScreenMagnificationGestureHandler h = new FullScreenMagnificationGestureHandler(
|
||||||
mContext, mFullScreenMagnificationController, mMockTraceManager, mMockCallback,
|
mContext, mFullScreenMagnificationController, mMockTraceManager, mMockCallback,
|
||||||
detectTripleTap, detectShortcutTrigger,
|
detectTripleTap, detectShortcutTrigger,
|
||||||
mWindowMagnificationPromptController, DISPLAY_0);
|
mWindowMagnificationPromptController, DISPLAY_0,
|
||||||
|
mMockFullScreenMagnificationVibrationHelper);
|
||||||
h.setSinglePanningEnabled(true);
|
h.setSinglePanningEnabled(true);
|
||||||
mHandler = new TestHandler(h.mDetectingState, mClock) {
|
mHandler = new TestHandler(h.mDetectingState, mClock) {
|
||||||
@Override
|
@Override
|
||||||
@@ -633,6 +637,52 @@ public class FullScreenMagnificationGestureHandlerTest {
|
|||||||
assertTrue(isZoomed());
|
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
|
@Test
|
||||||
public void testShortcutTriggered_invokeShowWindowPromptAction() {
|
public void testShortcutTriggered_invokeShowWindowPromptAction() {
|
||||||
goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED);
|
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