Files
frameworks_base/services/java/com/android/server/accessibility/ScreenMagnifier.java
Svetoslav Ganov 1cf70bbf96 Screen magnification - feature - framework.
This change is the initial check in of the screen magnification
feature. This feature enables magnification of the screen via
global gestures (assuming it has been enabled from settings)
to allow a low vision user to efficiently use an Android device.

Interaction model:

1. Triple tap toggles permanent screen magnification which is magnifying
   the area around the location of the triple tap. One can think of the
   location of the triple tap as the center of the magnified viewport.
   For example, a triple tap when not magnified would magnify the screen
   and leave it in a magnified state. A triple tapping when magnified would
   clear magnification and leave the screen in a not magnified state.

2. Triple tap and hold would magnify the screen if not magnified and enable
   viewport dragging mode until the finger goes up. One can think of this
   mode as a way to move the magnified viewport since the area around the
   moving finger will be magnified to fit the screen. For example, if the
   screen was not magnified and the user triple taps and holds the screen
   would magnify and the viewport will follow the user's finger. When the
   finger goes up the screen will clear zoom out. If the same user interaction
   is performed when the screen is magnified, the viewport movement will
   be the same but when the finger goes up the screen will stay magnified.
   In other words, the initial magnified state is sticky.

3. Pinching with any number of additional fingers when viewport dragging
   is enabled, i.e. the user triple tapped and holds, would adjust the
   magnification scale which will become the current default magnification
   scale. The next time the user magnifies the same magnification scale
   would be used.

4. When in a permanent magnified state the user can use two or more fingers
   to pan the viewport. Note that in this mode the content is panned as
   opposed to the viewport dragging mode in which the viewport is moved.

5. When in a permanent magnified state the user can use three or more
   fingers to change the magnification scale which will become the current
   default magnification scale. The next time the user magnifies the same
   magnification scale would be used.

6. The magnification scale will be persisted in settings and in the cloud.

Note: Since two fingers are used to pan the content in a permanently magnified
   state no other two finger gestures in touch exploration or applications
   will work unless the uses zooms out to normal state where all gestures
   works as expected. This is an intentional tradeoff to allow efficient
   panning since in a permanently magnified state this would be the dominant
   action to be performed.

Design:

1. The window manager exposes APIs for setting accessibility transformation
   which is a scale and offsets for X and Y axis. The window manager queries
   the window policy for which windows will not be magnified. For example,
   the IME windows and the navigation bar are not magnified including windows
   that are attached to them.

2. The accessibility features such a screen magnification and touch
   exploration are now impemented as a sequence of transformations on the
   event stream. The accessibility manager service may request each
   of these features or both. The behavior of the features is not changed
   based on the fact that another one is enabled.

3. The screen magnifier keeps a viewport of the content that is magnified
   which is surrounded by a glow in a magnified state. Interactions outside
   of the viewport are delegated directly to the application without
   interpretation. For example, a triple tap on the letter 'a' of the IME
   would type three letters instead of toggling magnified state. The viewport
   is updated on screen rotation and on window transitions. For example,
   when the IME pops up the viewport shrinks.

4. The glow around the viewport is implemented as a special type of window
   that does not take input focus, cannot be touched, is laid out in the
   screen coordiates with width and height matching these of the screen.
   When the magnified region changes the root view of the window draws the
   hightlight but the size of the window does not change - unless a rotation
   happens. All changes in the viewport size or showing or hiding it are
   animated.

5. The viewport is encapsulated in a class that knows how to show,
   hide, and resize the viewport - potentially animating that.
   This class uses the new animation framework for animations.

6. The magnification is handled by a magnification controller that
   keeps track of the current trnasformation to be applied to the screen
   content and the desired such. If these two are not the same it is
   responsibility of the magnification controller to reconcile them by
   potentially animating the transition from one to the other.

7. A dipslay content observer wathces for winodw transitions, screen
   rotations, and when a rectange on the screen has been reqeusted. This
   class is responsible for handling interesting state changes such
   as changing the viewport bounds on IME pop up or screen rotation,
   panning the content to make a requested rectangle visible on the
   screen, etc.

8. To implement viewport updates the window manger was updated with APIs
   to watch for window transitions and when a rectangle has been requested
   on the screen. These APIs are protected by a signature level permission.
   Also a parcelable and poolable window info class has been added with
   APIs for getting the window info given the window token. This enables
   getting some useful information about a window. There APIs are also
   signature protected.

bug:6795382

Change-Id: Iec93da8bf6376beebbd4f5167ab7723dc7d9bd00
2012-09-06 18:56:17 -07:00

1755 lines
73 KiB
Java

/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.accessibility;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.ObjectAnimator;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.PointF;
import android.graphics.PorterDuff.Mode;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.provider.Settings;
import android.util.MathUtils;
import android.util.Property;
import android.util.Slog;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.Gravity;
import android.view.IDisplayContentChangeListener;
import android.view.IWindowManager;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.MotionEvent.PointerCoords;
import android.view.MotionEvent.PointerProperties;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.view.Surface;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowInfo;
import android.view.WindowManager;
import android.view.WindowManagerPolicy;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import com.android.internal.R;
import com.android.internal.os.SomeArgs;
import java.util.ArrayList;
/**
* This class handles the screen magnification when accessibility is enabled.
* The behavior is as follows:
*
* 1. Triple tap toggles permanent screen magnification which is magnifying
* the area around the location of the triple tap. One can think of the
* location of the triple tap as the center of the magnified viewport.
* For example, a triple tap when not magnified would magnify the screen
* and leave it in a magnified state. A triple tapping when magnified would
* clear magnification and leave the screen in a not magnified state.
*
* 2. Triple tap and hold would magnify the screen if not magnified and enable
* viewport dragging mode until the finger goes up. One can think of this
* mode as a way to move the magnified viewport since the area around the
* moving finger will be magnified to fit the screen. For example, if the
* screen was not magnified and the user triple taps and holds the screen
* would magnify and the viewport will follow the user's finger. When the
* finger goes up the screen will clear zoom out. If the same user interaction
* is performed when the screen is magnified, the viewport movement will
* be the same but when the finger goes up the screen will stay magnified.
* In other words, the initial magnified state is sticky.
*
* 3. Pinching with any number of additional fingers when viewport dragging
* is enabled, i.e. the user triple tapped and holds, would adjust the
* magnification scale which will become the current default magnification
* scale. The next time the user magnifies the same magnification scale
* would be used.
*
* 4. When in a permanent magnified state the user can use two or more fingers
* to pan the viewport. Note that in this mode the content is panned as
* opposed to the viewport dragging mode in which the viewport is moved.
*
* 5. When in a permanent magnified state the user can use three or more
* fingers to change the magnification scale which will become the current
* default magnification scale. The next time the user magnifies the same
* magnification scale would be used.
*
* 6. The magnification scale will be persisted in settings and in the cloud.
*/
public final class ScreenMagnifier implements EventStreamTransformation {
private static final boolean DEBUG_STATE_TRANSITIONS = false;
private static final boolean DEBUG_DETECTING = false;
private static final boolean DEBUG_TRANSFORMATION = false;
private static final boolean DEBUG_PANNING = false;
private static final boolean DEBUG_SCALING = false;
private static final boolean DEBUG_VIEWPORT_WINDOW = false;
private static final boolean DEBUG_WINDOW_TRANSITIONS = false;
private static final boolean DEBUG_ROTATION = false;
private static final boolean DEBUG_GESTURE_DETECTOR = false;
private static final boolean DEBUG_MAGNIFICATION_CONTROLLER = false;
private static final String LOG_TAG = ScreenMagnifier.class.getSimpleName();
private static final int STATE_DELEGATING = 1;
private static final int STATE_DETECTING = 2;
private static final int STATE_SCALING = 3;
private static final int STATE_VIEWPORT_DRAGGING = 4;
private static final int STATE_PANNING = 5;
private static final int STATE_DECIDE_PAN_OR_SCALE = 6;
private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f;
private static final int DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE = 1;
private static final float DEFAULT_WINDOW_ANIMATION_SCALE = 1.0f;
private final IWindowManager mWindowManagerService = IWindowManager.Stub.asInterface(
ServiceManager.getService("window"));
private final WindowManager mWindowManager;
private final DisplayProvider mDisplayProvider;
private final DetectingStateHandler mDetectingStateHandler = new DetectingStateHandler();
private final GestureDetector mGestureDetector;
private final StateViewportDraggingHandler mStateViewportDraggingHandler =
new StateViewportDraggingHandler();
private final Interpolator mInterpolator = new DecelerateInterpolator(2.5f);
private final MagnificationController mMagnificationController;
private final DisplayContentObserver mDisplayContentObserver;
private final Viewport mViewport;
private final int mTapTimeSlop = ViewConfiguration.getTapTimeout();
private final int mMultiTapTimeSlop = ViewConfiguration.getDoubleTapTimeout();
private final int mTapDistanceSlop;
private final int mMultiTapDistanceSlop;
private final int mShortAnimationDuration;
private final int mLongAnimationDuration;
private final float mWindowAnimationScale;
private final Context mContext;
private EventStreamTransformation mNext;
private int mCurrentState;
private boolean mTranslationEnabledBeforePan;
private PointerCoords[] mTempPointerCoords;
private PointerProperties[] mTempPointerProperties;
public ScreenMagnifier(Context context) {
mContext = context;
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mShortAnimationDuration = context.getResources().getInteger(
com.android.internal.R.integer.config_shortAnimTime);
mLongAnimationDuration = context.getResources().getInteger(
com.android.internal.R.integer.config_longAnimTime);
mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
mWindowAnimationScale = Settings.System.getFloat(context.getContentResolver(),
Settings.System.WINDOW_ANIMATION_SCALE, DEFAULT_WINDOW_ANIMATION_SCALE);
mMagnificationController = new MagnificationController(mShortAnimationDuration);
mDisplayProvider = new DisplayProvider(context, mWindowManager);
mViewport = new Viewport(mContext, mWindowManager, mWindowManagerService,
mDisplayProvider, mInterpolator, mShortAnimationDuration);
mDisplayContentObserver = new DisplayContentObserver(mContext, mViewport,
mMagnificationController, mWindowManagerService, mDisplayProvider,
mLongAnimationDuration, mWindowAnimationScale);
mGestureDetector = new GestureDetector(context);
transitionToState(STATE_DETECTING);
}
@Override
public void onMotionEvent(MotionEvent event, int policyFlags) {
switch (mCurrentState) {
case STATE_DELEGATING: {
handleMotionEventStateDelegating(event, policyFlags);
} break;
case STATE_DETECTING: {
mDetectingStateHandler.onMotionEvent(event, policyFlags);
} break;
case STATE_VIEWPORT_DRAGGING: {
mStateViewportDraggingHandler.onMotionEvent(event, policyFlags);
} break;
case STATE_SCALING:
case STATE_PANNING:
case STATE_DECIDE_PAN_OR_SCALE: {
// Handled by the gesture detector. Since the detector
// needs all touch events to work properly we cannot
// call it only for these states.
} break;
default: {
throw new IllegalStateException("Unknown state: " + mCurrentState);
}
}
mGestureDetector.onMotionEvent(event);
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (mNext != null) {
mNext.onAccessibilityEvent(event);
}
}
@Override
public void setNext(EventStreamTransformation next) {
mNext = next;
}
@Override
public void clear() {
mCurrentState = STATE_DETECTING;
mDetectingStateHandler.clear();
mStateViewportDraggingHandler.clear();
mGestureDetector.clear();
if (mNext != null) {
mNext.clear();
}
}
@Override
public void onDestroy() {
mDisplayProvider.destroy();
mDisplayContentObserver.destroy();
}
private void handleMotionEventStateDelegating(MotionEvent event, int policyFlags) {
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
if (mDetectingStateHandler.mDelayedEventQueue == null) {
transitionToState(STATE_DETECTING);
}
}
if (mNext != null) {
// If the event is within the magnified portion of the screen we have
// to change its location to be where the user thinks he is poking the
// UI which may have been magnified and panned.
final float eventX = event.getX();
final float eventY = event.getY();
if (mMagnificationController.isMagnifying()
&& mViewport.getBounds().contains((int) eventX, (int) eventY)) {
final float scale = mMagnificationController.getScale();
final float scaledOffsetX = mMagnificationController.getScaledOffsetX();
final float scaledOffsetY = mMagnificationController.getScaledOffsetY();
final int pointerCount = event.getPointerCount();
PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount);
PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount);
for (int i = 0; i < pointerCount; i++) {
event.getPointerCoords(i, coords[i]);
coords[i].x = (coords[i].x - scaledOffsetX) / scale;
coords[i].y = (coords[i].y - scaledOffsetY) / scale;
event.getPointerProperties(i, properties[i]);
}
event = MotionEvent.obtain(event.getDownTime(),
event.getEventTime(), event.getAction(), pointerCount, properties,
coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(),
event.getFlags());
}
mNext.onMotionEvent(event, policyFlags);
}
}
private PointerCoords[] getTempPointerCoordsWithMinSize(int size) {
final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0;
if (oldSize < size) {
mTempPointerCoords = new PointerCoords[size];
}
for (int i = oldSize; i < size; i++) {
mTempPointerCoords[i] = new PointerCoords();
}
return mTempPointerCoords;
}
private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) {
final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length : 0;
if (oldSize < size) {
mTempPointerProperties = new PointerProperties[size];
}
for (int i = oldSize; i < size; i++) {
mTempPointerProperties[i] = new PointerProperties();
}
return mTempPointerProperties;
}
private void transitionToState(int state) {
if (DEBUG_STATE_TRANSITIONS) {
switch (state) {
case STATE_DELEGATING: {
Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING");
} break;
case STATE_DETECTING: {
Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING");
} break;
case STATE_VIEWPORT_DRAGGING: {
Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING");
} break;
case STATE_SCALING: {
Slog.i(LOG_TAG, "mCurrentState: STATE_SCALING");
} break;
case STATE_PANNING: {
Slog.i(LOG_TAG, "mCurrentState: STATE_PANNING");
} break;
case STATE_DECIDE_PAN_OR_SCALE: {
Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING_PAN_OR_SCALE");
} break;
default: {
throw new IllegalArgumentException("Unknown state: " + state);
}
}
}
mCurrentState = state;
}
private final class GestureDetector implements OnScaleGestureListener {
private static final float MIN_SCALE = 1.3f;
private static final float MAX_SCALE = 5.0f;
private static final float DETECT_SCALING_THRESHOLD = 0.25f;
private static final int DETECT_PANNING_THRESHOLD_DIP = 30;
private final float mScaledDetectPanningThreshold;
private final ScaleGestureDetector mScaleGestureDetector;
private final PointF mPrevFocus = new PointF(Float.NaN, Float.NaN);
private final PointF mInitialFocus = new PointF(Float.NaN, Float.NaN);
private float mCurrScale = Float.NaN;
private float mCurrScaleFactor = 1.0f;
private float mPrevScaleFactor = 1.0f;
private float mCurrPan;
private float mPrevPan;
private float mScaleFocusX = Float.NaN;
private float mScaleFocusY = Float.NaN;
public GestureDetector(Context context) {
final float density = context.getResources().getDisplayMetrics().density;
mScaledDetectPanningThreshold = DETECT_PANNING_THRESHOLD_DIP * density;
mScaleGestureDetector = new ScaleGestureDetector(context, this);
}
public void onMotionEvent(MotionEvent event) {
mScaleGestureDetector.onTouchEvent(event);
switch (mCurrentState) {
case STATE_DETECTING:
case STATE_DELEGATING:
case STATE_VIEWPORT_DRAGGING: {
return;
}
}
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
clear();
if (mCurrentState == STATE_SCALING) {
persistScale(mMagnificationController.getScale());
}
transitionToState(STATE_DETECTING);
}
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
switch (mCurrentState) {
case STATE_DETECTING:
case STATE_DELEGATING:
case STATE_VIEWPORT_DRAGGING: {
return true;
}
case STATE_DECIDE_PAN_OR_SCALE: {
mCurrScaleFactor = mScaleGestureDetector.getScaleFactor();
final float scaleDelta = Math.abs(1.0f - mCurrScaleFactor * mPrevScaleFactor);
if (DEBUG_GESTURE_DETECTOR) {
Slog.i(LOG_TAG, "scaleDelta: " + scaleDelta);
}
if (scaleDelta > DETECT_SCALING_THRESHOLD) {
performScale(detector, true);
transitionToState(STATE_SCALING);
return false;
}
mCurrPan = (float) MathUtils.dist(
mScaleGestureDetector.getFocusX(),
mScaleGestureDetector.getFocusY(),
mInitialFocus.x, mInitialFocus.y);
final float panDelta = mCurrPan + mPrevPan;
if (DEBUG_GESTURE_DETECTOR) {
Slog.i(LOG_TAG, "panDelta: " + panDelta);
}
if (panDelta > mScaledDetectPanningThreshold) {
transitionToState(STATE_PANNING);
performPan(detector, true);
return false;
}
} break;
case STATE_SCALING: {
performScale(detector, false);
} break;
case STATE_PANNING: {
performPan(detector, false);
} break;
}
return false;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
switch (mCurrentState) {
case STATE_DECIDE_PAN_OR_SCALE: {
mPrevScaleFactor *= mCurrScaleFactor;
mPrevPan += mCurrPan;
mPrevFocus.x = mInitialFocus.x = detector.getFocusX();
mPrevFocus.y = mInitialFocus.y = detector.getFocusY();
} break;
case STATE_SCALING: {
mPrevScaleFactor = 1.0f;
mCurrScale = Float.NaN;
} break;
case STATE_PANNING: {
mPrevPan += mCurrPan;
mPrevFocus.x = mInitialFocus.x = detector.getFocusX();
mPrevFocus.y = mInitialFocus.y = detector.getFocusY();
} break;
}
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
/* do nothing */
}
public void clear() {
mCurrScaleFactor = 1.0f;
mPrevScaleFactor = 1.0f;
mPrevPan = 0;
mCurrPan = 0;
mInitialFocus.set(Float.NaN, Float.NaN);
mPrevFocus.set(Float.NaN, Float.NaN);
mCurrScale = Float.NaN;
mScaleFocusX = Float.NaN;
mScaleFocusY = Float.NaN;
}
private void performPan(ScaleGestureDetector detector, boolean animate) {
if (Float.compare(mPrevFocus.x, Float.NaN) == 0
&& Float.compare(mPrevFocus.y, Float.NaN) == 0) {
mPrevFocus.set(detector.getFocusX(), detector.getFocusY());
return;
}
final float scale = mMagnificationController.getScale();
final float scrollX = (detector.getFocusX() - mPrevFocus.x) / scale;
final float scrollY = (detector.getFocusY() - mPrevFocus.y) / scale;
final float centerX = mMagnificationController.getMagnifiedRegionCenterX()
- scrollX;
final float centerY = mMagnificationController.getMagnifiedRegionCenterY()
- scrollY;
if (DEBUG_PANNING) {
Slog.i(LOG_TAG, "Panned content by scrollX: " + scrollX
+ " scrollY: " + scrollY);
}
mMagnificationController.setMagnifiedRegionCenter(centerX, centerY, animate);
mPrevFocus.set(detector.getFocusX(), detector.getFocusY());
}
private void performScale(ScaleGestureDetector detector, boolean animate) {
if (Float.compare(mCurrScale, Float.NaN) == 0) {
mCurrScale = mMagnificationController.getScale();
return;
}
final float totalScaleFactor = mPrevScaleFactor * detector.getScaleFactor();
final float newScale = mCurrScale * totalScaleFactor;
final float normalizedNewScale = Math.min(Math.max(newScale, MIN_SCALE),
MAX_SCALE);
if (DEBUG_SCALING) {
Slog.i(LOG_TAG, "normalizedNewScale: " + normalizedNewScale);
}
if (Float.compare(mScaleFocusX, Float.NaN) == 0
&& Float.compare(mScaleFocusY, Float.NaN) == 0) {
mScaleFocusX = detector.getFocusX();
mScaleFocusY = detector.getFocusY();
}
mMagnificationController.setScale(normalizedNewScale, mScaleFocusX,
mScaleFocusY, animate);
}
}
private final class StateViewportDraggingHandler {
private boolean mLastMoveOutsideMagnifiedRegion;
private void onMotionEvent(MotionEvent event, int policyFlags) {
final int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN: {
throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN");
}
case MotionEvent.ACTION_POINTER_DOWN: {
clear();
transitionToState(STATE_SCALING);
} break;
case MotionEvent.ACTION_MOVE: {
if (event.getPointerCount() != 1) {
throw new IllegalStateException("Should have one pointer down.");
}
final float eventX = event.getX();
final float eventY = event.getY();
if (mViewport.getBounds().contains((int) eventX, (int) eventY)) {
if (mLastMoveOutsideMagnifiedRegion) {
mLastMoveOutsideMagnifiedRegion = false;
mMagnificationController.setMagnifiedRegionCenter(eventX,
eventY, true);
} else {
mMagnificationController.setMagnifiedRegionCenter(eventX,
eventY, false);
}
} else {
mLastMoveOutsideMagnifiedRegion = true;
}
} break;
case MotionEvent.ACTION_UP: {
if (!mTranslationEnabledBeforePan) {
mMagnificationController.reset(true);
mViewport.setFrameShown(false, true);
}
clear();
transitionToState(STATE_DETECTING);
} break;
case MotionEvent.ACTION_POINTER_UP: {
throw new IllegalArgumentException("Unexpected event type: ACTION_POINTER_UP");
}
}
}
public void clear() {
mLastMoveOutsideMagnifiedRegion = false;
}
}
private final class DetectingStateHandler {
private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1;
private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2;
private static final int ACTION_TAP_COUNT = 3;
private MotionEventInfo mDelayedEventQueue;
private MotionEvent mLastDownEvent;
private MotionEvent mLastTapUpEvent;
private int mTapCount;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message message) {
final int type = message.what;
switch (type) {
case MESSAGE_ON_ACTION_TAP_AND_HOLD: {
MotionEvent event = (MotionEvent) message.obj;
final int policyFlags = message.arg1;
onActionTapAndHold(event, policyFlags);
} break;
case MESSAGE_TRANSITION_TO_DELEGATING_STATE: {
transitionToState(STATE_DELEGATING);
sendDelayedMotionEvents();
clear();
} break;
default: {
throw new IllegalArgumentException("Unknown message type: " + type);
}
}
}
};
public void onMotionEvent(MotionEvent event, int policyFlags) {
cacheDelayedMotionEvent(event, policyFlags);
final int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN: {
mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
if (!mViewport.getBounds().contains((int) event.getX(),
(int) event.getY())) {
transitionToDelegatingStateAndClear();
return;
}
if (mTapCount == ACTION_TAP_COUNT - 1 && mLastDownEvent != null
&& GestureUtils.isMultiTap(mLastDownEvent, event,
mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD,
policyFlags, 0, event);
mHandler.sendMessageDelayed(message,
ViewConfiguration.getLongPressTimeout());
} else if (mTapCount < ACTION_TAP_COUNT) {
Message message = mHandler.obtainMessage(
MESSAGE_TRANSITION_TO_DELEGATING_STATE);
mHandler.sendMessageDelayed(message, mTapTimeSlop + mMultiTapDistanceSlop);
}
clearLastDownEvent();
mLastDownEvent = MotionEvent.obtain(event);
} break;
case MotionEvent.ACTION_POINTER_DOWN: {
if (mMagnificationController.isMagnifying()) {
transitionToState(STATE_DECIDE_PAN_OR_SCALE);
clear();
} else {
transitionToDelegatingStateAndClear();
}
} break;
case MotionEvent.ACTION_MOVE: {
if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) {
final double distance = GestureUtils.computeDistance(mLastDownEvent,
event, 0);
if (Math.abs(distance) > mTapDistanceSlop) {
transitionToDelegatingStateAndClear();
}
}
} break;
case MotionEvent.ACTION_UP: {
if (mLastDownEvent == null) {
return;
}
mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
if (!mViewport.getBounds().contains((int) event.getX(), (int) event.getY())) {
transitionToDelegatingStateAndClear();
return;
}
if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop,
mTapDistanceSlop, 0)) {
transitionToDelegatingStateAndClear();
return;
}
if (mLastTapUpEvent != null && !GestureUtils.isMultiTap(mLastTapUpEvent,
event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
transitionToDelegatingStateAndClear();
return;
}
mTapCount++;
if (DEBUG_DETECTING) {
Slog.i(LOG_TAG, "Tap count:" + mTapCount);
}
if (mTapCount == ACTION_TAP_COUNT) {
clear();
onActionTap(event, policyFlags);
return;
}
clearLastTapUpEvent();
mLastTapUpEvent = MotionEvent.obtain(event);
} break;
case MotionEvent.ACTION_POINTER_UP: {
/* do nothing */
} break;
}
}
public void clear() {
mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
clearTapDetectionState();
clearDelayedMotionEvents();
}
private void clearTapDetectionState() {
mTapCount = 0;
clearLastTapUpEvent();
clearLastDownEvent();
}
private void clearLastTapUpEvent() {
if (mLastTapUpEvent != null) {
mLastTapUpEvent.recycle();
mLastTapUpEvent = null;
}
}
private void clearLastDownEvent() {
if (mLastDownEvent != null) {
mLastDownEvent.recycle();
mLastDownEvent = null;
}
}
private void cacheDelayedMotionEvent(MotionEvent event, int policyFlags) {
MotionEventInfo info = MotionEventInfo.obtain(event, policyFlags);
if (mDelayedEventQueue == null) {
mDelayedEventQueue = info;
} else {
MotionEventInfo tail = mDelayedEventQueue;
while (tail.mNext != null) {
tail = tail.mNext;
}
tail.mNext = info;
}
}
private void sendDelayedMotionEvents() {
while (mDelayedEventQueue != null) {
MotionEventInfo info = mDelayedEventQueue;
mDelayedEventQueue = info.mNext;
ScreenMagnifier.this.onMotionEvent(info.mEvent, info.mPolicyFlags);
info.recycle();
}
}
private void clearDelayedMotionEvents() {
while (mDelayedEventQueue != null) {
MotionEventInfo info = mDelayedEventQueue;
mDelayedEventQueue = info.mNext;
info.recycle();
}
}
private void transitionToDelegatingStateAndClear() {
transitionToState(STATE_DELEGATING);
sendDelayedMotionEvents();
clear();
}
private void onActionTap(MotionEvent up, int policyFlags) {
if (DEBUG_DETECTING) {
Slog.i(LOG_TAG, "onActionTap()");
}
if (!mMagnificationController.isMagnifying()) {
mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
up.getX(), up.getY(), true);
mViewport.setFrameShown(true, true);
} else {
mMagnificationController.reset(true);
mViewport.setFrameShown(false, true);
}
}
private void onActionTapAndHold(MotionEvent down, int policyFlags) {
if (DEBUG_DETECTING) {
Slog.i(LOG_TAG, "onActionTapAndHold()");
}
clear();
mTranslationEnabledBeforePan = mMagnificationController.isMagnifying();
mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
down.getX(), down.getY(), true);
mViewport.setFrameShown(true, true);
transitionToState(STATE_VIEWPORT_DRAGGING);
}
}
private void persistScale(final float scale) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
Settings.Secure.putFloat(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale);
return null;
}
}.execute();
}
private float getPersistedScale() {
return Settings.Secure.getFloat(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
DEFAULT_MAGNIFICATION_SCALE);
}
private static final class MotionEventInfo {
private static final int MAX_POOL_SIZE = 10;
private static final Object sLock = new Object();
private static MotionEventInfo sPool;
private static int sPoolSize;
private MotionEventInfo mNext;
private boolean mInPool;
public MotionEvent mEvent;
public int mPolicyFlags;
public static MotionEventInfo obtain(MotionEvent event, int policyFlags) {
synchronized (sLock) {
MotionEventInfo info;
if (sPoolSize > 0) {
sPoolSize--;
info = sPool;
sPool = info.mNext;
info.mNext = null;
info.mInPool = false;
} else {
info = new MotionEventInfo();
}
info.initialize(event, policyFlags);
return info;
}
}
private void initialize(MotionEvent event, int policyFlags) {
mEvent = MotionEvent.obtain(event);
mPolicyFlags = policyFlags;
}
public void recycle() {
synchronized (sLock) {
if (mInPool) {
throw new IllegalStateException("Already recycled.");
}
clear();
if (sPoolSize < MAX_POOL_SIZE) {
sPoolSize++;
mNext = sPool;
sPool = this;
mInPool = true;
}
}
}
private void clear() {
mEvent.recycle();
mEvent = null;
mPolicyFlags = 0;
}
}
private static final class DisplayContentObserver {
private static final int MESSAGE_SHOW_VIEWPORT_FRAME = 1;
private static final int MESSAGE_RECOMPUTE_VIEWPORT_BOUNDS = 2;
private static final int MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED = 3;
private static final int MESSAGE_ON_WINDOW_TRANSITION = 4;
private static final int MESSAGE_ON_ROTATION_CHANGED = 5;
private final Handler mHandler = new MyHandler();
private final Rect mTempRect = new Rect();
private final IDisplayContentChangeListener mDisplayContentChangeListener;
private final Context mContext;
private final Viewport mViewport;
private final MagnificationController mMagnificationController;
private final IWindowManager mWindowManagerService;
private final DisplayProvider mDisplayProvider;
private final long mLongAnimationDuration;
private final float mWindowAnimationScale;
public DisplayContentObserver(Context context, Viewport viewport,
MagnificationController magnificationController,
IWindowManager windowManagerService, DisplayProvider displayProvider,
long longAnimationDuration, float windowAnimationScale) {
mContext = context;
mViewport = viewport;
mMagnificationController = magnificationController;
mWindowManagerService = windowManagerService;
mDisplayProvider = displayProvider;
mLongAnimationDuration = longAnimationDuration;
mWindowAnimationScale = windowAnimationScale;
mDisplayContentChangeListener = new IDisplayContentChangeListener.Stub() {
@Override
public void onWindowTransition(int displayId, int transition, WindowInfo info) {
mHandler.obtainMessage(MESSAGE_ON_WINDOW_TRANSITION, transition, 0,
WindowInfo.obtain(info)).sendToTarget();
}
@Override
public void onRectangleOnScreenRequested(int dsiplayId, Rect rectangle,
boolean immediate) {
SomeArgs args = SomeArgs.obtain();
args.argi1 = rectangle.left;
args.argi2 = rectangle.top;
args.argi3 = rectangle.right;
args.argi4 = rectangle.bottom;
mHandler.obtainMessage(MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED, 0,
immediate ? 1 : 0, args).sendToTarget();
}
@Override
public void onRotationChanged(int rotation) throws RemoteException {
mHandler.obtainMessage(MESSAGE_ON_ROTATION_CHANGED, rotation, 0)
.sendToTarget();
}
};
try {
mWindowManagerService.addDisplayContentChangeListener(
mDisplayProvider.getDisplay().getDisplayId(),
mDisplayContentChangeListener);
} catch (RemoteException re) {
/* ignore */
}
}
public void destroy() {
try {
mWindowManagerService.removeDisplayContentChangeListener(
mDisplayProvider.getDisplay().getDisplayId(),
mDisplayContentChangeListener);
} catch (RemoteException re) {
/* ignore*/
}
}
private void handleOnRotationChanged(int rotation) {
if (DEBUG_ROTATION) {
Slog.i(LOG_TAG, "Rotation: " + rotationToString(rotation));
}
resetMagnificationIfNeeded();
mViewport.setFrameShown(false, false);
mViewport.rotationChanged();
mViewport.recomputeBounds(false);
if (mMagnificationController.isMagnifying()) {
final long delay = (long) (2 * mLongAnimationDuration * mWindowAnimationScale);
Message message = mHandler.obtainMessage(MESSAGE_SHOW_VIEWPORT_FRAME);
mHandler.sendMessageDelayed(message, delay);
}
}
private void handleOnWindowTransition(int transition, WindowInfo info) {
if (DEBUG_WINDOW_TRANSITIONS) {
Slog.i(LOG_TAG, "Window transitioning: "
+ windowTransitionToString(transition));
}
try {
final boolean magnifying = mMagnificationController.isMagnifying();
if (magnifying) {
switch (transition) {
case WindowManagerPolicy.TRANSIT_ACTIVITY_OPEN:
case WindowManagerPolicy.TRANSIT_TASK_OPEN:
case WindowManagerPolicy.TRANSIT_TASK_TO_FRONT:
case WindowManagerPolicy.TRANSIT_WALLPAPER_OPEN:
case WindowManagerPolicy.TRANSIT_WALLPAPER_CLOSE:
case WindowManagerPolicy.TRANSIT_WALLPAPER_INTRA_OPEN: {
resetMagnificationIfNeeded();
}
}
}
if (info.type == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
|| info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD
|| info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG) {
switch (transition) {
case WindowManagerPolicy.TRANSIT_ENTER:
case WindowManagerPolicy.TRANSIT_SHOW:
case WindowManagerPolicy.TRANSIT_EXIT:
case WindowManagerPolicy.TRANSIT_HIDE: {
mViewport.recomputeBounds(mMagnificationController.isMagnifying());
} break;
}
} else {
switch (transition) {
case WindowManagerPolicy.TRANSIT_ENTER:
case WindowManagerPolicy.TRANSIT_SHOW: {
if (!magnifying || !screenMagnificationAutoUpdateEnabled(mContext)) {
break;
}
final int type = info.type;
switch (type) {
// TODO: Are these all the windows we want to make
// visible when they appear on the screen?
// Do we need to take some of them out?
case WindowManager.LayoutParams.TYPE_APPLICATION_PANEL:
case WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA:
case WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL:
case WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG:
case WindowManager.LayoutParams.TYPE_SEARCH_BAR:
case WindowManager.LayoutParams.TYPE_PHONE:
case WindowManager.LayoutParams.TYPE_SYSTEM_ALERT:
case WindowManager.LayoutParams.TYPE_TOAST:
case WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY:
case WindowManager.LayoutParams.TYPE_PRIORITY_PHONE:
case WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG:
case WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG:
case WindowManager.LayoutParams.TYPE_SYSTEM_ERROR:
case WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY:
case WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL: {
Rect magnifiedRegionBounds = mMagnificationController
.getMagnifiedRegionBounds();
Rect touchableRegion = info.touchableRegion;
if (!magnifiedRegionBounds.intersect(touchableRegion)) {
ensureRectangleInMagnifiedRegionBounds(
magnifiedRegionBounds, touchableRegion);
}
} break;
} break;
}
}
}
} finally {
if (info != null) {
info.recycle();
}
}
}
private void handleOnRectangleOnScreenRequested(Rect rectangle, boolean immediate) {
if (!mMagnificationController.isMagnifying()) {
return;
}
Rect magnifiedRegionBounds = mMagnificationController.getMagnifiedRegionBounds();
if (magnifiedRegionBounds.contains(rectangle)) {
return;
}
ensureRectangleInMagnifiedRegionBounds(magnifiedRegionBounds, rectangle);
}
private void ensureRectangleInMagnifiedRegionBounds(Rect magnifiedRegionBounds,
Rect rectangle) {
if (!Rect.intersects(rectangle, mViewport.getBounds())) {
return;
}
final float scrollX;
final float scrollY;
if (rectangle.width() > magnifiedRegionBounds.width()) {
scrollX = rectangle.left - magnifiedRegionBounds.left;
} else if (rectangle.left < magnifiedRegionBounds.left) {
scrollX = rectangle.left - magnifiedRegionBounds.left;
} else if (rectangle.right > magnifiedRegionBounds.right) {
scrollX = rectangle.right - magnifiedRegionBounds.right;
} else {
scrollX = 0;
}
if (rectangle.height() > magnifiedRegionBounds.height()) {
scrollY = rectangle.top - magnifiedRegionBounds.top;
} else if (rectangle.top < magnifiedRegionBounds.top) {
scrollY = rectangle.top - magnifiedRegionBounds.top;
} else if (rectangle.bottom > magnifiedRegionBounds.bottom) {
scrollY = rectangle.bottom - magnifiedRegionBounds.bottom;
} else {
scrollY = 0;
}
final float viewportCenterX = mMagnificationController.getMagnifiedRegionCenterX()
+ scrollX;
final float viewportCenterY = mMagnificationController.getMagnifiedRegionCenterY()
+ scrollY;
mMagnificationController.setMagnifiedRegionCenter(viewportCenterX, viewportCenterY,
true);
}
private void resetMagnificationIfNeeded() {
if (mMagnificationController.isMagnifying()
&& screenMagnificationAutoUpdateEnabled(mContext)) {
mMagnificationController.reset(true);
mViewport.setFrameShown(false, true);
}
}
private boolean screenMagnificationAutoUpdateEnabled(Context context) {
return (Settings.Secure.getInt(context.getContentResolver(),
Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1);
}
private String windowTransitionToString(int transition) {
switch (transition) {
case WindowManagerPolicy.TRANSIT_UNSET: {
return "TRANSIT_UNSET";
}
case WindowManagerPolicy.TRANSIT_NONE: {
return "TRANSIT_NONE";
}
case WindowManagerPolicy.TRANSIT_ENTER: {
return "TRANSIT_ENTER";
}
case WindowManagerPolicy.TRANSIT_EXIT: {
return "TRANSIT_EXIT";
}
case WindowManagerPolicy.TRANSIT_SHOW: {
return "TRANSIT_SHOW";
}
case WindowManagerPolicy.TRANSIT_EXIT_MASK: {
return "TRANSIT_EXIT_MASK";
}
case WindowManagerPolicy.TRANSIT_PREVIEW_DONE: {
return "TRANSIT_PREVIEW_DONE";
}
case WindowManagerPolicy.TRANSIT_ACTIVITY_OPEN: {
return "TRANSIT_ACTIVITY_OPEN";
}
case WindowManagerPolicy.TRANSIT_ACTIVITY_CLOSE: {
return "TRANSIT_ACTIVITY_CLOSE";
}
case WindowManagerPolicy.TRANSIT_TASK_OPEN: {
return "TRANSIT_TASK_OPEN";
}
case WindowManagerPolicy.TRANSIT_TASK_CLOSE: {
return "TRANSIT_TASK_CLOSE";
}
case WindowManagerPolicy.TRANSIT_TASK_TO_FRONT: {
return "TRANSIT_TASK_TO_FRONT";
}
case WindowManagerPolicy.TRANSIT_TASK_TO_BACK: {
return "TRANSIT_TASK_TO_BACK";
}
case WindowManagerPolicy.TRANSIT_WALLPAPER_CLOSE: {
return "TRANSIT_WALLPAPER_CLOSE";
}
case WindowManagerPolicy.TRANSIT_WALLPAPER_OPEN: {
return "TRANSIT_WALLPAPER_OPEN";
}
case WindowManagerPolicy.TRANSIT_WALLPAPER_INTRA_OPEN: {
return "TRANSIT_WALLPAPER_INTRA_OPEN";
}
case WindowManagerPolicy.TRANSIT_WALLPAPER_INTRA_CLOSE: {
return "TRANSIT_WALLPAPER_INTRA_CLOSE";
}
default: {
return "<UNKNOWN>";
}
}
}
private String rotationToString(int rotation) {
switch (rotation) {
case Surface.ROTATION_0: {
return "ROTATION_0";
}
case Surface.ROTATION_90: {
return "ROATATION_90";
}
case Surface.ROTATION_180: {
return "ROATATION_180";
}
case Surface.ROTATION_270: {
return "ROATATION_270";
}
default: {
throw new IllegalArgumentException("Invalid rotation: "
+ rotation);
}
}
}
private final class MyHandler extends Handler {
@Override
public void handleMessage(Message message) {
final int action = message.what;
switch (action) {
case MESSAGE_SHOW_VIEWPORT_FRAME: {
mViewport.setFrameShown(true, true);
} break;
case MESSAGE_RECOMPUTE_VIEWPORT_BOUNDS: {
final boolean animate = message.arg1 == 1;
mViewport.recomputeBounds(animate);
} break;
case MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED: {
SomeArgs args = (SomeArgs) message.obj;
try {
mTempRect.set(args.argi1, args.argi2, args.argi3, args.argi4);
final boolean immediate = (message.arg1 == 1);
handleOnRectangleOnScreenRequested(mTempRect, immediate);
} finally {
args.recycle();
}
} break;
case MESSAGE_ON_WINDOW_TRANSITION: {
final int transition = message.arg1;
WindowInfo info = (WindowInfo) message.obj;
handleOnWindowTransition(transition, info);
} break;
case MESSAGE_ON_ROTATION_CHANGED: {
final int rotation = message.arg1;
handleOnRotationChanged(rotation);
} break;
default: {
throw new IllegalArgumentException("Unknown message: " + action);
}
}
}
}
}
private final class MagnificationController {
private static final String PROPERTY_NAME_ACCESSIBILITY_TRANSFORMATION =
"accessibilityTransformation";
private final MagnificationSpec mSentMagnificationSpec = new MagnificationSpec();
private final MagnificationSpec mCurrentMagnificationSpec = new MagnificationSpec();
private final Rect mTempRect = new Rect();
private final ValueAnimator mTransformationAnimator;
public MagnificationController(int animationDuration) {
Property<MagnificationController, MagnificationSpec> property =
Property.of(MagnificationController.class, MagnificationSpec.class,
PROPERTY_NAME_ACCESSIBILITY_TRANSFORMATION);
TypeEvaluator<MagnificationSpec> evaluator = new TypeEvaluator<MagnificationSpec>() {
private final MagnificationSpec mTempTransformationSpec = new MagnificationSpec();
@Override
public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec,
MagnificationSpec toSpec) {
MagnificationSpec result = mTempTransformationSpec;
result.mScale = fromSpec.mScale
+ (toSpec.mScale - fromSpec.mScale) * fraction;
result.mMagnifiedRegionCenterX = fromSpec.mMagnifiedRegionCenterX
+ (toSpec.mMagnifiedRegionCenterX - fromSpec.mMagnifiedRegionCenterX)
* fraction;
result.mMagnifiedRegionCenterY = fromSpec.mMagnifiedRegionCenterY
+ (toSpec.mMagnifiedRegionCenterY - fromSpec.mMagnifiedRegionCenterY)
* fraction;
result.mScaledOffsetX = fromSpec.mScaledOffsetX
+ (toSpec.mScaledOffsetX - fromSpec.mScaledOffsetX)
* fraction;
result.mScaledOffsetY = fromSpec.mScaledOffsetY
+ (toSpec.mScaledOffsetY - fromSpec.mScaledOffsetY)
* fraction;
return result;
}
};
mTransformationAnimator = ObjectAnimator.ofObject(this, property,
evaluator, mSentMagnificationSpec, mCurrentMagnificationSpec);
mTransformationAnimator.setDuration((long) (animationDuration));
mTransformationAnimator.setInterpolator(mInterpolator);
}
public boolean isMagnifying() {
return mCurrentMagnificationSpec.mScale > 1.0f;
}
public void reset(boolean animate) {
if (mTransformationAnimator.isRunning()) {
mTransformationAnimator.cancel();
}
mCurrentMagnificationSpec.reset();
if (animate) {
animateAccessibilityTranformation(mSentMagnificationSpec,
mCurrentMagnificationSpec);
} else {
setAccessibilityTransformation(mCurrentMagnificationSpec);
}
}
public Rect getMagnifiedRegionBounds() {
mTempRect.set(mViewport.getBounds());
mTempRect.offset((int) -mCurrentMagnificationSpec.mScaledOffsetX,
(int) -mCurrentMagnificationSpec.mScaledOffsetY);
mTempRect.scale(1.0f / mCurrentMagnificationSpec.mScale);
return mTempRect;
}
public float getScale() {
return mCurrentMagnificationSpec.mScale;
}
public float getMagnifiedRegionCenterX() {
return mCurrentMagnificationSpec.mMagnifiedRegionCenterX;
}
public float getMagnifiedRegionCenterY() {
return mCurrentMagnificationSpec.mMagnifiedRegionCenterY;
}
public float getScaledOffsetX() {
return mCurrentMagnificationSpec.mScaledOffsetX;
}
public float getScaledOffsetY() {
return mCurrentMagnificationSpec.mScaledOffsetY;
}
public void setScale(float scale, float pivotX, float pivotY, boolean animate) {
MagnificationSpec spec = mCurrentMagnificationSpec;
final float oldScale = spec.mScale;
final float oldCenterX = spec.mMagnifiedRegionCenterX;
final float oldCenterY = spec.mMagnifiedRegionCenterY;
final float normPivotX = (-spec.mScaledOffsetX + pivotX) / oldScale;
final float normPivotY = (-spec.mScaledOffsetY + pivotY) / oldScale;
final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale);
final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale);
final float centerX = normPivotX + offsetX;
final float centerY = normPivotY + offsetY;
setScaleAndMagnifiedRegionCenter(scale, centerX, centerY, animate);
}
public void setMagnifiedRegionCenter(float centerX, float centerY, boolean animate) {
setScaleAndMagnifiedRegionCenter(mCurrentMagnificationSpec.mScale, centerX, centerY,
animate);
}
public void setScaleAndMagnifiedRegionCenter(float scale, float centerX, float centerY,
boolean animate) {
if (Float.compare(mCurrentMagnificationSpec.mScale, scale) == 0
&& Float.compare(mCurrentMagnificationSpec.mMagnifiedRegionCenterX,
centerX) == 0
&& Float.compare(mCurrentMagnificationSpec.mMagnifiedRegionCenterY,
centerY) == 0) {
return;
}
if (mTransformationAnimator.isRunning()) {
mTransformationAnimator.cancel();
}
if (DEBUG_MAGNIFICATION_CONTROLLER) {
Slog.i(LOG_TAG, "scale: " + scale + " centerX: " + centerX
+ " centerY: " + centerY);
}
mCurrentMagnificationSpec.initialize(scale, centerX, centerY);
if (animate) {
animateAccessibilityTranformation(mSentMagnificationSpec,
mCurrentMagnificationSpec);
} else {
setAccessibilityTransformation(mCurrentMagnificationSpec);
}
}
private void animateAccessibilityTranformation(MagnificationSpec fromSpec,
MagnificationSpec toSpec) {
mTransformationAnimator.setObjectValues(fromSpec, toSpec);
mTransformationAnimator.start();
}
@SuppressWarnings("unused")
// Called from an animator.
public MagnificationSpec getAccessibilityTransformation() {
return mSentMagnificationSpec;
}
public void setAccessibilityTransformation(MagnificationSpec transformation) {
if (DEBUG_TRANSFORMATION) {
Slog.i(LOG_TAG, "Transformation scale: " + transformation.mScale
+ " offsetX: " + transformation.mScaledOffsetX
+ " offsetY: " + transformation.mScaledOffsetY);
}
try {
mSentMagnificationSpec.updateFrom(transformation);
mWindowManagerService.magnifyDisplay(mDisplayProvider.getDisplay().getDisplayId(),
transformation.mScale, transformation.mScaledOffsetX,
transformation.mScaledOffsetY);
} catch (RemoteException re) {
/* ignore */
}
}
private class MagnificationSpec {
private static final float DEFAULT_SCALE = 1.0f;
public float mScale = DEFAULT_SCALE;
public float mMagnifiedRegionCenterX;
public float mMagnifiedRegionCenterY;
public float mScaledOffsetX;
public float mScaledOffsetY;
public void initialize(float scale, float magnifiedRegionCenterX,
float magnifiedRegionCenterY) {
mScale = scale;
final int viewportWidth = mViewport.getBounds().width();
final int viewportHeight = mViewport.getBounds().height();
final float minMagnifiedRegionCenterX = (viewportWidth / 2) / scale;
final float minMagnifiedRegionCenterY = (viewportHeight / 2) / scale;
final float maxMagnifiedRegionCenterX = viewportWidth - minMagnifiedRegionCenterX;
final float maxMagnifiedRegionCenterY = viewportHeight - minMagnifiedRegionCenterY;
mMagnifiedRegionCenterX = Math.min(Math.max(magnifiedRegionCenterX,
minMagnifiedRegionCenterX), maxMagnifiedRegionCenterX);
mMagnifiedRegionCenterY = Math.min(Math.max(magnifiedRegionCenterY,
minMagnifiedRegionCenterY), maxMagnifiedRegionCenterY);
mScaledOffsetX = -(mMagnifiedRegionCenterX * scale - viewportWidth / 2);
mScaledOffsetY = -(mMagnifiedRegionCenterY * scale - viewportHeight / 2);
}
public void updateFrom(MagnificationSpec other) {
mScale = other.mScale;
mMagnifiedRegionCenterX = other.mMagnifiedRegionCenterX;
mMagnifiedRegionCenterY = other.mMagnifiedRegionCenterY;
mScaledOffsetX = other.mScaledOffsetX;
mScaledOffsetY = other.mScaledOffsetY;
}
public void reset() {
mScale = DEFAULT_SCALE;
mMagnifiedRegionCenterX = 0;
mMagnifiedRegionCenterY = 0;
mScaledOffsetX = 0;
mScaledOffsetY = 0;
}
}
}
private static final class Viewport {
private static final String PROPERTY_NAME_ALPHA = "alpha";
private static final String PROPERTY_NAME_BOUNDS = "bounds";
private static final int MIN_ALPHA = 0;
private static final int MAX_ALPHA = 255;
private final ArrayList<WindowInfo> mTempWindowInfoList = new ArrayList<WindowInfo>();
private final Rect mTempRect = new Rect();
private final IWindowManager mWindowManagerService;
private final DisplayProvider mDisplayProvider;
private final ViewportWindow mViewportFrame;
private final ValueAnimator mResizeFrameAnimator;
private final ValueAnimator mShowHideFrameAnimator;
public Viewport(Context context, WindowManager windowManager,
IWindowManager windowManagerService, DisplayProvider displayInfoProvider,
Interpolator animationInterpolator, long animationDuration) {
mWindowManagerService = windowManagerService;
mDisplayProvider = displayInfoProvider;
mViewportFrame = new ViewportWindow(context, windowManager, displayInfoProvider);
mShowHideFrameAnimator = ObjectAnimator.ofInt(mViewportFrame, PROPERTY_NAME_ALPHA,
MIN_ALPHA, MAX_ALPHA);
mShowHideFrameAnimator.setInterpolator(animationInterpolator);
mShowHideFrameAnimator.setDuration(animationDuration);
mShowHideFrameAnimator.addListener(new AnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
if (mShowHideFrameAnimator.getAnimatedValue().equals(MIN_ALPHA)) {
mViewportFrame.hide();
}
}
@Override
public void onAnimationStart(Animator animation) {
/* do nothing - stub */
}
@Override
public void onAnimationCancel(Animator animation) {
/* do nothing - stub */
}
@Override
public void onAnimationRepeat(Animator animation) {
/* do nothing - stub */
}
});
Property<ViewportWindow, Rect> property = Property.of(ViewportWindow.class,
Rect.class, PROPERTY_NAME_BOUNDS);
TypeEvaluator<Rect> evaluator = new TypeEvaluator<Rect>() {
private final Rect mReusableResultRect = new Rect();
@Override
public Rect evaluate(float fraction, Rect fromFrame, Rect toFrame) {
Rect result = mReusableResultRect;
result.left = (int) (fromFrame.left
+ (toFrame.left - fromFrame.left) * fraction);
result.top = (int) (fromFrame.top
+ (toFrame.top - fromFrame.top) * fraction);
result.right = (int) (fromFrame.right
+ (toFrame.right - fromFrame.right) * fraction);
result.bottom = (int) (fromFrame.bottom
+ (toFrame.bottom - fromFrame.bottom) * fraction);
return result;
}
};
mResizeFrameAnimator = ObjectAnimator.ofObject(mViewportFrame, property,
evaluator, mViewportFrame.mBounds, mViewportFrame.mBounds);
mResizeFrameAnimator.setDuration((long) (animationDuration));
mResizeFrameAnimator.setInterpolator(animationInterpolator);
recomputeBounds(false);
}
public void recomputeBounds(boolean animate) {
Rect frame = mTempRect;
frame.set(0, 0, mDisplayProvider.getDisplayInfo().logicalWidth,
mDisplayProvider.getDisplayInfo().logicalHeight);
ArrayList<WindowInfo> infos = mTempWindowInfoList;
infos.clear();
try {
mWindowManagerService.getVisibleWindowsForDisplay(
mDisplayProvider.getDisplay().getDisplayId(), infos);
final int windowCount = infos.size();
for (int i = 0; i < windowCount; i++) {
WindowInfo info = infos.get(i);
if (info.type == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
|| info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD
|| info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG) {
subtract(frame, info.touchableRegion);
}
info.recycle();
}
} catch (RemoteException re) {
/* ignore */
} finally {
infos.clear();
}
resize(frame, animate);
}
public void rotationChanged() {
mViewportFrame.rotationChanged();
}
public Rect getBounds() {
return mViewportFrame.getBounds();
}
public void setFrameShown(boolean shown, boolean animate) {
if (mViewportFrame.isShown() == shown) {
return;
}
if (animate) {
if (mShowHideFrameAnimator.isRunning()) {
mShowHideFrameAnimator.reverse();
} else {
if (shown) {
mViewportFrame.show();
mShowHideFrameAnimator.start();
} else {
mShowHideFrameAnimator.reverse();
}
}
} else {
mShowHideFrameAnimator.cancel();
if (shown) {
mViewportFrame.show();
} else {
mViewportFrame.hide();
}
}
}
private void resize(Rect bounds, boolean animate) {
if (mViewportFrame.getBounds().equals(bounds)) {
return;
}
if (animate) {
if (mResizeFrameAnimator.isRunning()) {
mResizeFrameAnimator.cancel();
}
mResizeFrameAnimator.setObjectValues(mViewportFrame.mBounds, bounds);
mResizeFrameAnimator.start();
} else {
mViewportFrame.setBounds(bounds);
}
}
private boolean subtract(Rect lhs, Rect rhs) {
if (lhs.right < rhs.left || lhs.left > rhs.right
|| lhs.bottom < rhs.top || lhs.top > rhs.bottom) {
return false;
}
if (lhs.left < rhs.left) {
lhs.right = rhs.left;
}
if (lhs.top < rhs.top) {
lhs.bottom = rhs.top;
}
if (lhs.right > rhs.right) {
lhs.left = rhs.right;
}
if (lhs.bottom > rhs.bottom) {
lhs.top = rhs.bottom;
}
return true;
}
private static final class ViewportWindow {
private static final String WINDOW_TITLE = "Magnification Overlay";
private final WindowManager mWindowManager;
private final DisplayProvider mDisplayProvider;
private final ContentView mWindowContent;
private final WindowManager.LayoutParams mWindowParams;
private final Rect mBounds = new Rect();
private boolean mShown;
private int mAlpha;
public ViewportWindow(Context context, WindowManager windowManager,
DisplayProvider displayProvider) {
mWindowManager = windowManager;
mDisplayProvider = displayProvider;
ViewGroup.LayoutParams contentParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mWindowContent = new ContentView(context);
mWindowContent.setLayoutParams(contentParams);
mWindowContent.setBackgroundColor(R.color.transparent);
mWindowParams = new WindowManager.LayoutParams(
WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY);
mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
mWindowParams.setTitle(WINDOW_TITLE);
mWindowParams.gravity = Gravity.CENTER;
mWindowParams.width = displayProvider.getDisplayInfo().logicalWidth;
mWindowParams.height = displayProvider.getDisplayInfo().logicalHeight;
mWindowParams.format = PixelFormat.TRANSLUCENT;
}
public boolean isShown() {
return mShown;
}
public void show() {
if (mShown) {
return;
}
mShown = true;
mWindowManager.addView(mWindowContent, mWindowParams);
if (DEBUG_VIEWPORT_WINDOW) {
Slog.i(LOG_TAG, "ViewportWindow shown.");
}
}
public void hide() {
if (!mShown) {
return;
}
mShown = false;
mWindowManager.removeView(mWindowContent);
if (DEBUG_VIEWPORT_WINDOW) {
Slog.i(LOG_TAG, "ViewportWindow hidden.");
}
}
@SuppressWarnings("unused")
// Called reflectively from an animator.
public int getAlpha() {
return mAlpha;
}
@SuppressWarnings("unused")
// Called reflectively from an animator.
public void setAlpha(int alpha) {
if (mAlpha == alpha) {
return;
}
mAlpha = alpha;
if (mShown) {
mWindowContent.invalidate();
}
if (DEBUG_VIEWPORT_WINDOW) {
Slog.i(LOG_TAG, "ViewportFrame set alpha: " + alpha);
}
}
public Rect getBounds() {
return mBounds;
}
public void rotationChanged() {
mWindowParams.width = mDisplayProvider.getDisplayInfo().logicalWidth;
mWindowParams.height = mDisplayProvider.getDisplayInfo().logicalHeight;
if (mShown) {
mWindowManager.updateViewLayout(mWindowContent, mWindowParams);
}
}
public void setBounds(Rect bounds) {
if (mBounds.equals(bounds)) {
return;
}
mBounds.set(bounds);
if (mShown) {
mWindowContent.invalidate();
}
if (DEBUG_VIEWPORT_WINDOW) {
Slog.i(LOG_TAG, "ViewportFrame set bounds: " + bounds);
}
}
private final class ContentView extends View {
private final Drawable mHighlightFrame;
public ContentView(Context context) {
super(context);
mHighlightFrame = context.getResources().getDrawable(
R.drawable.magnified_region_frame);
}
@Override
public void onDraw(Canvas canvas) {
canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
mHighlightFrame.setBounds(mBounds);
mHighlightFrame.setAlpha(mAlpha);
mHighlightFrame.draw(canvas);
}
}
}
}
private static class DisplayProvider implements DisplayListener {
private final WindowManager mWindowManager;
private final DisplayManager mDisplayManager;
private final Display mDefaultDisplay;
private final DisplayInfo mDefaultDisplayInfo = new DisplayInfo();
public DisplayProvider(Context context, WindowManager windowManager) {
mWindowManager = windowManager;
mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
mDefaultDisplay = mWindowManager.getDefaultDisplay();
mDisplayManager.registerDisplayListener(this, null);
updateDisplayInfo();
}
public DisplayInfo getDisplayInfo() {
return mDefaultDisplayInfo;
}
public Display getDisplay() {
return mDefaultDisplay;
}
private void updateDisplayInfo() {
if (!mDefaultDisplay.getDisplayInfo(mDefaultDisplayInfo)) {
Slog.e(LOG_TAG, "Default display is not valid.");
}
}
public void destroy() {
mDisplayManager.unregisterDisplayListener(this);
}
@Override
public void onDisplayAdded(int displayId) {
/* do noting */
}
@Override
public void onDisplayRemoved(int displayId) {
// Having no default display
}
@Override
public void onDisplayChanged(int displayId) {
updateDisplayInfo();
}
}
}