/* * Copyright (C) 2015 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; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Handler; import android.os.Message; import android.util.MathUtils; import android.util.Slog; import android.util.TypedValue; import android.view.GestureDetector; import android.view.GestureDetector.SimpleOnGestureListener; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; import android.view.MotionEvent.PointerProperties; import android.view.ScaleGestureDetector; import android.view.ScaleGestureDetector.OnScaleGestureListener; import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; /** * This class handles magnification in response to touch events. * * The behavior is as follows: * * 1. Triple tap toggles permanent screen magnification which is magnifying * the area around the location of the triple tap. One can think of the * location of the triple tap as the center of the magnified viewport. * For example, a triple tap when not magnified would magnify the screen * and leave it in a magnified state. A triple tapping when magnified would * clear magnification and leave the screen in a not magnified state. * * 2. Triple tap and hold would magnify the screen if not magnified and enable * viewport dragging mode until the finger goes up. One can think of this * mode as a way to move the magnified viewport since the area around the * moving finger will be magnified to fit the screen. For example, if the * screen was not magnified and the user triple taps and holds the screen * would magnify and the viewport will follow the user's finger. When the * finger goes up the screen will zoom out. If the same user interaction * is performed when the screen is magnified, the viewport movement will * be the same but when the finger goes up the screen will stay magnified. * In other words, the initial magnified state is sticky. * * 3. Magnification can optionally be "triggered" by some external shortcut * affordance. When this occurs via {@link #notifyShortcutTriggered()} a * subsequent tap in a magnifiable region will engage permanent screen * magnification as described in #1. Alternatively, a subsequent long-press * or drag will engage magnification with viewport dragging as described in * #2. Once magnified, all following behaviors apply whether magnification * was engaged via a triple-tap or by a triggered shortcut. * * 4. Pinching with any number of additional fingers when viewport dragging * is enabled, i.e. the user triple tapped and holds, would adjust the * magnification scale which will become the current default magnification * scale. The next time the user magnifies the same magnification scale * would be used. * * 5. When in a permanent magnified state the user can use two or more fingers * to pan the viewport. Note that in this mode the content is panned as * opposed to the viewport dragging mode in which the viewport is moved. * * 6. When in a permanent magnified state the user can use two or more * fingers to change the magnification scale which will become the current * default magnification scale. The next time the user magnifies the same * magnification scale would be used. * * 7. The magnification scale will be persisted in settings and in the cloud. */ class MagnificationGestureHandler implements EventStreamTransformation { private static final String LOG_TAG = "MagnificationEventHandler"; private static final boolean DEBUG_STATE_TRANSITIONS = false; private static final boolean DEBUG_DETECTING = false; private static final boolean DEBUG_PANNING = false; private static final int STATE_DELEGATING = 1; private static final int STATE_DETECTING = 2; private static final int STATE_VIEWPORT_DRAGGING = 3; private static final int STATE_MAGNIFIED_INTERACTION = 4; private static final float MIN_SCALE = 2.0f; private static final float MAX_SCALE = 5.0f; private final MagnificationController mMagnificationController; private final DetectingStateHandler mDetectingStateHandler; private final MagnifiedContentInteractionStateHandler mMagnifiedContentInteractionStateHandler; private final StateViewportDraggingHandler mStateViewportDraggingHandler; private final ScreenStateReceiver mScreenStateReceiver; private final boolean mDetectTripleTap; private final boolean mTriggerable; private EventStreamTransformation mNext; private int mCurrentState; private int mPreviousState; private boolean mTranslationEnabledBeforePan; private boolean mShortcutTriggered; private PointerCoords[] mTempPointerCoords; private PointerProperties[] mTempPointerProperties; private long mDelegatingStateDownTime; /** * @param context Context for resolving various magnification-related resources * @param ams AccessibilityManagerService used to obtain a {@link MagnificationController} * @param detectTripleTap {@code true} if this detector should detect and respond to triple-tap * gestures for engaging and disengaging magnification, * {@code false} if it should ignore such gestures * @param triggerable {@code true} if this detector should be "triggerable" by some external * shortcut invoking {@link #notifyShortcutTriggered}, {@code * false} if it should ignore such triggers. */ public MagnificationGestureHandler(Context context, AccessibilityManagerService ams, boolean detectTripleTap, boolean triggerable) { mMagnificationController = ams.getMagnificationController(); mDetectingStateHandler = new DetectingStateHandler(context); mStateViewportDraggingHandler = new StateViewportDraggingHandler(); mMagnifiedContentInteractionStateHandler = new MagnifiedContentInteractionStateHandler(context); mDetectTripleTap = detectTripleTap; mTriggerable = triggerable; if (triggerable) { mScreenStateReceiver = new ScreenStateReceiver(context, this); mScreenStateReceiver.register(); } else { mScreenStateReceiver = null; } transitionToState(STATE_DETECTING); } @Override public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { if (!event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) { if (mNext != null) { mNext.onMotionEvent(event, rawEvent, policyFlags); } return; } if (!mDetectTripleTap && !mTriggerable) { if (mNext != null) { dispatchTransformedEvent(event, rawEvent, policyFlags); } return; } mMagnifiedContentInteractionStateHandler.onMotionEvent(event, rawEvent, policyFlags); switch (mCurrentState) { case STATE_DELEGATING: { handleMotionEventStateDelegating(event, rawEvent, policyFlags); } break; case STATE_DETECTING: { mDetectingStateHandler.onMotionEvent(event, rawEvent, policyFlags); } break; case STATE_VIEWPORT_DRAGGING: { mStateViewportDraggingHandler.onMotionEvent(event, rawEvent, policyFlags); } break; case STATE_MAGNIFIED_INTERACTION: { // mMagnifiedContentInteractionStateHandler handles events only // if this is the current state since it uses ScaleGestureDetector // and a GestureDetector which need well formed event stream. } break; default: { throw new IllegalStateException("Unknown state: " + mCurrentState); } } } @Override public void onKeyEvent(KeyEvent event, int policyFlags) { if (mNext != null) { mNext.onKeyEvent(event, policyFlags); } } @Override public void onAccessibilityEvent(AccessibilityEvent event) { if (mNext != null) { mNext.onAccessibilityEvent(event); } } @Override public void setNext(EventStreamTransformation next) { mNext = next; } @Override public void clearEvents(int inputSource) { if (inputSource == InputDevice.SOURCE_TOUCHSCREEN) { clear(); } if (mNext != null) { mNext.clearEvents(inputSource); } } @Override public void onDestroy() { if (mScreenStateReceiver != null) { mScreenStateReceiver.unregister(); } clear(); } void notifyShortcutTriggered() { if (mTriggerable) { if (mMagnificationController.resetIfNeeded(true)) { clear(); } else { setMagnificationShortcutTriggered(!mShortcutTriggered); } } } private void setMagnificationShortcutTriggered(boolean state) { if (mShortcutTriggered == state) { return; } mShortcutTriggered = state; mMagnificationController.setForceShowMagnifiableBounds(state); } private void clear() { mCurrentState = STATE_DETECTING; setMagnificationShortcutTriggered(false); mDetectingStateHandler.clear(); mStateViewportDraggingHandler.clear(); mMagnifiedContentInteractionStateHandler.clear(); } private void handleMotionEventStateDelegating(MotionEvent event, MotionEvent rawEvent, int policyFlags) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: { mDelegatingStateDownTime = event.getDownTime(); } break; case MotionEvent.ACTION_UP: { if (mDetectingStateHandler.mDelayedEventQueue == null) { transitionToState(STATE_DETECTING); } } break; } if (mNext != null) { // We cache some events to see if the user wants to trigger magnification. // If no magnification is triggered we inject these events with adjusted // time and down time to prevent subsequent transformations being confused // by stale events. After the cached events, which always have a down, are // injected we need to also update the down time of all subsequent non cached // events. All delegated events cached and non-cached are delivered here. event.setDownTime(mDelegatingStateDownTime); dispatchTransformedEvent(event, rawEvent, policyFlags); } } private void dispatchTransformedEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { // If the event is within the magnified portion of the screen we have // to change its location to be where the user thinks he is poking the // UI which may have been magnified and panned. final float eventX = event.getX(); final float eventY = event.getY(); if (mMagnificationController.isMagnifying() && mMagnificationController.magnificationRegionContains(eventX, eventY)) { final float scale = mMagnificationController.getScale(); final float scaledOffsetX = mMagnificationController.getOffsetX(); final float scaledOffsetY = mMagnificationController.getOffsetY(); final int pointerCount = event.getPointerCount(); PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount); PointerProperties[] properties = getTempPointerPropertiesWithMinSize( pointerCount); for (int i = 0; i < pointerCount; i++) { event.getPointerCoords(i, coords[i]); coords[i].x = (coords[i].x - scaledOffsetX) / scale; coords[i].y = (coords[i].y - scaledOffsetY) / scale; event.getPointerProperties(i, properties[i]); } event = MotionEvent.obtain(event.getDownTime(), event.getEventTime(), event.getAction(), pointerCount, properties, coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(), event.getFlags()); } mNext.onMotionEvent(event, rawEvent, policyFlags); } private PointerCoords[] getTempPointerCoordsWithMinSize(int size) { final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0; if (oldSize < size) { PointerCoords[] oldTempPointerCoords = mTempPointerCoords; mTempPointerCoords = new PointerCoords[size]; if (oldTempPointerCoords != null) { System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize); } } for (int i = oldSize; i < size; i++) { mTempPointerCoords[i] = new PointerCoords(); } return mTempPointerCoords; } private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) { final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length : 0; if (oldSize < size) { PointerProperties[] oldTempPointerProperties = mTempPointerProperties; mTempPointerProperties = new PointerProperties[size]; if (oldTempPointerProperties != null) { System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, oldSize); } } for (int i = oldSize; i < size; i++) { mTempPointerProperties[i] = new PointerProperties(); } return mTempPointerProperties; } private void transitionToState(int state) { if (DEBUG_STATE_TRANSITIONS) { switch (state) { case STATE_DELEGATING: { Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING"); } break; case STATE_DETECTING: { Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING"); } break; case STATE_VIEWPORT_DRAGGING: { Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING"); } break; case STATE_MAGNIFIED_INTERACTION: { Slog.i(LOG_TAG, "mCurrentState: STATE_MAGNIFIED_INTERACTION"); } break; default: { throw new IllegalArgumentException("Unknown state: " + state); } } } mPreviousState = mCurrentState; mCurrentState = state; } private interface MotionEventHandler { void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags); void clear(); } /** * This class determines if the user is performing a scale or pan gesture. */ private final class MagnifiedContentInteractionStateHandler extends SimpleOnGestureListener implements OnScaleGestureListener, MotionEventHandler { private final ScaleGestureDetector mScaleGestureDetector; private final GestureDetector mGestureDetector; private final float mScalingThreshold; private float mInitialScaleFactor = -1; private boolean mScaling; public MagnifiedContentInteractionStateHandler(Context context) { final TypedValue scaleValue = new TypedValue(); context.getResources().getValue( com.android.internal.R.dimen.config_screen_magnification_scaling_threshold, scaleValue, false); mScalingThreshold = scaleValue.getFloat(); mScaleGestureDetector = new ScaleGestureDetector(context, this); mScaleGestureDetector.setQuickScaleEnabled(false); mGestureDetector = new GestureDetector(context, this); } @Override public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { mScaleGestureDetector.onTouchEvent(event); mGestureDetector.onTouchEvent(event); if (mCurrentState != STATE_MAGNIFIED_INTERACTION) { return; } if (event.getActionMasked() == MotionEvent.ACTION_UP) { clear(); mMagnificationController.persistScale(); if (mPreviousState == STATE_VIEWPORT_DRAGGING) { transitionToState(STATE_VIEWPORT_DRAGGING); } else { transitionToState(STATE_DETECTING); } } } @Override public boolean onScroll(MotionEvent first, MotionEvent second, float distanceX, float distanceY) { if (mCurrentState != STATE_MAGNIFIED_INTERACTION) { return true; } if (DEBUG_PANNING) { Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX + " scrollY: " + distanceY); } mMagnificationController.offsetMagnifiedRegion(distanceX, distanceY, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); return true; } @Override public boolean onScale(ScaleGestureDetector detector) { if (!mScaling) { if (mInitialScaleFactor < 0) { mInitialScaleFactor = detector.getScaleFactor(); } else { final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor; if (Math.abs(deltaScale) > mScalingThreshold) { mScaling = true; return true; } } return false; } final float initialScale = mMagnificationController.getScale(); final float targetScale = initialScale * detector.getScaleFactor(); // Don't allow a gesture to move the user further outside the // desired bounds for gesture-controlled scaling. final float scale; if (targetScale > MAX_SCALE && targetScale > initialScale) { // The target scale is too big and getting bigger. scale = MAX_SCALE; } else if (targetScale < MIN_SCALE && targetScale < initialScale) { // The target scale is too small and getting smaller. scale = MIN_SCALE; } else { // The target scale may be outside our bounds, but at least // it's moving in the right direction. This avoids a "jump" if // we're at odds with some other service's desired bounds. scale = targetScale; } final float pivotX = detector.getFocusX(); final float pivotY = detector.getFocusY(); mMagnificationController.setScale(scale, pivotX, pivotY, false, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); return true; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { return (mCurrentState == STATE_MAGNIFIED_INTERACTION); } @Override public void onScaleEnd(ScaleGestureDetector detector) { clear(); } @Override public void clear() { mInitialScaleFactor = -1; mScaling = false; } } /** * This class handles motion events when the event dispatcher has * determined that the user is performing a single-finger drag of the * magnification viewport. */ private final class StateViewportDraggingHandler implements MotionEventHandler { private boolean mLastMoveOutsideMagnifiedRegion; @Override public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: { throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN"); } case MotionEvent.ACTION_POINTER_DOWN: { clear(); transitionToState(STATE_MAGNIFIED_INTERACTION); } break; case MotionEvent.ACTION_MOVE: { if (event.getPointerCount() != 1) { throw new IllegalStateException("Should have one pointer down."); } final float eventX = event.getX(); final float eventY = event.getY(); if (mMagnificationController.magnificationRegionContains(eventX, eventY)) { if (mLastMoveOutsideMagnifiedRegion) { mLastMoveOutsideMagnifiedRegion = false; mMagnificationController.setCenter(eventX, eventY, true, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); } else { mMagnificationController.setCenter(eventX, eventY, false, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); } } else { mLastMoveOutsideMagnifiedRegion = true; } } break; case MotionEvent.ACTION_UP: { if (!mTranslationEnabledBeforePan) { mMagnificationController.reset(true); } clear(); transitionToState(STATE_DETECTING); } break; case MotionEvent.ACTION_POINTER_UP: { throw new IllegalArgumentException( "Unexpected event type: ACTION_POINTER_UP"); } } } @Override public void clear() { mLastMoveOutsideMagnifiedRegion = false; } } /** * This class handles motion events when the event dispatch has not yet * determined what the user is doing. It watches for various tap events. */ private final class DetectingStateHandler implements MotionEventHandler { private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1; private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2; private static final int ACTION_TAP_COUNT = 3; private final int mTapTimeSlop = ViewConfiguration.getJumpTapTimeout(); private final int mMultiTapTimeSlop; private final int mTapDistanceSlop; private final int mMultiTapDistanceSlop; private MotionEventInfo mDelayedEventQueue; private MotionEvent mLastDownEvent; private MotionEvent mLastTapUpEvent; private int mTapCount; public DetectingStateHandler(Context context) { mMultiTapTimeSlop = ViewConfiguration.getDoubleTapTimeout() + context.getResources().getInteger( com.android.internal.R.integer.config_screen_magnification_multi_tap_adjustment); mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); } private final Handler mHandler = new Handler() { @Override public void handleMessage(Message message) { final int type = message.what; switch (type) { case MESSAGE_ON_ACTION_TAP_AND_HOLD: { MotionEvent event = (MotionEvent) message.obj; final int policyFlags = message.arg1; onActionTapAndHold(event, policyFlags); } break; case MESSAGE_TRANSITION_TO_DELEGATING_STATE: { transitionToState(STATE_DELEGATING); sendDelayedMotionEvents(); clear(); } break; default: { throw new IllegalArgumentException("Unknown message type: " + type); } } } }; @Override public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { cacheDelayedMotionEvent(event, rawEvent, policyFlags); final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: { mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); if (!mMagnificationController.magnificationRegionContains( event.getX(), event.getY())) { transitionToDelegatingState(!mShortcutTriggered); return; } if (mShortcutTriggered) { Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD, policyFlags, 0, event); mHandler.sendMessageDelayed(message, ViewConfiguration.getLongPressTimeout()); return; } if (mDetectTripleTap) { if ((mTapCount == ACTION_TAP_COUNT - 1) && (mLastDownEvent != null) && GestureUtils.isMultiTap(mLastDownEvent, event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) { Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD, policyFlags, 0, event); mHandler.sendMessageDelayed(message, ViewConfiguration.getLongPressTimeout()); } else if (mTapCount < ACTION_TAP_COUNT) { Message message = mHandler.obtainMessage( MESSAGE_TRANSITION_TO_DELEGATING_STATE); mHandler.sendMessageDelayed(message, mMultiTapTimeSlop); } clearLastDownEvent(); mLastDownEvent = MotionEvent.obtain(event); } else if (mMagnificationController.isMagnifying()) { // If magnified, consume an ACTION_DOWN until mMultiTapTimeSlop or // mTapDistanceSlop is reached to ensure MAGNIFIED_INTERACTION is reachable. Message message = mHandler.obtainMessage( MESSAGE_TRANSITION_TO_DELEGATING_STATE); mHandler.sendMessageDelayed(message, mMultiTapTimeSlop); return; } else { transitionToDelegatingState(true); return; } } break; case MotionEvent.ACTION_POINTER_DOWN: { if (mMagnificationController.isMagnifying()) { mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); transitionToState(STATE_MAGNIFIED_INTERACTION); clear(); } else { transitionToDelegatingState(true); } } break; case MotionEvent.ACTION_MOVE: { if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) { final double distance = GestureUtils.computeDistance(mLastDownEvent, event, 0); if (Math.abs(distance) > mTapDistanceSlop) { transitionToDelegatingState(true); } } } break; case MotionEvent.ACTION_UP: { if (!mMagnificationController.magnificationRegionContains( event.getX(), event.getY())) { transitionToDelegatingState(!mShortcutTriggered); return; } if (mShortcutTriggered) { clear(); onActionTap(event, policyFlags); return; } if (mLastDownEvent == null) { return; } mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD); if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop, mTapDistanceSlop, 0)) { transitionToDelegatingState(true); return; } if (mLastTapUpEvent != null && !GestureUtils.isMultiTap( mLastTapUpEvent, event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) { transitionToDelegatingState(true); return; } mTapCount++; if (DEBUG_DETECTING) { Slog.i(LOG_TAG, "Tap count:" + mTapCount); } if (mTapCount == ACTION_TAP_COUNT) { clear(); onActionTap(event, policyFlags); return; } clearLastTapUpEvent(); mLastTapUpEvent = MotionEvent.obtain(event); } break; case MotionEvent.ACTION_POINTER_UP: { /* do nothing */ } break; } } @Override public void clear() { setMagnificationShortcutTriggered(false); mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD); mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); clearTapDetectionState(); clearDelayedMotionEvents(); } private void clearTapDetectionState() { mTapCount = 0; clearLastTapUpEvent(); clearLastDownEvent(); } private void clearLastTapUpEvent() { if (mLastTapUpEvent != null) { mLastTapUpEvent.recycle(); mLastTapUpEvent = null; } } private void clearLastDownEvent() { if (mLastDownEvent != null) { mLastDownEvent.recycle(); mLastDownEvent = null; } } private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent, policyFlags); if (mDelayedEventQueue == null) { mDelayedEventQueue = info; } else { MotionEventInfo tail = mDelayedEventQueue; while (tail.mNext != null) { tail = tail.mNext; } tail.mNext = info; } } private void sendDelayedMotionEvents() { while (mDelayedEventQueue != null) { MotionEventInfo info = mDelayedEventQueue; mDelayedEventQueue = info.mNext; MagnificationGestureHandler.this.onMotionEvent(info.mEvent, info.mRawEvent, info.mPolicyFlags); info.recycle(); } } private void clearDelayedMotionEvents() { while (mDelayedEventQueue != null) { MotionEventInfo info = mDelayedEventQueue; mDelayedEventQueue = info.mNext; info.recycle(); } } private void transitionToDelegatingState(boolean andClear) { transitionToState(STATE_DELEGATING); sendDelayedMotionEvents(); if (andClear) { clear(); } } private void onActionTap(MotionEvent up, int policyFlags) { if (DEBUG_DETECTING) { Slog.i(LOG_TAG, "onActionTap()"); } if (!mMagnificationController.isMagnifying()) { final float targetScale = mMagnificationController.getPersistedScale(); final float scale = MathUtils.constrain(targetScale, MIN_SCALE, MAX_SCALE); mMagnificationController.setScaleAndCenter(scale, up.getX(), up.getY(), true, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); } else { mMagnificationController.reset(true); } } private void onActionTapAndHold(MotionEvent down, int policyFlags) { if (DEBUG_DETECTING) { Slog.i(LOG_TAG, "onActionTapAndHold()"); } clear(); mTranslationEnabledBeforePan = mMagnificationController.isMagnifying(); final float targetScale = mMagnificationController.getPersistedScale(); final float scale = MathUtils.constrain(targetScale, MIN_SCALE, MAX_SCALE); mMagnificationController.setScaleAndCenter(scale, down.getX(), down.getY(), true, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); transitionToState(STATE_VIEWPORT_DRAGGING); } } private static final class MotionEventInfo { private static final int MAX_POOL_SIZE = 10; private static final Object sLock = new Object(); private static MotionEventInfo sPool; private static int sPoolSize; private MotionEventInfo mNext; private boolean mInPool; public MotionEvent mEvent; public MotionEvent mRawEvent; public int mPolicyFlags; public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent, int policyFlags) { synchronized (sLock) { MotionEventInfo info; if (sPoolSize > 0) { sPoolSize--; info = sPool; sPool = info.mNext; info.mNext = null; info.mInPool = false; } else { info = new MotionEventInfo(); } info.initialize(event, rawEvent, policyFlags); return info; } } private void initialize(MotionEvent event, MotionEvent rawEvent, int policyFlags) { mEvent = MotionEvent.obtain(event); mRawEvent = MotionEvent.obtain(rawEvent); mPolicyFlags = policyFlags; } public void recycle() { synchronized (sLock) { if (mInPool) { throw new IllegalStateException("Already recycled."); } clear(); if (sPoolSize < MAX_POOL_SIZE) { sPoolSize++; mNext = sPool; sPool = this; mInPool = true; } } } private void clear() { mEvent.recycle(); mEvent = null; mRawEvent.recycle(); mRawEvent = null; mPolicyFlags = 0; } } /** * BroadcastReceiver used to cancel the magnification shortcut when the screen turns off */ private static class ScreenStateReceiver extends BroadcastReceiver { private final Context mContext; private final MagnificationGestureHandler mGestureHandler; public ScreenStateReceiver(Context context, MagnificationGestureHandler gestureHandler) { mContext = context; mGestureHandler = gestureHandler; } public void register() { mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF)); } public void unregister() { mContext.unregisterReceiver(this); } @Override public void onReceive(Context context, Intent intent) { mGestureHandler.setMagnificationShortcutTriggered(false); } } }