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