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 mGestureDetector = new GestureDetector(context, this); 506 } 507 508 public void onMotionEvent(MotionEvent event) { 509 mScaleGestureDetector.onTouchEvent(event); 510 mGestureDetector.onTouchEvent(event); 511 if (mCurrentState != STATE_MAGNIFIED_INTERACTION) { 512 return; 513 } 514 if (event.getActionMasked() == MotionEvent.ACTION_UP) { 515 clear(); 516 final float scale = Math.min(Math.max(mMagnificationController.getScale(), 517 MIN_SCALE), MAX_SCALE); 518 if (scale != getPersistedScale()) { 519 persistScale(scale); 520 } 521 if (mPreviousState == STATE_VIEWPORT_DRAGGING) { 522 transitionToState(STATE_VIEWPORT_DRAGGING); 523 } else { 524 transitionToState(STATE_DETECTING); 525 } 526 } 527 } 528 529 @Override 530 public boolean onScroll(MotionEvent first, MotionEvent second, float distanceX, 531 float distanceY) { 532 if (mCurrentState != STATE_MAGNIFIED_INTERACTION) { 533 return true; 534 } 535 if (DEBUG_PANNING) { 536 Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX 537 + " scrollY: " + distanceY); 538 } 539 mMagnificationController.offsetMagnifiedRegionCenter(distanceX, distanceY); 540 return true; 541 } 542 543 @Override 544 public boolean onScale(ScaleGestureDetector detector) { 545 if (!mScaling) { 546 if (mInitialScaleFactor < 0) { 547 mInitialScaleFactor = detector.getScaleFactor(); 548 } else { 549 final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor; 550 if (Math.abs(deltaScale) > SCALING_THRESHOLD) { 551 mScaling = true; 552 return true; 553 } 554 } 555 return false; 556 } 557 final float newScale = mMagnificationController.getScale() 558 * detector.getScaleFactor(); 559 final float normalizedNewScale = Math.min(Math.max(newScale, MIN_SCALE), MAX_SCALE); 560 if (DEBUG_SCALING) { 561 Slog.i(LOG_TAG, "normalizedNewScale: " + normalizedNewScale); 562 } 563 mMagnificationController.setScale(normalizedNewScale, detector.getFocusX(), 564 detector.getFocusY(), false); 565 return true; 566 } 567 568 @Override 569 public boolean onScaleBegin(ScaleGestureDetector detector) { 570 return (mCurrentState == STATE_MAGNIFIED_INTERACTION); 571 } 572 573 @Override 574 public void onScaleEnd(ScaleGestureDetector detector) { 575 clear(); 576 } 577 578 private void clear() { 579 mInitialScaleFactor = -1; 580 mScaling = false; 581 } 582 } 583 584 private final class StateViewportDraggingHandler { 585 private boolean mLastMoveOutsideMagnifiedRegion; 586 587 private void onMotionEvent(MotionEvent event, int policyFlags) { 588 final int action = event.getActionMasked(); 589 switch (action) { 590 case MotionEvent.ACTION_DOWN: { 591 throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN"); 592 } 593 case MotionEvent.ACTION_POINTER_DOWN: { 594 clear(); 595 transitionToState(STATE_MAGNIFIED_INTERACTION); 596 } break; 597 case MotionEvent.ACTION_MOVE: { 598 if (event.getPointerCount() != 1) { 599 throw new IllegalStateException("Should have one pointer down."); 600 } 601 final float eventX = event.getX(); 602 final float eventY = event.getY(); 603 if (mMagnifiedBounds.contains((int) eventX, (int) eventY)) { 604 if (mLastMoveOutsideMagnifiedRegion) { 605 mLastMoveOutsideMagnifiedRegion = false; 606 mMagnificationController.setMagnifiedRegionCenter(eventX, 607 eventY, true); 608 } else { 609 mMagnificationController.setMagnifiedRegionCenter(eventX, 610 eventY, false); 611 } 612 } else { 613 mLastMoveOutsideMagnifiedRegion = true; 614 } 615 } break; 616 case MotionEvent.ACTION_UP: { 617 if (!mTranslationEnabledBeforePan) { 618 mMagnificationController.reset(true); 619 } 620 clear(); 621 transitionToState(STATE_DETECTING); 622 } break; 623 case MotionEvent.ACTION_POINTER_UP: { 624 throw new IllegalArgumentException("Unexpected event type: ACTION_POINTER_UP"); 625 } 626 } 627 } 628 629 public void clear() { 630 mLastMoveOutsideMagnifiedRegion = false; 631 } 632 } 633 634 private final class DetectingStateHandler { 635 636 private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1; 637 638 private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2; 639 640 private static final int ACTION_TAP_COUNT = 3; 641 642 private MotionEventInfo mDelayedEventQueue; 643 644 private MotionEvent mLastDownEvent; 645 private MotionEvent mLastTapUpEvent; 646 private int mTapCount; 647 648 private final Handler mHandler = new Handler() { 649 @Override 650 public void handleMessage(Message message) { 651 final int type = message.what; 652 switch (type) { 653 case MESSAGE_ON_ACTION_TAP_AND_HOLD: { 654 MotionEvent event = (MotionEvent) message.obj; 655 final int policyFlags = message.arg1; 656 onActionTapAndHold(event, policyFlags); 657 } break; 658 case MESSAGE_TRANSITION_TO_DELEGATING_STATE: { 659 transitionToState(STATE_DELEGATING); 660 sendDelayedMotionEvents(); 661 clear(); 662 } break; 663 default: { 664 throw new IllegalArgumentException("Unknown message type: " + type); 665 } 666 } 667 } 668 }; 669 670 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 671 cacheDelayedMotionEvent(event, rawEvent, policyFlags); 672 final int action = event.getActionMasked(); 673 switch (action) { 674 case MotionEvent.ACTION_DOWN: { 675 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 676 if (!mMagnifiedBounds.contains((int) event.getX(), 677 (int) event.getY())) { 678 transitionToDelegatingStateAndClear(); 679 return; 680 } 681 if (mTapCount == ACTION_TAP_COUNT - 1 && mLastDownEvent != null 682 && GestureUtils.isMultiTap(mLastDownEvent, event, 683 mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) { 684 Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD, 685 policyFlags, 0, event); 686 mHandler.sendMessageDelayed(message, 687 ViewConfiguration.getLongPressTimeout()); 688 } else if (mTapCount < ACTION_TAP_COUNT) { 689 Message message = mHandler.obtainMessage( 690 MESSAGE_TRANSITION_TO_DELEGATING_STATE); 691 mHandler.sendMessageDelayed(message, mMultiTapTimeSlop); 692 } 693 clearLastDownEvent(); 694 mLastDownEvent = MotionEvent.obtain(event); 695 } break; 696 case MotionEvent.ACTION_POINTER_DOWN: { 697 if (mMagnificationController.isMagnifying()) { 698 transitionToState(STATE_MAGNIFIED_INTERACTION); 699 clear(); 700 } else { 701 transitionToDelegatingStateAndClear(); 702 } 703 } break; 704 case MotionEvent.ACTION_MOVE: { 705 if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) { 706 final double distance = GestureUtils.computeDistance(mLastDownEvent, 707 event, 0); 708 if (Math.abs(distance) > mTapDistanceSlop) { 709 transitionToDelegatingStateAndClear(); 710 } 711 } 712 } break; 713 case MotionEvent.ACTION_UP: { 714 if (mLastDownEvent == null) { 715 return; 716 } 717 mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD); 718 if (!mMagnifiedBounds.contains((int) event.getX(), (int) event.getY())) { 719 transitionToDelegatingStateAndClear(); 720 return; 721 } 722 if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop, 723 mTapDistanceSlop, 0)) { 724 transitionToDelegatingStateAndClear(); 725 return; 726 } 727 if (mLastTapUpEvent != null && !GestureUtils.isMultiTap(mLastTapUpEvent, 728 event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) { 729 transitionToDelegatingStateAndClear(); 730 return; 731 } 732 mTapCount++; 733 if (DEBUG_DETECTING) { 734 Slog.i(LOG_TAG, "Tap count:" + mTapCount); 735 } 736 if (mTapCount == ACTION_TAP_COUNT) { 737 clear(); 738 onActionTap(event, policyFlags); 739 return; 740 } 741 clearLastTapUpEvent(); 742 mLastTapUpEvent = MotionEvent.obtain(event); 743 } break; 744 case MotionEvent.ACTION_POINTER_UP: { 745 /* do nothing */ 746 } break; 747 } 748 } 749 750 public void clear() { 751 mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD); 752 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 753 clearTapDetectionState(); 754 clearDelayedMotionEvents(); 755 } 756 757 private void clearTapDetectionState() { 758 mTapCount = 0; 759 clearLastTapUpEvent(); 760 clearLastDownEvent(); 761 } 762 763 private void clearLastTapUpEvent() { 764 if (mLastTapUpEvent != null) { 765 mLastTapUpEvent.recycle(); 766 mLastTapUpEvent = null; 767 } 768 } 769 770 private void clearLastDownEvent() { 771 if (mLastDownEvent != null) { 772 mLastDownEvent.recycle(); 773 mLastDownEvent = null; 774 } 775 } 776 777 private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, 778 int policyFlags) { 779 MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent, 780 policyFlags); 781 if (mDelayedEventQueue == null) { 782 mDelayedEventQueue = info; 783 } else { 784 MotionEventInfo tail = mDelayedEventQueue; 785 while (tail.mNext != null) { 786 tail = tail.mNext; 787 } 788 tail.mNext = info; 789 } 790 } 791 792 private void sendDelayedMotionEvents() { 793 while (mDelayedEventQueue != null) { 794 MotionEventInfo info = mDelayedEventQueue; 795 mDelayedEventQueue = info.mNext; 796 final long offset = SystemClock.uptimeMillis() - info.mCachedTimeMillis; 797 MotionEvent event = obtainEventWithOffsetTimeAndDownTime(info.mEvent, offset); 798 MotionEvent rawEvent = obtainEventWithOffsetTimeAndDownTime(info.mRawEvent, offset); 799 ScreenMagnifier.this.onMotionEvent(event, rawEvent, info.mPolicyFlags); 800 event.recycle(); 801 rawEvent.recycle(); 802 info.recycle(); 803 } 804 } 805 806 private MotionEvent obtainEventWithOffsetTimeAndDownTime(MotionEvent event, long offset) { 807 final int pointerCount = event.getPointerCount(); 808 PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount); 809 PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount); 810 for (int i = 0; i < pointerCount; i++) { 811 event.getPointerCoords(i, coords[i]); 812 event.getPointerProperties(i, properties[i]); 813 } 814 final long downTime = event.getDownTime() + offset; 815 final long eventTime = event.getEventTime() + offset; 816 return MotionEvent.obtain(downTime, eventTime, 817 event.getAction(), pointerCount, properties, coords, 818 event.getMetaState(), event.getButtonState(), 819 1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(), 820 event.getSource(), event.getFlags()); 821 } 822 823 private void clearDelayedMotionEvents() { 824 while (mDelayedEventQueue != null) { 825 MotionEventInfo info = mDelayedEventQueue; 826 mDelayedEventQueue = info.mNext; 827 info.recycle(); 828 } 829 } 830 831 private void transitionToDelegatingStateAndClear() { 832 transitionToState(STATE_DELEGATING); 833 sendDelayedMotionEvents(); 834 clear(); 835 } 836 837 private void onActionTap(MotionEvent up, int policyFlags) { 838 if (DEBUG_DETECTING) { 839 Slog.i(LOG_TAG, "onActionTap()"); 840 } 841 if (!mMagnificationController.isMagnifying()) { 842 mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(), 843 up.getX(), up.getY(), true); 844 } else { 845 mMagnificationController.reset(true); 846 } 847 } 848 849 private void onActionTapAndHold(MotionEvent down, int policyFlags) { 850 if (DEBUG_DETECTING) { 851 Slog.i(LOG_TAG, "onActionTapAndHold()"); 852 } 853 clear(); 854 mTranslationEnabledBeforePan = mMagnificationController.isMagnifying(); 855 mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(), 856 down.getX(), down.getY(), true); 857 transitionToState(STATE_VIEWPORT_DRAGGING); 858 } 859 } 860 861 private void persistScale(final float scale) { 862 new AsyncTask<Void, Void, Void>() { 863 @Override 864 protected Void doInBackground(Void... params) { 865 Settings.Secure.putFloat(mContext.getContentResolver(), 866 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale); 867 return null; 868 } 869 }.execute(); 870 } 871 872 private float getPersistedScale() { 873 return Settings.Secure.getFloat(mContext.getContentResolver(), 874 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 875 DEFAULT_MAGNIFICATION_SCALE); 876 } 877 878 private static boolean isScreenMagnificationAutoUpdateEnabled(Context context) { 879 return (Settings.Secure.getInt(context.getContentResolver(), 880 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE, 881 DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1); 882 } 883 884 private static final class MotionEventInfo { 885 886 private static final int MAX_POOL_SIZE = 10; 887 888 private static final Object sLock = new Object(); 889 private static MotionEventInfo sPool; 890 private static int sPoolSize; 891 892 private MotionEventInfo mNext; 893 private boolean mInPool; 894 895 public MotionEvent mEvent; 896 public MotionEvent mRawEvent; 897 public int mPolicyFlags; 898 public long mCachedTimeMillis; 899 900 public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent, 901 int policyFlags) { 902 synchronized (sLock) { 903 MotionEventInfo info; 904 if (sPoolSize > 0) { 905 sPoolSize--; 906 info = sPool; 907 sPool = info.mNext; 908 info.mNext = null; 909 info.mInPool = false; 910 } else { 911 info = new MotionEventInfo(); 912 } 913 info.initialize(event, rawEvent, policyFlags); 914 return info; 915 } 916 } 917 918 private void initialize(MotionEvent event, MotionEvent rawEvent, 919 int policyFlags) { 920 mEvent = MotionEvent.obtain(event); 921 mRawEvent = MotionEvent.obtain(rawEvent); 922 mPolicyFlags = policyFlags; 923 mCachedTimeMillis = SystemClock.uptimeMillis(); 924 } 925 926 public void recycle() { 927 synchronized (sLock) { 928 if (mInPool) { 929 throw new IllegalStateException("Already recycled."); 930 } 931 clear(); 932 if (sPoolSize < MAX_POOL_SIZE) { 933 sPoolSize++; 934 mNext = sPool; 935 sPool = this; 936 mInPool = true; 937 } 938 } 939 } 940 941 private void clear() { 942 mEvent.recycle(); 943 mEvent = null; 944 mRawEvent.recycle(); 945 mRawEvent = null; 946 mPolicyFlags = 0; 947 mCachedTimeMillis = 0; 948 } 949 } 950 951 private final class MagnificationController { 952 953 private static final String PROPERTY_NAME_MAGNIFICATION_SPEC = 954 "magnificationSpec"; 955 956 private final MagnificationSpec mSentMagnificationSpec = MagnificationSpec.obtain(); 957 958 private final MagnificationSpec mCurrentMagnificationSpec = MagnificationSpec.obtain(); 959 960 private final Rect mTempRect = new Rect(); 961 962 private final ValueAnimator mTransformationAnimator; 963 964 public MagnificationController(long animationDuration) { 965 Property<MagnificationController, MagnificationSpec> property = 966 Property.of(MagnificationController.class, MagnificationSpec.class, 967 PROPERTY_NAME_MAGNIFICATION_SPEC); 968 TypeEvaluator<MagnificationSpec> evaluator = new TypeEvaluator<MagnificationSpec>() { 969 private final MagnificationSpec mTempTransformationSpec = 970 MagnificationSpec.obtain(); 971 @Override 972 public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec, 973 MagnificationSpec toSpec) { 974 MagnificationSpec result = mTempTransformationSpec; 975 result.scale = fromSpec.scale 976 + (toSpec.scale - fromSpec.scale) * fraction; 977 result.offsetX = fromSpec.offsetX + (toSpec.offsetX - fromSpec.offsetX) 978 * fraction; 979 result.offsetY = fromSpec.offsetY + (toSpec.offsetY - fromSpec.offsetY) 980 * fraction; 981 return result; 982 } 983 }; 984 mTransformationAnimator = ObjectAnimator.ofObject(this, property, 985 evaluator, mSentMagnificationSpec, mCurrentMagnificationSpec); 986 mTransformationAnimator.setDuration((long) (animationDuration)); 987 mTransformationAnimator.setInterpolator(new DecelerateInterpolator(2.5f)); 988 } 989 990 public boolean isMagnifying() { 991 return mCurrentMagnificationSpec.scale > 1.0f; 992 } 993 994 public void reset(boolean animate) { 995 if (mTransformationAnimator.isRunning()) { 996 mTransformationAnimator.cancel(); 997 } 998 mCurrentMagnificationSpec.clear(); 999 if (animate) { 1000 animateMangificationSpec(mSentMagnificationSpec, 1001 mCurrentMagnificationSpec); 1002 } else { 1003 setMagnificationSpec(mCurrentMagnificationSpec); 1004 } 1005 Rect bounds = mTempRect; 1006 bounds.setEmpty(); 1007 mAms.onMagnificationStateChanged(); 1008 } 1009 1010 public float getScale() { 1011 return mCurrentMagnificationSpec.scale; 1012 } 1013 1014 public float getOffsetX() { 1015 return mCurrentMagnificationSpec.offsetX; 1016 } 1017 1018 public float getOffsetY() { 1019 return mCurrentMagnificationSpec.offsetY; 1020 } 1021 1022 public void setScale(float scale, float pivotX, float pivotY, boolean animate) { 1023 Rect magnifiedFrame = mTempRect; 1024 mMagnifiedBounds.getBounds(magnifiedFrame); 1025 MagnificationSpec spec = mCurrentMagnificationSpec; 1026 final float oldScale = spec.scale; 1027 final float oldCenterX = (-spec.offsetX + magnifiedFrame.width() / 2) / oldScale; 1028 final float oldCenterY = (-spec.offsetY + magnifiedFrame.height() / 2) / oldScale; 1029 final float normPivotX = (-spec.offsetX + pivotX) / oldScale; 1030 final float normPivotY = (-spec.offsetY + pivotY) / oldScale; 1031 final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale); 1032 final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale); 1033 final float centerX = normPivotX + offsetX; 1034 final float centerY = normPivotY + offsetY; 1035 setScaleAndMagnifiedRegionCenter(scale, centerX, centerY, animate); 1036 } 1037 1038 public void setMagnifiedRegionCenter(float centerX, float centerY, boolean animate) { 1039 setScaleAndMagnifiedRegionCenter(mCurrentMagnificationSpec.scale, centerX, centerY, 1040 animate); 1041 } 1042 1043 public void offsetMagnifiedRegionCenter(float offsetX, float offsetY) { 1044 final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX; 1045 mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX, 1046 getMinOffsetX()), 0); 1047 final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY; 1048 mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY, 1049 getMinOffsetY()), 0); 1050 setMagnificationSpec(mCurrentMagnificationSpec); 1051 } 1052 1053 public void setScaleAndMagnifiedRegionCenter(float scale, float centerX, float centerY, 1054 boolean animate) { 1055 if (Float.compare(mCurrentMagnificationSpec.scale, scale) == 0 1056 && Float.compare(mCurrentMagnificationSpec.offsetX, 1057 centerX) == 0 1058 && Float.compare(mCurrentMagnificationSpec.offsetY, 1059 centerY) == 0) { 1060 return; 1061 } 1062 if (mTransformationAnimator.isRunning()) { 1063 mTransformationAnimator.cancel(); 1064 } 1065 if (DEBUG_MAGNIFICATION_CONTROLLER) { 1066 Slog.i(LOG_TAG, "scale: " + scale + " offsetX: " + centerX 1067 + " offsetY: " + centerY); 1068 } 1069 updateMagnificationSpec(scale, centerX, centerY); 1070 if (animate) { 1071 animateMangificationSpec(mSentMagnificationSpec, 1072 mCurrentMagnificationSpec); 1073 } else { 1074 setMagnificationSpec(mCurrentMagnificationSpec); 1075 } 1076 mAms.onMagnificationStateChanged(); 1077 } 1078 1079 public void updateMagnificationSpec(float scale, float magnifiedCenterX, 1080 float magnifiedCenterY) { 1081 Rect magnifiedFrame = mTempRect; 1082 mMagnifiedBounds.getBounds(magnifiedFrame); 1083 mCurrentMagnificationSpec.scale = scale; 1084 final int viewportWidth = magnifiedFrame.width(); 1085 final float nonNormOffsetX = viewportWidth / 2 - magnifiedCenterX * scale; 1086 mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX, 1087 getMinOffsetX()), 0); 1088 final int viewportHeight = magnifiedFrame.height(); 1089 final float nonNormOffsetY = viewportHeight / 2 - magnifiedCenterY * scale; 1090 mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY, 1091 getMinOffsetY()), 0); 1092 } 1093 1094 private float getMinOffsetX() { 1095 Rect magnifiedFrame = mTempRect; 1096 mMagnifiedBounds.getBounds(magnifiedFrame); 1097 final float viewportWidth = magnifiedFrame.width(); 1098 return viewportWidth - viewportWidth * mCurrentMagnificationSpec.scale; 1099 } 1100 1101 private float getMinOffsetY() { 1102 Rect magnifiedFrame = mTempRect; 1103 mMagnifiedBounds.getBounds(magnifiedFrame); 1104 final float viewportHeight = magnifiedFrame.height(); 1105 return viewportHeight - viewportHeight * mCurrentMagnificationSpec.scale; 1106 } 1107 1108 private void animateMangificationSpec(MagnificationSpec fromSpec, 1109 MagnificationSpec toSpec) { 1110 mTransformationAnimator.setObjectValues(fromSpec, toSpec); 1111 mTransformationAnimator.start(); 1112 } 1113 1114 public MagnificationSpec getMagnificationSpec() { 1115 return mSentMagnificationSpec; 1116 } 1117 1118 public void setMagnificationSpec(MagnificationSpec spec) { 1119 if (DEBUG_SET_MAGNIFICATION_SPEC) { 1120 Slog.i(LOG_TAG, "Sending: " + spec); 1121 } 1122 try { 1123 mSentMagnificationSpec.scale = spec.scale; 1124 mSentMagnificationSpec.offsetX = spec.offsetX; 1125 mSentMagnificationSpec.offsetY = spec.offsetY; 1126 mWindowManager.setMagnificationSpec( 1127 MagnificationSpec.obtain(spec)); 1128 } catch (RemoteException re) { 1129 /* ignore */ 1130 } 1131 } 1132 } 1133 1134 private final class ScreenStateObserver extends BroadcastReceiver { 1135 private static final int MESSAGE_ON_SCREEN_STATE_CHANGE = 1; 1136 1137 private final Context mContext; 1138 private final MagnificationController mMagnificationController; 1139 1140 private final Handler mHandler = new Handler() { 1141 @Override 1142 public void handleMessage(Message message) { 1143 switch (message.what) { 1144 case MESSAGE_ON_SCREEN_STATE_CHANGE: { 1145 String action = (String) message.obj; 1146 handleOnScreenStateChange(action); 1147 } break; 1148 } 1149 } 1150 }; 1151 1152 public ScreenStateObserver(Context context, 1153 MagnificationController magnificationController) { 1154 mContext = context; 1155 mMagnificationController = magnificationController; 1156 mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF)); 1157 } 1158 1159 public void destroy() { 1160 mContext.unregisterReceiver(this); 1161 } 1162 1163 @Override 1164 public void onReceive(Context context, Intent intent) { 1165 mHandler.obtainMessage(MESSAGE_ON_SCREEN_STATE_CHANGE, 1166 intent.getAction()).sendToTarget(); 1167 } 1168 1169 private void handleOnScreenStateChange(String action) { 1170 if (mMagnificationController.isMagnifying() 1171 && isScreenMagnificationAutoUpdateEnabled(mContext)) { 1172 mMagnificationController.reset(false); 1173 } 1174 } 1175 } 1176 } 1177