1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.accessibility.magnification; 18 19 import static android.view.InputDevice.SOURCE_TOUCHSCREEN; 20 import static android.view.MotionEvent.ACTION_CANCEL; 21 import static android.view.MotionEvent.ACTION_UP; 22 23 import static java.util.Arrays.asList; 24 import static java.util.Arrays.copyOfRange; 25 26 import android.annotation.Nullable; 27 import android.annotation.UiContext; 28 import android.content.Context; 29 import android.graphics.Point; 30 import android.provider.Settings; 31 import android.util.MathUtils; 32 import android.util.Slog; 33 import android.view.Display; 34 import android.view.MotionEvent; 35 36 import com.android.internal.annotations.VisibleForTesting; 37 import com.android.server.accessibility.EventStreamTransformation; 38 import com.android.server.accessibility.gestures.MultiTap; 39 import com.android.server.accessibility.gestures.MultiTapAndHold; 40 41 import java.util.List; 42 43 /** 44 * This class handles window magnification in response to touch events and shortcut. 45 * 46 * The behavior is as follows: 47 * 48 * <ol> 49 * <li> 1. Toggle Window magnification by triple-tap gesture shortcut. It is triggered via 50 * {@link #onTripleTap(MotionEvent)}. 51 * <li> 2. Toggle Window magnification by tapping shortcut. It is triggered via 52 * {@link #notifyShortcutTriggered()}. 53 * <li> When the window magnifier is visible, pinching with any number of additional fingers 54 * would adjust the magnification scale .<strong>Note</strong> that this operation is valid only 55 * when at least one finger is in the window. 56 * <li> When the window magnifier is visible, to do scrolling to move the window magnifier, 57 * the user can use two or more fingers and at least one of them is inside the window. 58 * <br><strong>Note</strong> that the offset of this callback is opposed to moving direction. 59 * The operation becomes invalid after performing scaling operation until all fingers are 60 * lifted. 61 * </ol> 62 */ 63 @SuppressWarnings("WeakerAccess") 64 public class WindowMagnificationGestureHandler extends MagnificationGestureHandler { 65 66 private static final boolean DEBUG_STATE_TRANSITIONS = false | DEBUG_ALL; 67 private static final boolean DEBUG_DETECTING = false | DEBUG_ALL; 68 69 //Ensure the range has consistency with FullScreenMagnificationGestureHandler. 70 private static final float MIN_SCALE = 2.0f; 71 private static final float MAX_SCALE = WindowMagnificationManager.MAX_SCALE; 72 73 private final WindowMagnificationManager mWindowMagnificationMgr; 74 @VisibleForTesting 75 final DelegatingState mDelegatingState; 76 @VisibleForTesting 77 final DetectingState mDetectingState; 78 @VisibleForTesting 79 final PanningScalingGestureState mObservePanningScalingState; 80 81 @VisibleForTesting 82 State mCurrentState; 83 @VisibleForTesting 84 State mPreviousState; 85 86 private MotionEventDispatcherDelegate mMotionEventDispatcherDelegate; 87 private final Context mContext; 88 private final Point mTempPoint = new Point(); 89 WindowMagnificationGestureHandler(@iContext Context context, WindowMagnificationManager windowMagnificationMgr, Callback callback, boolean detectTripleTap, boolean detectShortcutTrigger, int displayId)90 public WindowMagnificationGestureHandler(@UiContext Context context, 91 WindowMagnificationManager windowMagnificationMgr, 92 Callback callback, 93 boolean detectTripleTap, boolean detectShortcutTrigger, int displayId) { 94 super(displayId, detectTripleTap, detectShortcutTrigger, callback); 95 if (DEBUG_ALL) { 96 Slog.i(mLogTag, 97 "WindowMagnificationGestureHandler() , displayId = " + displayId + ")"); 98 } 99 mContext = context; 100 mWindowMagnificationMgr = windowMagnificationMgr; 101 mMotionEventDispatcherDelegate = new MotionEventDispatcherDelegate(context, 102 (event, rawEvent, policyFlags) -> dispatchTransformedEvent(event, rawEvent, 103 policyFlags)); 104 mDelegatingState = new DelegatingState(mMotionEventDispatcherDelegate); 105 mDetectingState = new DetectingState(context, mDetectTripleTap); 106 mObservePanningScalingState = new PanningScalingGestureState( 107 new PanningScalingHandler(context, MAX_SCALE, MIN_SCALE, true, 108 new PanningScalingHandler.MagnificationDelegate() { 109 @Override 110 public boolean processScroll(int displayId, float distanceX, 111 float distanceY) { 112 return mWindowMagnificationMgr.processScroll(displayId, distanceX, 113 distanceY); 114 } 115 116 @Override 117 public void setScale(int displayId, float scale) { 118 mWindowMagnificationMgr.setScale(displayId, scale); 119 } 120 121 @Override 122 public float getScale(int displayId) { 123 return mWindowMagnificationMgr.getScale(displayId); 124 } 125 })); 126 127 transitionTo(mDetectingState); 128 } 129 130 @Override onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags)131 void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 132 // To keep InputEventConsistencyVerifiers within GestureDetectors happy. 133 mObservePanningScalingState.mPanningScalingHandler.onTouchEvent(event); 134 mCurrentState.onMotionEvent(event, rawEvent, policyFlags); 135 } 136 137 @Override clearEvents(int inputSource)138 public void clearEvents(int inputSource) { 139 if (inputSource == SOURCE_TOUCHSCREEN) { 140 resetToDetectState(); 141 } 142 super.clearEvents(inputSource); 143 } 144 145 @Override onDestroy()146 public void onDestroy() { 147 if (DEBUG_ALL) { 148 Slog.i(mLogTag, "onDestroy(); delayed = " 149 + mDetectingState.toString()); 150 } 151 mWindowMagnificationMgr.disableWindowMagnification(mDisplayId, true); 152 resetToDetectState(); 153 } 154 155 @Override handleShortcutTriggered()156 public void handleShortcutTriggered() { 157 final Point screenSize = mTempPoint; 158 getScreenSize(mTempPoint); 159 toggleMagnification(screenSize.x / 2.0f, screenSize.y / 2.0f); 160 } 161 getScreenSize(Point outSize)162 private void getScreenSize(Point outSize) { 163 final Display display = mContext.getDisplay(); 164 display.getRealSize(outSize); 165 } 166 167 @Override getMode()168 public int getMode() { 169 return Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; 170 } 171 enableWindowMagnifier(float centerX, float centerY)172 private void enableWindowMagnifier(float centerX, float centerY) { 173 if (DEBUG_ALL) { 174 Slog.i(mLogTag, "enableWindowMagnifier :" + centerX + ", " + centerY); 175 } 176 177 final float scale = MathUtils.constrain( 178 mWindowMagnificationMgr.getPersistedScale(), 179 MIN_SCALE, MAX_SCALE); 180 mWindowMagnificationMgr.enableWindowMagnification(mDisplayId, scale, centerX, centerY); 181 } 182 disableWindowMagnifier()183 private void disableWindowMagnifier() { 184 if (DEBUG_ALL) { 185 Slog.i(mLogTag, "disableWindowMagnifier()"); 186 } 187 mWindowMagnificationMgr.disableWindowMagnification(mDisplayId, false); 188 } 189 toggleMagnification(float centerX, float centerY)190 private void toggleMagnification(float centerX, float centerY) { 191 if (mWindowMagnificationMgr.isWindowMagnifierEnabled(mDisplayId)) { 192 disableWindowMagnifier(); 193 } else { 194 enableWindowMagnifier(centerX, centerY); 195 } 196 } 197 onTripleTap(MotionEvent up)198 private void onTripleTap(MotionEvent up) { 199 if (DEBUG_DETECTING) { 200 Slog.i(mLogTag, "onTripleTap()"); 201 } 202 toggleMagnification(up.getX(), up.getY()); 203 mCallback.onTripleTapped(mDisplayId, getMode()); 204 } 205 resetToDetectState()206 void resetToDetectState() { 207 transitionTo(mDetectingState); 208 } 209 210 /** 211 * An interface to intercept the {@link MotionEvent} for gesture detection. The intercepted 212 * events should be delivered to next {@link EventStreamTransformation} with { 213 * {@link EventStreamTransformation#onMotionEvent(MotionEvent, MotionEvent, int)}} if there is 214 * no valid gestures. 215 */ 216 interface State { onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)217 void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags); 218 clear()219 default void clear() { 220 } 221 onEnter()222 default void onEnter() { 223 } 224 onExit()225 default void onExit() { 226 } 227 name()228 default String name() { 229 return getClass().getSimpleName(); 230 } 231 nameOf(@ullable State s)232 static String nameOf(@Nullable State s) { 233 return s != null ? s.name() : "null"; 234 } 235 } 236 transitionTo(State state)237 private void transitionTo(State state) { 238 if (DEBUG_STATE_TRANSITIONS) { 239 Slog.i(mLogTag, "state transition: " + (State.nameOf(mCurrentState) + " -> " 240 + State.nameOf(state) + " at " 241 + asList(copyOfRange(new RuntimeException().getStackTrace(), 1, 5))) 242 .replace(getClass().getName(), "")); 243 } 244 mPreviousState = mCurrentState; 245 if (mPreviousState != null) { 246 mPreviousState.onExit(); 247 } 248 mCurrentState = state; 249 if (mCurrentState != null) { 250 mCurrentState.onEnter(); 251 } 252 } 253 254 /** 255 * When entering this state, {@link PanningScalingHandler} will be enabled to address the 256 * gestures until receiving {@link MotionEvent#ACTION_UP} or {@link MotionEvent#ACTION_CANCEL}. 257 * When leaving this state, current scale will be persisted. 258 */ 259 final class PanningScalingGestureState implements State { 260 private final PanningScalingHandler mPanningScalingHandler; 261 PanningScalingGestureState(PanningScalingHandler panningScalingHandler)262 PanningScalingGestureState(PanningScalingHandler panningScalingHandler) { 263 mPanningScalingHandler = panningScalingHandler; 264 } 265 266 @Override onEnter()267 public void onEnter() { 268 mPanningScalingHandler.setEnabled(true); 269 } 270 271 @Override onExit()272 public void onExit() { 273 mPanningScalingHandler.setEnabled(false); 274 mWindowMagnificationMgr.persistScale(mDisplayId); 275 clear(); 276 } 277 278 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)279 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 280 int action = event.getActionMasked(); 281 if (action == ACTION_UP || action == ACTION_CANCEL) { 282 transitionTo(mDetectingState); 283 } 284 } 285 286 @Override clear()287 public void clear() { 288 mPanningScalingHandler.clear(); 289 } 290 291 @Override toString()292 public String toString() { 293 return "PanningScalingState{" 294 + "mPanningScalingHandler =" + mPanningScalingHandler + '}'; 295 } 296 } 297 298 /** 299 * A state not to intercept {@link MotionEvent}. Leaving this state until receiving 300 * {@link MotionEvent#ACTION_UP} or {@link MotionEvent#ACTION_CANCEL}. 301 */ 302 final class DelegatingState implements State { 303 private final MotionEventDispatcherDelegate mMotionEventDispatcherDelegate; 304 DelegatingState(MotionEventDispatcherDelegate motionEventDispatcherDelegate)305 DelegatingState(MotionEventDispatcherDelegate motionEventDispatcherDelegate) { 306 mMotionEventDispatcherDelegate = motionEventDispatcherDelegate; 307 } 308 309 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)310 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 311 mMotionEventDispatcherDelegate.dispatchMotionEvent(event, rawEvent, policyFlags); 312 switch (event.getActionMasked()) { 313 case ACTION_UP: 314 case ACTION_CANCEL: { 315 transitionTo(mDetectingState); 316 } 317 break; 318 } 319 } 320 } 321 322 /** 323 * This class handles motion events in a duration to determine if the user is going to 324 * manipulate the window magnifier or want to interact with current UI. The rule of leaving 325 * this state is as follows: 326 * <ol> 327 * <li> If {@link MagnificationGestureMatcher#GESTURE_TWO_FINGERS_DOWN_OR_SWIPE} is detected, 328 * {@link State} will be transited to {@link PanningScalingGestureState}.</li> 329 * <li> If other gesture is detected and the last motion event is neither ACTION_UP nor 330 * ACTION_CANCEL. 331 * </ol> 332 * <b>Note</b> The motion events will be cached and dispatched before leaving this state. 333 */ 334 final class DetectingState implements State, 335 MagnificationGesturesObserver.Callback { 336 337 private final MagnificationGesturesObserver mGesturesObserver; 338 339 /** 340 * {@code true} if this detector should detect and respond to triple-tap 341 * gestures for engaging and disengaging magnification, 342 * {@code false} if it should ignore such gestures 343 */ 344 private final boolean mDetectTripleTap; 345 DetectingState(@iContext Context context, boolean detectTripleTap)346 DetectingState(@UiContext Context context, boolean detectTripleTap) { 347 mDetectTripleTap = detectTripleTap; 348 final MultiTap multiTap = new MultiTap(context, mDetectTripleTap ? 3 : 1, 349 mDetectTripleTap 350 ? MagnificationGestureMatcher.GESTURE_TRIPLE_TAP 351 : MagnificationGestureMatcher.GESTURE_SINGLE_TAP, null); 352 final MultiTapAndHold multiTapAndHold = new MultiTapAndHold(context, 353 mDetectTripleTap ? 3 : 1, 354 mDetectTripleTap 355 ? MagnificationGestureMatcher.GESTURE_TRIPLE_TAP_AND_HOLD 356 : MagnificationGestureMatcher.GESTURE_SINGLE_TAP_AND_HOLD, null); 357 mGesturesObserver = new MagnificationGesturesObserver(this, 358 new SimpleSwipe(context), 359 multiTap, 360 multiTapAndHold, 361 new TwoFingersDownOrSwipe(context)); 362 } 363 364 @Override onExit()365 public void onExit() { 366 clear(); 367 } 368 369 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)370 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 371 mGesturesObserver.onMotionEvent(event, rawEvent, policyFlags); 372 } 373 374 @Override clear()375 public void clear() { 376 mGesturesObserver.clear(); 377 } 378 379 @Override toString()380 public String toString() { 381 return "DetectingState{" 382 + ", mGestureTimeoutObserver =" + mGesturesObserver 383 + '}'; 384 } 385 386 @Override shouldStopDetection(MotionEvent motionEvent)387 public boolean shouldStopDetection(MotionEvent motionEvent) { 388 return !mWindowMagnificationMgr.isWindowMagnifierEnabled(mDisplayId) 389 && !mDetectTripleTap; 390 } 391 392 @Override onGestureCompleted(int gestureId, long lastDownEventTime, List<MotionEventInfo> delayedEventQueue, MotionEvent motionEvent)393 public void onGestureCompleted(int gestureId, long lastDownEventTime, 394 List<MotionEventInfo> delayedEventQueue, 395 MotionEvent motionEvent) { 396 if (DEBUG_DETECTING) { 397 Slog.d(mLogTag, "onGestureDetected : gesture = " 398 + MagnificationGestureMatcher.gestureIdToString( 399 gestureId)); 400 Slog.d(mLogTag, 401 "onGestureDetected : delayedEventQueue = " + delayedEventQueue); 402 } 403 if (gestureId == MagnificationGestureMatcher.GESTURE_TWO_FINGERS_DOWN_OR_SWIPE 404 && mWindowMagnificationMgr.pointersInWindow(mDisplayId, motionEvent) > 0) { 405 transitionTo(mObservePanningScalingState); 406 } else if (gestureId == MagnificationGestureMatcher.GESTURE_TRIPLE_TAP) { 407 onTripleTap(motionEvent); 408 } else { 409 mMotionEventDispatcherDelegate.sendDelayedMotionEvents(delayedEventQueue, 410 lastDownEventTime); 411 changeToDelegateStateIfNeed(motionEvent); 412 } 413 } 414 415 @Override onGestureCancelled(long lastDownEventTime, List<MotionEventInfo> delayedEventQueue, MotionEvent motionEvent)416 public void onGestureCancelled(long lastDownEventTime, 417 List<MotionEventInfo> delayedEventQueue, 418 MotionEvent motionEvent) { 419 if (DEBUG_DETECTING) { 420 Slog.d(mLogTag, 421 "onGestureCancelled : delayedEventQueue = " + delayedEventQueue); 422 } 423 mMotionEventDispatcherDelegate.sendDelayedMotionEvents(delayedEventQueue, 424 lastDownEventTime); 425 changeToDelegateStateIfNeed(motionEvent); 426 } 427 changeToDelegateStateIfNeed(MotionEvent motionEvent)428 private void changeToDelegateStateIfNeed(MotionEvent motionEvent) { 429 if (motionEvent != null && (motionEvent.getActionMasked() == ACTION_UP 430 || motionEvent.getActionMasked() == ACTION_CANCEL)) { 431 return; 432 } 433 transitionTo(mDelegatingState); 434 } 435 } 436 437 @Override toString()438 public String toString() { 439 return "WindowMagnificationGestureHandler{" 440 + "mDetectingState=" + mDetectingState 441 + ", mDelegatingState=" + mDelegatingState 442 + ", mMagnifiedInteractionState=" + mObservePanningScalingState 443 + ", mCurrentState=" + State.nameOf(mCurrentState) 444 + ", mPreviousState=" + State.nameOf(mPreviousState) 445 + ", mWindowMagnificationMgr=" + mWindowMagnificationMgr 446 + ", mDisplayId=" + mDisplayId 447 + '}'; 448 } 449 } 450