1 /* 2 * Copyright (C) 2012 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; 18 19 import android.animation.ObjectAnimator; 20 import android.animation.TypeEvaluator; 21 import android.animation.ValueAnimator; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.graphics.Rect; 27 import android.graphics.Region; 28 import android.os.AsyncTask; 29 import android.os.Binder; 30 import android.os.Handler; 31 import android.os.Message; 32 import android.os.RemoteException; 33 import android.os.ServiceManager; 34 import android.os.SystemClock; 35 import android.provider.Settings; 36 import android.text.TextUtils; 37 import android.util.Property; 38 import android.util.Slog; 39 import android.view.GestureDetector; 40 import android.view.GestureDetector.SimpleOnGestureListener; 41 import android.view.IMagnificationCallbacks; 42 import android.view.IWindowManager; 43 import android.view.MagnificationSpec; 44 import android.view.MotionEvent; 45 import android.view.MotionEvent.PointerCoords; 46 import android.view.MotionEvent.PointerProperties; 47 import android.view.ScaleGestureDetector; 48 import android.view.ScaleGestureDetector.OnScaleGestureListener; 49 import android.view.View; 50 import android.view.ViewConfiguration; 51 import android.view.accessibility.AccessibilityEvent; 52 import android.view.animation.DecelerateInterpolator; 53 54 import com.android.internal.os.SomeArgs; 55 56 import java.util.Locale; 57 58 /** 59 * This class handles the screen magnification when accessibility is enabled. 60 * The behavior is as follows: 61 * 62 * 1. Triple tap toggles permanent screen magnification which is magnifying 63 * the area around the location of the triple tap. One can think of the 64 * location of the triple tap as the center of the magnified viewport. 65 * For example, a triple tap when not magnified would magnify the screen 66 * and leave it in a magnified state. A triple tapping when magnified would 67 * clear magnification and leave the screen in a not magnified state. 68 * 69 * 2. Triple tap and hold would magnify the screen if not magnified and enable 70 * viewport dragging mode until the finger goes up. One can think of this 71 * mode as a way to move the magnified viewport since the area around the 72 * moving finger will be magnified to fit the screen. For example, if the 73 * screen was not magnified and the user triple taps and holds the screen 74 * would magnify and the viewport will follow the user's finger. When the 75 * finger goes up the screen will zoom out. If the same user interaction 76 * is performed when the screen is magnified, the viewport movement will 77 * be the same but when the finger goes up the screen will stay magnified. 78 * In other words, the initial magnified state is sticky. 79 * 80 * 3. Pinching with any number of additional fingers when viewport dragging 81 * is enabled, i.e. the user triple tapped and holds, would adjust the 82 * magnification scale which will become the current default magnification 83 * scale. The next time the user magnifies the same magnification scale 84 * would be used. 85 * 86 * 4. When in a permanent magnified state the user can use two or more fingers 87 * to pan the viewport. Note that in this mode the content is panned as 88 * opposed to the viewport dragging mode in which the viewport is moved. 89 * 90 * 5. When in a permanent magnified state the user can use two or more 91 * fingers to change the magnification scale which will become the current 92 * default magnification scale. The next time the user magnifies the same 93 * magnification scale would be used. 94 * 95 * 6. The magnification scale will be persisted in settings and in the cloud. 96 */ 97 public final class ScreenMagnifier extends IMagnificationCallbacks.Stub 98 implements EventStreamTransformation { 99 100 private static final String LOG_TAG = ScreenMagnifier.class.getSimpleName(); 101 102 private static final boolean DEBUG_STATE_TRANSITIONS = false; 103 private static final boolean DEBUG_DETECTING = false; 104 private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false; 105 private static final boolean DEBUG_PANNING = false; 106 private static final boolean DEBUG_SCALING = false; 107 private static final boolean DEBUG_MAGNIFICATION_CONTROLLER = false; 108 109 private static final int STATE_DELEGATING = 1; 110 private static final int STATE_DETECTING = 2; 111 private static final int STATE_VIEWPORT_DRAGGING = 3; 112 private static final int STATE_MAGNIFIED_INTERACTION = 4; 113 114 private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f; 115 private static final int MULTI_TAP_TIME_SLOP_ADJUSTMENT = 50; 116 117 private static final int MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED = 1; 118 private static final int MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED = 2; 119 private static final int MESSAGE_ON_USER_CONTEXT_CHANGED = 3; 120 private static final int MESSAGE_ON_ROTATION_CHANGED = 4; 121 122 private static final int DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE = 1; 123 124 private static final int MY_PID = android.os.Process.myPid(); 125 126 private final Rect mTempRect = new Rect(); 127 private final Rect mTempRect1 = new Rect(); 128 129 private final Context mContext; 130 private final IWindowManager mWindowManager; 131 private final MagnificationController mMagnificationController; 132 private final ScreenStateObserver mScreenStateObserver; 133 134 private final DetectingStateHandler mDetectingStateHandler; 135 private final MagnifiedContentInteractonStateHandler mMagnifiedContentInteractonStateHandler; 136 private final StateViewportDraggingHandler mStateViewportDraggingHandler; 137 138 private final AccessibilityManagerService mAms; 139 140 private final int mTapTimeSlop = ViewConfiguration.getTapTimeout(); 141 private final int mMultiTapTimeSlop = 142 ViewConfiguration.getDoubleTapTimeout() - MULTI_TAP_TIME_SLOP_ADJUSTMENT; 143 private final int mTapDistanceSlop; 144 private final int mMultiTapDistanceSlop; 145 146 private final long mLongAnimationDuration; 147 148 private final Region mMagnifiedBounds = new Region(); 149 150 private EventStreamTransformation mNext; 151 152 private int mCurrentState; 153 private int mPreviousState; 154 private boolean mTranslationEnabledBeforePan; 155 156 private PointerCoords[] mTempPointerCoords; 157 private PointerProperties[] mTempPointerProperties; 158 159 private long mDelegatingStateDownTime; 160 161 private boolean mUpdateMagnificationSpecOnNextBoundsChange; 162 163 private final Handler mHandler = new Handler() { 164 @Override 165 public void handleMessage(Message message) { 166 switch (message.what) { 167 case MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED: { 168 Region bounds = (Region) message.obj; 169 handleOnMagnifiedBoundsChanged(bounds); 170 bounds.recycle(); 171 } break; 172 case MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED: { 173 SomeArgs args = (SomeArgs) message.obj; 174 final int left = args.argi1; 175 final int top = args.argi2; 176 final int right = args.argi3; 177 final int bottom = args.argi4; 178 handleOnRectangleOnScreenRequested(left, top, right, bottom); 179 args.recycle(); 180 } break; 181 case MESSAGE_ON_USER_CONTEXT_CHANGED: { 182 handleOnUserContextChanged(); 183 } break; 184 case MESSAGE_ON_ROTATION_CHANGED: { 185 final int rotation = message.arg1; 186 handleOnRotationChanged(rotation); 187 } break; 188 } 189 } 190 }; 191 192 public ScreenMagnifier(Context context, int displayId, AccessibilityManagerService service) { 193 mContext = context; 194 mWindowManager = IWindowManager.Stub.asInterface( 195 ServiceManager.getService("window")); 196 mAms = service; 197 198 mLongAnimationDuration = context.getResources().getInteger( 199 com.android.internal.R.integer.config_longAnimTime); 200 mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 201 mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); 202 203 mDetectingStateHandler = new DetectingStateHandler(); 204 mStateViewportDraggingHandler = new StateViewportDraggingHandler(); 205 mMagnifiedContentInteractonStateHandler = new MagnifiedContentInteractonStateHandler( 206 context); 207 208 mMagnificationController = new MagnificationController(mLongAnimationDuration); 209 mScreenStateObserver = new ScreenStateObserver(context, mMagnificationController); 210 211 try { 212 mWindowManager.setMagnificationCallbacks(this); 213 } catch (RemoteException re) { 214 /* ignore */ 215 } 216 217 transitionToState(STATE_DETECTING); 218 } 219 220 @Override 221 public void onMagnifedBoundsChanged(Region bounds) { 222 Region newBounds = Region.obtain(bounds); 223 mHandler.obtainMessage(MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED, newBounds).sendToTarget(); 224 if (MY_PID != Binder.getCallingPid()) { 225 bounds.recycle(); 226 } 227 } 228 229 private void handleOnMagnifiedBoundsChanged(Region bounds) { 230 // If there was a rotation we have to update the center of the magnified 231 // region since the old offset X/Y may be out of its acceptable range for 232 // the new display width and height. 233 if (mUpdateMagnificationSpecOnNextBoundsChange) { 234 mUpdateMagnificationSpecOnNextBoundsChange = false; 235 MagnificationSpec spec = mMagnificationController.getMagnificationSpec(); 236 Rect magnifiedFrame = mTempRect; 237 mMagnifiedBounds.getBounds(magnifiedFrame); 238 final float scale = spec.scale; 239 final float centerX = (-spec.offsetX + magnifiedFrame.width() / 2) / scale; 240 final float centerY = (-spec.offsetY + magnifiedFrame.height() / 2) / scale; 241 mMagnificationController.setScaleAndMagnifiedRegionCenter(scale, centerX, 242 centerY, false); 243 } 244 mMagnifiedBounds.set(bounds); 245 mAms.onMagnificationStateChanged(); 246 } 247 248 @Override 249 public void onRectangleOnScreenRequested(int left, int top, int right, int bottom) { 250 SomeArgs args = SomeArgs.obtain(); 251 args.argi1 = left; 252 args.argi2 = top; 253 args.argi3 = right; 254 args.argi4 = bottom; 255 mHandler.obtainMessage(MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED, args).sendToTarget(); 256 } 257 258 private void handleOnRectangleOnScreenRequested(int left, int top, int right, int bottom) { 259 Rect magnifiedFrame = mTempRect; 260 mMagnifiedBounds.getBounds(magnifiedFrame); 261 if (!magnifiedFrame.intersects(left, top, right, bottom)) { 262 return; 263 } 264 Rect magnifFrameInScreenCoords = mTempRect1; 265 getMagnifiedFrameInContentCoords(magnifFrameInScreenCoords); 266 final float scrollX; 267 final float scrollY; 268 if (right - left > magnifFrameInScreenCoords.width()) { 269 final int direction = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()); 270 if (direction == View.LAYOUT_DIRECTION_LTR) { 271 scrollX = left - magnifFrameInScreenCoords.left; 272 } else { 273 scrollX = right - magnifFrameInScreenCoords.right; 274 } 275 } else if (left < magnifFrameInScreenCoords.left) { 276 scrollX = left - magnifFrameInScreenCoords.left; 277 } else if (right > magnifFrameInScreenCoords.right) { 278 scrollX = right - magnifFrameInScreenCoords.right; 279 } else { 280 scrollX = 0; 281 } 282 if (bottom - top > magnifFrameInScreenCoords.height()) { 283 scrollY = top - magnifFrameInScreenCoords.top; 284 } else if (top < magnifFrameInScreenCoords.top) { 285 scrollY = top - magnifFrameInScreenCoords.top; 286 } else if (bottom > magnifFrameInScreenCoords.bottom) { 287 scrollY = bottom - magnifFrameInScreenCoords.bottom; 288 } else { 289 scrollY = 0; 290 } 291 final float scale = mMagnificationController.getScale(); 292 mMagnificationController.offsetMagnifiedRegionCenter(scrollX * scale, scrollY * scale); 293 } 294 295 @Override 296 public void onRotationChanged(int rotation) { 297 mHandler.obtainMessage(MESSAGE_ON_ROTATION_CHANGED, rotation, 0).sendToTarget(); 298 } 299 300 private void handleOnRotationChanged(int rotation) { 301 resetMagnificationIfNeeded(); 302 if (mMagnificationController.isMagnifying()) { 303 mUpdateMagnificationSpecOnNextBoundsChange = true; 304 } 305 } 306 307 @Override 308 public void onUserContextChanged() { 309 mHandler.sendEmptyMessage(MESSAGE_ON_USER_CONTEXT_CHANGED); 310 } 311 312 private void handleOnUserContextChanged() { 313 resetMagnificationIfNeeded(); 314 } 315 316 private void getMagnifiedFrameInContentCoords(Rect rect) { 317 MagnificationSpec spec = mMagnificationController.getMagnificationSpec(); 318 mMagnifiedBounds.getBounds(rect); 319 rect.offset((int) -spec.offsetX, (int) -spec.offsetY); 320 rect.scale(1.0f / spec.scale); 321 } 322 323 private void resetMagnificationIfNeeded() { 324 if (mMagnificationController.isMagnifying() 325 && isScreenMagnificationAutoUpdateEnabled(mContext)) { 326 mMagnificationController.reset(true); 327 } 328 } 329 330 @Override 331 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, 332 int policyFlags) { 333 mMagnifiedContentInteractonStateHandler.onMotionEvent(event); 334 switch (mCurrentState) { 335 case STATE_DELEGATING: { 336 handleMotionEventStateDelegating(event, rawEvent, policyFlags); 337 } break; 338 case STATE_DETECTING: { 339 mDetectingStateHandler.onMotionEvent(event, rawEvent, policyFlags); 340 } break; 341 case STATE_VIEWPORT_DRAGGING: { 342 mStateViewportDraggingHandler.onMotionEvent(event, policyFlags); 343 } break; 344 case STATE_MAGNIFIED_INTERACTION: { 345 // mMagnifiedContentInteractonStateHandler handles events only 346 // if this is the current state since it uses ScaleGestureDetecotr 347 // and a GestureDetector which need well formed event stream. 348 } break; 349 default: { 350 throw new IllegalStateException("Unknown state: " + mCurrentState); 351 } 352 } 353 } 354 355 @Override 356 public void onAccessibilityEvent(AccessibilityEvent event) { 357 if (mNext != null) { 358 mNext.onAccessibilityEvent(event); 359 } 360 } 361 362 @Override 363 public void setNext(EventStreamTransformation next) { 364 mNext = next; 365 } 366 367 @Override 368 public void clear() { 369 mCurrentState = STATE_DETECTING; 370 mDetectingStateHandler.clear(); 371 mStateViewportDraggingHandler.clear(); 372 mMagnifiedContentInteractonStateHandler.clear(); 373 if (mNext != null) { 374 mNext.clear(); 375 } 376 } 377 378 @Override 379 public void onDestroy() { 380 mScreenStateObserver.destroy(); 381 try { 382 mWindowManager.setMagnificationCallbacks(null); 383 } catch (RemoteException re) { 384 /* ignore */ 385 } 386 } 387 388 private void handleMotionEventStateDelegating(MotionEvent event, 389 MotionEvent rawEvent, int policyFlags) { 390 switch (event.getActionMasked()) { 391 case MotionEvent.ACTION_DOWN: { 392 mDelegatingStateDownTime = event.getDownTime(); 393 } break; 394 case MotionEvent.ACTION_UP: { 395 if (mDetectingStateHandler.mDelayedEventQueue == null) { 396 transitionToState(STATE_DETECTING); 397 } 398 } break; 399 } 400 if (mNext != null) { 401 // If the event is within the magnified portion of the screen we have 402 // to change its location to be where the user thinks he is poking the 403 // UI which may have been magnified and panned. 404 final float eventX = event.getX(); 405 final float eventY = event.getY(); 406 if (mMagnificationController.isMagnifying() 407 && mMagnifiedBounds.contains((int) eventX, (int) eventY)) { 408 final float scale = mMagnificationController.getScale(); 409 final float scaledOffsetX = mMagnificationController.getOffsetX(); 410 final float scaledOffsetY = mMagnificationController.getOffsetY(); 411 final int pointerCount = event.getPointerCount(); 412 PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount); 413 PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount); 414 for (int i = 0; i < pointerCount; i++) { 415 event.getPointerCoords(i, coords[i]); 416 coords[i].x = (coords[i].x - scaledOffsetX) / scale; 417 coords[i].y = (coords[i].y - scaledOffsetY) / scale; 418 event.getPointerProperties(i, properties[i]); 419 } 420 event = MotionEvent.obtain(event.getDownTime(), 421 event.getEventTime(), event.getAction(), pointerCount, properties, 422 coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(), 423 event.getFlags()); 424 } 425 // We cache some events to see if the user wants to trigger magnification. 426 // If no magnification is triggered we inject these events with adjusted 427 // time and down time to prevent subsequent transformations being confused 428 // by stale events. After the cached events, which always have a down, are 429 // injected we need to also update the down time of all subsequent non cached 430 // events. All delegated events cached and non-cached are delivered here. 431 event.setDownTime(mDelegatingStateDownTime); 432 mNext.onMotionEvent(event, rawEvent, policyFlags); 433 } 434 } 435 436 private PointerCoords[] getTempPointerCoordsWithMinSize(int size) { 437 final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0; 438 if (oldSize < size) { 439 PointerCoords[] oldTempPointerCoords = mTempPointerCoords; 440 mTempPointerCoords = new PointerCoords[size]; 441 if (oldTempPointerCoords != null) { 442 System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize); 443 } 444 } 445 for (int i = oldSize; i < size; i++) { 446 mTempPointerCoords[i] = new PointerCoords(); 447 } 448 return mTempPointerCoords; 449 } 450 451 private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) { 452 final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length : 0; 453 if (oldSize < size) { 454 PointerProperties[] oldTempPointerProperties = mTempPointerProperties; 455 mTempPointerProperties = new PointerProperties[size]; 456 if (oldTempPointerProperties != null) { 457 System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, oldSize); 458 } 459 } 460 for (int i = oldSize; i < size; i++) { 461 mTempPointerProperties[i] = new PointerProperties(); 462 } 463 return mTempPointerProperties; 464 } 465 466 private void transitionToState(int state) { 467 if (DEBUG_STATE_TRANSITIONS) { 468 switch (state) { 469 case STATE_DELEGATING: { 470 Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING"); 471 } break; 472 case STATE_DETECTING: { 473 Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING"); 474 } break; 475 case STATE_VIEWPORT_DRAGGING: { 476 Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING"); 477 } break; 478 case STATE_MAGNIFIED_INTERACTION: { 479 Slog.i(LOG_TAG, "mCurrentState: STATE_MAGNIFIED_INTERACTION"); 480 } break; 481 default: { 482 throw new IllegalArgumentException("Unknown state: " + state); 483 } 484 } 485 } 486 mPreviousState = mCurrentState; 487 mCurrentState = state; 488 } 489 490 private final class MagnifiedContentInteractonStateHandler 491 extends SimpleOnGestureListener implements OnScaleGestureListener { 492 private static final float MIN_SCALE = 1.3f; 493 private static final float MAX_SCALE = 5.0f; 494 495 private static final float SCALING_THRESHOLD = 0.3f; 496 497 private final ScaleGestureDetector mScaleGestureDetector; 498 private final GestureDetector mGestureDetector; 499 500 private float mInitialScaleFactor = -1; 501 private boolean mScaling; 502 503 public MagnifiedContentInteractonStateHandler(Context context) { 504 mScaleGestureDetector = new ScaleGestureDetector(context, this); 505 mScaleGestureDetector.setQuickScaleEnabled(false); 506 mGestureDetector = new GestureDetector(context, this); 507 } 508 509 public void onMotionEvent(MotionEvent event) { 510 mScaleGestureDetector.onTouchEvent(event); 511 mGestureDetector.onTouchEvent(event); 512 if (mCurrentState != STATE_MAGNIFIED_INTERACTION) { 513 return; 514 } 515 if (event.getActionMasked() == MotionEvent.ACTION_UP) { 516 clear(); 517 final float scale = Math.min(Math.max(mMagnificationController.getScale(), 518 MIN_SCALE), MAX_SCALE); 519 if (scale != getPersistedScale()) { 520 persistScale(scale); 521 } 522 if (mPreviousState == STATE_VIEWPORT_DRAGGING) { 523 transitionToState(STATE_VIEWPORT_DRAGGING); 524 } else { 525 transitionToState(STATE_DETECTING); 526 } 527 } 528 } 529 530 @Override 531 public boolean onScroll(MotionEvent first, MotionEvent second, float distanceX, 532 float distanceY) { 533 if (mCurrentState != STATE_MAGNIFIED_INTERACTION) { 534 return true; 535 } 536 if (DEBUG_PANNING) { 537 Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX 538 + " scrollY: " + distanceY); 539 } 540 mMagnificationController.offsetMagnifiedRegionCenter(distanceX, distanceY); 541 return true; 542 } 543 544 @Override 545 public boolean onScale(ScaleGestureDetector detector) { 546 if (!mScaling) { 547 if (mInitialScaleFactor < 0) { 548 mInitialScaleFactor = detector.getScaleFactor(); 549 } else { 550 final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor; 551 if (Math.abs(deltaScale) > SCALING_THRESHOLD) { 552 mScaling = true; 553 return true; 554 } 555 } 556 return false; 557 } 558 final float newScale = mMagnificationController.getScale() 559 * detector.getScaleFactor(); 560 final float normalizedNewScale = Math.min(Math.max(newScale, MIN_SCALE), MAX_SCALE); 561 if (DEBUG_SCALING) { 562 Slog.i(LOG_TAG, "normalizedNewScale: " + normalizedNewScale); 563 } 564 mMagnificationController.setScale(normalizedNewScale, detector.getFocusX(), 565 detector.getFocusY(), false); 566 return true; 567 } 568 569 @Override 570 public boolean onScaleBegin(ScaleGestureDetector detector) { 571 return (mCurrentState == STATE_MAGNIFIED_INTERACTION); 572 } 573 574 @Override 575 public void onScaleEnd(ScaleGestureDetector detector) { 576 clear(); 577 } 578 579 private void clear() { 580 mInitialScaleFactor = -1; 581 mScaling = false; 582 } 583 } 584 585 private final class StateViewportDraggingHandler { 586 private boolean mLastMoveOutsideMagnifiedRegion; 587 588 private void onMotionEvent(MotionEvent event, int policyFlags) { 589 final int action = event.getActionMasked(); 590 switch (action) { 591 case MotionEvent.ACTION_DOWN: { 592 throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN"); 593 } 594 case MotionEvent.ACTION_POINTER_DOWN: { 595 clear(); 596 transitionToState(STATE_MAGNIFIED_INTERACTION); 597 } break; 598 case MotionEvent.ACTION_MOVE: { 599 if (event.getPointerCount() != 1) { 600 throw new IllegalStateException("Should have one pointer down."); 601 } 602 final float eventX = event.getX(); 603 final float eventY = event.getY(); 604 if (mMagnifiedBounds.contains((int) eventX, (int) eventY)) { 605 if (mLastMoveOutsideMagnifiedRegion) { 606 mLastMoveOutsideMagnifiedRegion = false; 607 mMagnificationController.setMagnifiedRegionCenter(eventX, 608 eventY, true); 609 } else { 610 mMagnificationController.setMagnifiedRegionCenter(eventX, 611 eventY, false); 612 } 613 } else { 614 mLastMoveOutsideMagnifiedRegion = true; 615 } 616 } break; 617 case MotionEvent.ACTION_UP: { 618 if (!mTranslationEnabledBeforePan) { 619 mMagnificationController.reset(true); 620 } 621 clear(); 622 transitionToState(STATE_DETECTING); 623 } break; 624 case MotionEvent.ACTION_POINTER_UP: { 625 throw new IllegalArgumentException("Unexpected event type: ACTION_POINTER_UP"); 626 } 627 } 628 } 629 630 public void clear() { 631 mLastMoveOutsideMagnifiedRegion = false; 632 } 633 } 634 635 private final class DetectingStateHandler { 636 637 private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1; 638 639 private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2; 640 641 private static final int ACTION_TAP_COUNT = 3; 642 643 private MotionEventInfo mDelayedEventQueue; 644 645 private MotionEvent mLastDownEvent; 646 private MotionEvent mLastTapUpEvent; 647 private int mTapCount; 648 649 private final Handler mHandler = new Handler() { 650 @Override 651 public void handleMessage(Message message) { 652 final int type = message.what; 653 switch (type) { 654 case MESSAGE_ON_ACTION_TAP_AND_HOLD: { 655 MotionEvent event = (MotionEvent) message.obj; 656 final int policyFlags = message.arg1; 657 onActionTapAndHold(event, policyFlags); 658 } break; 659 case MESSAGE_TRANSITION_TO_DELEGATING_STATE: { 660 transitionToState(STATE_DELEGATING); 661 sendDelayedMotionEvents(); 662 clear(); 663 } break; 664 default: { 665 throw new IllegalArgumentException("Unknown message type: " + type); 666 } 667 } 668 } 669 }; 670 671 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 672 cacheDelayedMotionEvent(event, rawEvent, policyFlags); 673 final int action = event.getActionMasked(); 674 switch (action) { 675 case MotionEvent.ACTION_DOWN: { 676 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 677 if (!mMagnifiedBounds.contains((int) event.getX(), 678 (int) event.getY())) { 679 transitionToDelegatingStateAndClear(); 680 return; 681 } 682 if (mTapCount == ACTION_TAP_COUNT - 1 && mLastDownEvent != null 683 && GestureUtils.isMultiTap(mLastDownEvent, event, 684 mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) { 685 Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD, 686 policyFlags, 0, event); 687 mHandler.sendMessageDelayed(message, 688 ViewConfiguration.getLongPressTimeout()); 689 } else if (mTapCount < ACTION_TAP_COUNT) { 690 Message message = mHandler.obtainMessage( 691 MESSAGE_TRANSITION_TO_DELEGATING_STATE); 692 mHandler.sendMessageDelayed(message, mMultiTapTimeSlop); 693 } 694 clearLastDownEvent(); 695 mLastDownEvent = MotionEvent.obtain(event); 696 } break; 697 case MotionEvent.ACTION_POINTER_DOWN: { 698 if (mMagnificationController.isMagnifying()) { 699 transitionToState(STATE_MAGNIFIED_INTERACTION); 700 clear(); 701 } else { 702 transitionToDelegatingStateAndClear(); 703 } 704 } break; 705 case MotionEvent.ACTION_MOVE: { 706 if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) { 707 final double distance = GestureUtils.computeDistance(mLastDownEvent, 708 event, 0); 709 if (Math.abs(distance) > mTapDistanceSlop) { 710 transitionToDelegatingStateAndClear(); 711 } 712 } 713 } break; 714 case MotionEvent.ACTION_UP: { 715 if (mLastDownEvent == null) { 716 return; 717 } 718 mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD); 719 if (!mMagnifiedBounds.contains((int) event.getX(), (int) event.getY())) { 720 transitionToDelegatingStateAndClear(); 721 return; 722 } 723 if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop, 724 mTapDistanceSlop, 0)) { 725 transitionToDelegatingStateAndClear(); 726 return; 727 } 728 if (mLastTapUpEvent != null && !GestureUtils.isMultiTap(mLastTapUpEvent, 729 event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) { 730 transitionToDelegatingStateAndClear(); 731 return; 732 } 733 mTapCount++; 734 if (DEBUG_DETECTING) { 735 Slog.i(LOG_TAG, "Tap count:" + mTapCount); 736 } 737 if (mTapCount == ACTION_TAP_COUNT) { 738 clear(); 739 onActionTap(event, policyFlags); 740 return; 741 } 742 clearLastTapUpEvent(); 743 mLastTapUpEvent = MotionEvent.obtain(event); 744 } break; 745 case MotionEvent.ACTION_POINTER_UP: { 746 /* do nothing */ 747 } break; 748 } 749 } 750 751 public void clear() { 752 mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD); 753 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 754 clearTapDetectionState(); 755 clearDelayedMotionEvents(); 756 } 757 758 private void clearTapDetectionState() { 759 mTapCount = 0; 760 clearLastTapUpEvent(); 761 clearLastDownEvent(); 762 } 763 764 private void clearLastTapUpEvent() { 765 if (mLastTapUpEvent != null) { 766 mLastTapUpEvent.recycle(); 767 mLastTapUpEvent = null; 768 } 769 } 770 771 private void clearLastDownEvent() { 772 if (mLastDownEvent != null) { 773 mLastDownEvent.recycle(); 774 mLastDownEvent = null; 775 } 776 } 777 778 private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, 779 int policyFlags) { 780 MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent, 781 policyFlags); 782 if (mDelayedEventQueue == null) { 783 mDelayedEventQueue = info; 784 } else { 785 MotionEventInfo tail = mDelayedEventQueue; 786 while (tail.mNext != null) { 787 tail = tail.mNext; 788 } 789 tail.mNext = info; 790 } 791 } 792 793 private void sendDelayedMotionEvents() { 794 while (mDelayedEventQueue != null) { 795 MotionEventInfo info = mDelayedEventQueue; 796 mDelayedEventQueue = info.mNext; 797 final long offset = SystemClock.uptimeMillis() - info.mCachedTimeMillis; 798 MotionEvent event = obtainEventWithOffsetTimeAndDownTime(info.mEvent, offset); 799 MotionEvent rawEvent = obtainEventWithOffsetTimeAndDownTime(info.mRawEvent, offset); 800 ScreenMagnifier.this.onMotionEvent(event, rawEvent, info.mPolicyFlags); 801 event.recycle(); 802 rawEvent.recycle(); 803 info.recycle(); 804 } 805 } 806 807 private MotionEvent obtainEventWithOffsetTimeAndDownTime(MotionEvent event, long offset) { 808 final int pointerCount = event.getPointerCount(); 809 PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount); 810 PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount); 811 for (int i = 0; i < pointerCount; i++) { 812 event.getPointerCoords(i, coords[i]); 813 event.getPointerProperties(i, properties[i]); 814 } 815 final long downTime = event.getDownTime() + offset; 816 final long eventTime = event.getEventTime() + offset; 817 return MotionEvent.obtain(downTime, eventTime, 818 event.getAction(), pointerCount, properties, coords, 819 event.getMetaState(), event.getButtonState(), 820 1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(), 821 event.getSource(), event.getFlags()); 822 } 823 824 private void clearDelayedMotionEvents() { 825 while (mDelayedEventQueue != null) { 826 MotionEventInfo info = mDelayedEventQueue; 827 mDelayedEventQueue = info.mNext; 828 info.recycle(); 829 } 830 } 831 832 private void transitionToDelegatingStateAndClear() { 833 transitionToState(STATE_DELEGATING); 834 sendDelayedMotionEvents(); 835 clear(); 836 } 837 838 private void onActionTap(MotionEvent up, int policyFlags) { 839 if (DEBUG_DETECTING) { 840 Slog.i(LOG_TAG, "onActionTap()"); 841 } 842 if (!mMagnificationController.isMagnifying()) { 843 mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(), 844 up.getX(), up.getY(), true); 845 } else { 846 mMagnificationController.reset(true); 847 } 848 } 849 850 private void onActionTapAndHold(MotionEvent down, int policyFlags) { 851 if (DEBUG_DETECTING) { 852 Slog.i(LOG_TAG, "onActionTapAndHold()"); 853 } 854 clear(); 855 mTranslationEnabledBeforePan = mMagnificationController.isMagnifying(); 856 mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(), 857 down.getX(), down.getY(), true); 858 transitionToState(STATE_VIEWPORT_DRAGGING); 859 } 860 } 861 862 private void persistScale(final float scale) { 863 new AsyncTask<Void, Void, Void>() { 864 @Override 865 protected Void doInBackground(Void... params) { 866 Settings.Secure.putFloat(mContext.getContentResolver(), 867 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale); 868 return null; 869 } 870 }.execute(); 871 } 872 873 private float getPersistedScale() { 874 return Settings.Secure.getFloat(mContext.getContentResolver(), 875 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 876 DEFAULT_MAGNIFICATION_SCALE); 877 } 878 879 private static boolean isScreenMagnificationAutoUpdateEnabled(Context context) { 880 return (Settings.Secure.getInt(context.getContentResolver(), 881 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE, 882 DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1); 883 } 884 885 private static final class MotionEventInfo { 886 887 private static final int MAX_POOL_SIZE = 10; 888 889 private static final Object sLock = new Object(); 890 private static MotionEventInfo sPool; 891 private static int sPoolSize; 892 893 private MotionEventInfo mNext; 894 private boolean mInPool; 895 896 public MotionEvent mEvent; 897 public MotionEvent mRawEvent; 898 public int mPolicyFlags; 899 public long mCachedTimeMillis; 900 901 public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent, 902 int policyFlags) { 903 synchronized (sLock) { 904 MotionEventInfo info; 905 if (sPoolSize > 0) { 906 sPoolSize--; 907 info = sPool; 908 sPool = info.mNext; 909 info.mNext = null; 910 info.mInPool = false; 911 } else { 912 info = new MotionEventInfo(); 913 } 914 info.initialize(event, rawEvent, policyFlags); 915 return info; 916 } 917 } 918 919 private void initialize(MotionEvent event, MotionEvent rawEvent, 920 int policyFlags) { 921 mEvent = MotionEvent.obtain(event); 922 mRawEvent = MotionEvent.obtain(rawEvent); 923 mPolicyFlags = policyFlags; 924 mCachedTimeMillis = SystemClock.uptimeMillis(); 925 } 926 927 public void recycle() { 928 synchronized (sLock) { 929 if (mInPool) { 930 throw new IllegalStateException("Already recycled."); 931 } 932 clear(); 933 if (sPoolSize < MAX_POOL_SIZE) { 934 sPoolSize++; 935 mNext = sPool; 936 sPool = this; 937 mInPool = true; 938 } 939 } 940 } 941 942 private void clear() { 943 mEvent.recycle(); 944 mEvent = null; 945 mRawEvent.recycle(); 946 mRawEvent = null; 947 mPolicyFlags = 0; 948 mCachedTimeMillis = 0; 949 } 950 } 951 952 private final class MagnificationController { 953 954 private static final String PROPERTY_NAME_MAGNIFICATION_SPEC = 955 "magnificationSpec"; 956 957 private final MagnificationSpec mSentMagnificationSpec = MagnificationSpec.obtain(); 958 959 private final MagnificationSpec mCurrentMagnificationSpec = MagnificationSpec.obtain(); 960 961 private final Rect mTempRect = new Rect(); 962 963 private final ValueAnimator mTransformationAnimator; 964 965 public MagnificationController(long animationDuration) { 966 Property<MagnificationController, MagnificationSpec> property = 967 Property.of(MagnificationController.class, MagnificationSpec.class, 968 PROPERTY_NAME_MAGNIFICATION_SPEC); 969 TypeEvaluator<MagnificationSpec> evaluator = new TypeEvaluator<MagnificationSpec>() { 970 private final MagnificationSpec mTempTransformationSpec = 971 MagnificationSpec.obtain(); 972 @Override 973 public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec, 974 MagnificationSpec toSpec) { 975 MagnificationSpec result = mTempTransformationSpec; 976 result.scale = fromSpec.scale 977 + (toSpec.scale - fromSpec.scale) * fraction; 978 result.offsetX = fromSpec.offsetX + (toSpec.offsetX - fromSpec.offsetX) 979 * fraction; 980 result.offsetY = fromSpec.offsetY + (toSpec.offsetY - fromSpec.offsetY) 981 * fraction; 982 return result; 983 } 984 }; 985 mTransformationAnimator = ObjectAnimator.ofObject(this, property, 986 evaluator, mSentMagnificationSpec, mCurrentMagnificationSpec); 987 mTransformationAnimator.setDuration((long) (animationDuration)); 988 mTransformationAnimator.setInterpolator(new DecelerateInterpolator(2.5f)); 989 } 990 991 public boolean isMagnifying() { 992 return mCurrentMagnificationSpec.scale > 1.0f; 993 } 994 995 public void reset(boolean animate) { 996 if (mTransformationAnimator.isRunning()) { 997 mTransformationAnimator.cancel(); 998 } 999 mCurrentMagnificationSpec.clear(); 1000 if (animate) { 1001 animateMangificationSpec(mSentMagnificationSpec, 1002 mCurrentMagnificationSpec); 1003 } else { 1004 setMagnificationSpec(mCurrentMagnificationSpec); 1005 } 1006 Rect bounds = mTempRect; 1007 bounds.setEmpty(); 1008 mAms.onMagnificationStateChanged(); 1009 } 1010 1011 public float getScale() { 1012 return mCurrentMagnificationSpec.scale; 1013 } 1014 1015 public float getOffsetX() { 1016 return mCurrentMagnificationSpec.offsetX; 1017 } 1018 1019 public float getOffsetY() { 1020 return mCurrentMagnificationSpec.offsetY; 1021 } 1022 1023 public void setScale(float scale, float pivotX, float pivotY, boolean animate) { 1024 Rect magnifiedFrame = mTempRect; 1025 mMagnifiedBounds.getBounds(magnifiedFrame); 1026 MagnificationSpec spec = mCurrentMagnificationSpec; 1027 final float oldScale = spec.scale; 1028 final float oldCenterX = (-spec.offsetX + magnifiedFrame.width() / 2) / oldScale; 1029 final float oldCenterY = (-spec.offsetY + magnifiedFrame.height() / 2) / oldScale; 1030 final float normPivotX = (-spec.offsetX + pivotX) / oldScale; 1031 final float normPivotY = (-spec.offsetY + pivotY) / oldScale; 1032 final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale); 1033 final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale); 1034 final float centerX = normPivotX + offsetX; 1035 final float centerY = normPivotY + offsetY; 1036 setScaleAndMagnifiedRegionCenter(scale, centerX, centerY, animate); 1037 } 1038 1039 public void setMagnifiedRegionCenter(float centerX, float centerY, boolean animate) { 1040 setScaleAndMagnifiedRegionCenter(mCurrentMagnificationSpec.scale, centerX, centerY, 1041 animate); 1042 } 1043 1044 public void offsetMagnifiedRegionCenter(float offsetX, float offsetY) { 1045 final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX; 1046 mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX, 1047 getMinOffsetX()), 0); 1048 final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY; 1049 mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY, 1050 getMinOffsetY()), 0); 1051 setMagnificationSpec(mCurrentMagnificationSpec); 1052 } 1053 1054 public void setScaleAndMagnifiedRegionCenter(float scale, float centerX, float centerY, 1055 boolean animate) { 1056 if (Float.compare(mCurrentMagnificationSpec.scale, scale) == 0 1057 && Float.compare(mCurrentMagnificationSpec.offsetX, 1058 centerX) == 0 1059 && Float.compare(mCurrentMagnificationSpec.offsetY, 1060 centerY) == 0) { 1061 return; 1062 } 1063 if (mTransformationAnimator.isRunning()) { 1064 mTransformationAnimator.cancel(); 1065 } 1066 if (DEBUG_MAGNIFICATION_CONTROLLER) { 1067 Slog.i(LOG_TAG, "scale: " + scale + " offsetX: " + centerX 1068 + " offsetY: " + centerY); 1069 } 1070 updateMagnificationSpec(scale, centerX, centerY); 1071 if (animate) { 1072 animateMangificationSpec(mSentMagnificationSpec, 1073 mCurrentMagnificationSpec); 1074 } else { 1075 setMagnificationSpec(mCurrentMagnificationSpec); 1076 } 1077 mAms.onMagnificationStateChanged(); 1078 } 1079 1080 public void updateMagnificationSpec(float scale, float magnifiedCenterX, 1081 float magnifiedCenterY) { 1082 Rect magnifiedFrame = mTempRect; 1083 mMagnifiedBounds.getBounds(magnifiedFrame); 1084 mCurrentMagnificationSpec.scale = scale; 1085 final int viewportWidth = magnifiedFrame.width(); 1086 final float nonNormOffsetX = viewportWidth / 2 - magnifiedCenterX * scale; 1087 mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX, 1088 getMinOffsetX()), 0); 1089 final int viewportHeight = magnifiedFrame.height(); 1090 final float nonNormOffsetY = viewportHeight / 2 - magnifiedCenterY * scale; 1091 mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY, 1092 getMinOffsetY()), 0); 1093 } 1094 1095 private float getMinOffsetX() { 1096 Rect magnifiedFrame = mTempRect; 1097 mMagnifiedBounds.getBounds(magnifiedFrame); 1098 final float viewportWidth = magnifiedFrame.width(); 1099 return viewportWidth - viewportWidth * mCurrentMagnificationSpec.scale; 1100 } 1101 1102 private float getMinOffsetY() { 1103 Rect magnifiedFrame = mTempRect; 1104 mMagnifiedBounds.getBounds(magnifiedFrame); 1105 final float viewportHeight = magnifiedFrame.height(); 1106 return viewportHeight - viewportHeight * mCurrentMagnificationSpec.scale; 1107 } 1108 1109 private void animateMangificationSpec(MagnificationSpec fromSpec, 1110 MagnificationSpec toSpec) { 1111 mTransformationAnimator.setObjectValues(fromSpec, toSpec); 1112 mTransformationAnimator.start(); 1113 } 1114 1115 public MagnificationSpec getMagnificationSpec() { 1116 return mSentMagnificationSpec; 1117 } 1118 1119 public void setMagnificationSpec(MagnificationSpec spec) { 1120 if (DEBUG_SET_MAGNIFICATION_SPEC) { 1121 Slog.i(LOG_TAG, "Sending: " + spec); 1122 } 1123 try { 1124 mSentMagnificationSpec.scale = spec.scale; 1125 mSentMagnificationSpec.offsetX = spec.offsetX; 1126 mSentMagnificationSpec.offsetY = spec.offsetY; 1127 mWindowManager.setMagnificationSpec( 1128 MagnificationSpec.obtain(spec)); 1129 } catch (RemoteException re) { 1130 /* ignore */ 1131 } 1132 } 1133 } 1134 1135 private final class ScreenStateObserver extends BroadcastReceiver { 1136 private static final int MESSAGE_ON_SCREEN_STATE_CHANGE = 1; 1137 1138 private final Context mContext; 1139 private final MagnificationController mMagnificationController; 1140 1141 private final Handler mHandler = new Handler() { 1142 @Override 1143 public void handleMessage(Message message) { 1144 switch (message.what) { 1145 case MESSAGE_ON_SCREEN_STATE_CHANGE: { 1146 String action = (String) message.obj; 1147 handleOnScreenStateChange(action); 1148 } break; 1149 } 1150 } 1151 }; 1152 1153 public ScreenStateObserver(Context context, 1154 MagnificationController magnificationController) { 1155 mContext = context; 1156 mMagnificationController = magnificationController; 1157 mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF)); 1158 } 1159 1160 public void destroy() { 1161 mContext.unregisterReceiver(this); 1162 } 1163 1164 @Override 1165 public void onReceive(Context context, Intent intent) { 1166 mHandler.obtainMessage(MESSAGE_ON_SCREEN_STATE_CHANGE, 1167 intent.getAction()).sendToTarget(); 1168 } 1169 1170 private void handleOnScreenStateChange(String action) { 1171 if (mMagnificationController.isMagnifying() 1172 && isScreenMagnificationAutoUpdateEnabled(mContext)) { 1173 mMagnificationController.reset(false); 1174 } 1175 } 1176 } 1177 } 1178