blob: 48781ac2acbfc53dd0f03eeaf156b86f380464ca [file] [log] [blame]
/*
* 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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PixelFormat;
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.Property;
import android.util.Slog;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.Gravity;
import android.view.IDisplayContentChangeListener;
import android.view.IWindowManager;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
import android.view.MotionEvent.PointerProperties;
import android.view.ScaleGestureDetector;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.view.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;
import java.util.Collections;
import java.util.Comparator;
/**
* 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_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_VIEWPORT_DRAGGING = 3;
private static final int STATE_MAGNIFIED_INTERACTION = 4;
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 static final int MULTI_TAP_TIME_SLOP_ADJUSTMENT = 50;
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 MagnifiedContentInteractonStateHandler mMagnifiedContentInteractonStateHandler;
private final StateViewportDraggingHandler mStateViewportDraggingHandler =
new StateViewportDraggingHandler();
private final Interpolator mInterpolator = new DecelerateInterpolator(2.5f);
private final MagnificationController mMagnificationController;
private final DisplayContentObserver mDisplayContentObserver;
private final ScreenStateObserver mScreenStateObserver;
private final Viewport mViewport;
private final int mTapTimeSlop = ViewConfiguration.getTapTimeout();
private final int mMultiTapTimeSlop =
ViewConfiguration.getDoubleTapTimeout() - MULTI_TAP_TIME_SLOP_ADJUSTMENT;
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 int mPreviousState;
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);
mScreenStateObserver = new ScreenStateObserver(mContext, mViewport,
mMagnificationController);
mMagnifiedContentInteractonStateHandler = new MagnifiedContentInteractonStateHandler(
context);
transitionToState(STATE_DETECTING);
}
@Override
public void onMotionEvent(MotionEvent event, int policyFlags) {
mMagnifiedContentInteractonStateHandler.onMotionEvent(event);
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_MAGNIFIED_INTERACTION: {
// mMagnifiedContentInteractonStateHandler handles events only
// if this is the current state since it uses ScaleGestureDetecotr
// and a GestureDetector which need well formed event stream.
} break;
default: {
throw new IllegalStateException("Unknown state: " + mCurrentState);
}
}
}
@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();
mMagnifiedContentInteractonStateHandler.clear();
if (mNext != null) {
mNext.clear();
}
}
@Override
public void onDestroy() {
mMagnificationController.setScaleAndMagnifiedRegionCenter(1.0f,
0, 0, true);
mViewport.setFrameShown(false, true);
mDisplayProvider.destroy();
mDisplayContentObserver.destroy();
mScreenStateObserver.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) {
PointerCoords[] oldTempPointerCoords = mTempPointerCoords;
mTempPointerCoords = new PointerCoords[size];
if (oldTempPointerCoords != null) {
System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize);
}
}
for (int i = oldSize; i < size; i++) {
mTempPointerCoords[i] = new PointerCoords();
}
return mTempPointerCoords;
}
private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) {
final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length : 0;
if (oldSize < size) {
PointerProperties[] oldTempPointerProperties = mTempPointerProperties;
mTempPointerProperties = new PointerProperties[size];
if (oldTempPointerProperties != null) {
System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, oldSize);
}
}
for (int i = oldSize; i < size; i++) {
mTempPointerProperties[i] = new PointerProperties();
}
return mTempPointerProperties;
}
private void transitionToState(int state) {
if (DEBUG_STATE_TRANSITIONS) {
switch (state) {
case STATE_DELEGATING: {
Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING");
} break;
case STATE_DETECTING: {
Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING");
} break;
case STATE_VIEWPORT_DRAGGING: {
Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING");
} break;
case STATE_MAGNIFIED_INTERACTION: {
Slog.i(LOG_TAG, "mCurrentState: STATE_MAGNIFIED_INTERACTION");
} break;
default: {
throw new IllegalArgumentException("Unknown state: " + state);
}
}
}
mPreviousState = mCurrentState;
mCurrentState = state;
}
private final class MagnifiedContentInteractonStateHandler
extends SimpleOnGestureListener implements OnScaleGestureListener {
private static final float MIN_SCALE = 1.3f;
private static final float MAX_SCALE = 5.0f;
private static final float SCALING_THRESHOLD = 0.3f;
private final ScaleGestureDetector mScaleGestureDetector;
private final GestureDetector mGestureDetector;
private float mInitialScaleFactor = -1;
private boolean mScaling;
public MagnifiedContentInteractonStateHandler(Context context) {
mScaleGestureDetector = new ScaleGestureDetector(context, this);
mGestureDetector = new GestureDetector(context, this);
}
public void onMotionEvent(MotionEvent event) {
mScaleGestureDetector.onTouchEvent(event);
mGestureDetector.onTouchEvent(event);
if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {
return;
}
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
clear();
final float scale = mMagnificationController.getScale();
if (scale != getPersistedScale()) {
persistScale(scale);
}
if (mPreviousState == STATE_VIEWPORT_DRAGGING) {
transitionToState(STATE_VIEWPORT_DRAGGING);
} else {
transitionToState(STATE_DETECTING);
}
}
}
@Override
public boolean onScroll(MotionEvent first, MotionEvent second, float distanceX,
float distanceY) {
if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {
return true;
}
final float scale = mMagnificationController.getScale();
final float scrollX = distanceX / scale;
final float scrollY = distanceY / 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, false);
return true;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (!mScaling) {
if (mInitialScaleFactor < 0) {
mInitialScaleFactor = detector.getScaleFactor();
} else {
final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor;
if (Math.abs(deltaScale) > SCALING_THRESHOLD) {
mScaling = true;
return true;
}
}
return false;
}
final float newScale = mMagnificationController.getScale()
* detector.getScaleFactor();
final float normalizedNewScale = Math.min(Math.max(newScale, MIN_SCALE), MAX_SCALE);
if (DEBUG_SCALING) {
Slog.i(LOG_TAG, "normalizedNewScale: " + normalizedNewScale);
}
mMagnificationController.setScale(normalizedNewScale, detector.getFocusX(),
detector.getFocusY(), false);
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return (mCurrentState == STATE_MAGNIFIED_INTERACTION);
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
clear();
}
private void clear() {
mInitialScaleFactor = -1;
mScaling = false;
}
}
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_MAGNIFIED_INTERACTION);
} break;
case MotionEvent.ACTION_MOVE: {
if (event.getPointerCount() != 1) {
throw new IllegalStateException("Should have one pointer down.");
}
final float eventX = event.getX();
final float eventY = event.getY();
if (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, mMultiTapTimeSlop);
}
clearLastDownEvent();
mLastDownEvent = MotionEvent.obtain(event);
} break;
case MotionEvent.ACTION_POINTER_DOWN: {
if (mMagnificationController.isMagnifying()) {
transitionToState(STATE_MAGNIFIED_INTERACTION);
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 boolean isScreenMagnificationAutoUpdateEnabled(Context context) {
return (Settings.Secure.getInt(context.getContentResolver(),
Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1);
}
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 ScreenStateObserver extends BroadcastReceiver {
private static final int MESSAGE_ON_SCREEN_STATE_CHANGE = 1;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message message) {
switch (message.what) {
case MESSAGE_ON_SCREEN_STATE_CHANGE: {
String action = (String) message.obj;
handleOnScreenStateChange(action);
} break;
}
}
};
private final Context mContext;
private final Viewport mViewport;
private final MagnificationController mMagnificationController;
public ScreenStateObserver(Context context, Viewport viewport,
MagnificationController magnificationController) {
mContext = context;
mViewport = viewport;
mMagnificationController = magnificationController;
mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF));
}
public void destroy() {
mContext.unregisterReceiver(this);
}
@Override
public void onReceive(Context context, Intent intent) {
mHandler.obtainMessage(MESSAGE_ON_SCREEN_STATE_CHANGE,
intent.getAction()).sendToTarget();
}
private void handleOnScreenStateChange(String action) {
if (action.equals(Intent.ACTION_SCREEN_OFF)
&& mMagnificationController.isMagnifying()
&& isScreenMagnificationAutoUpdateEnabled(mContext)) {
mMagnificationController.reset(false);
mViewport.setFrameShown(false, false);
}
}
}
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
|| info.type == WindowManager.LayoutParams.TYPE_KEYGUARD
|| info.type == WindowManager.LayoutParams.TYPE_KEYGUARD_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 || !isScreenMagnificationAutoUpdateEnabled(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()
&& isScreenMagnificationAutoUpdateEnabled(mContext)) {
mMagnificationController.reset(true);
mViewport.setFrameShown(false, true);
}
}
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 mTempRect1 = new Rect();
private final Rect mTempRect2 = new Rect();
private final Rect mTempRect3 = 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);
}
private final Comparator<WindowInfo> mWindowInfoInverseComparator =
new Comparator<WindowInfo>() {
@Override
public int compare(WindowInfo lhs, WindowInfo rhs) {
if (lhs.layer != rhs.layer) {
return rhs.layer - lhs.layer;
}
if (lhs.touchableRegion.top != rhs.touchableRegion.top) {
return rhs.touchableRegion.top - lhs.touchableRegion.top;
}
if (lhs.touchableRegion.left != rhs.touchableRegion.left) {
return rhs.touchableRegion.left - lhs.touchableRegion.left;
}
if (lhs.touchableRegion.right != rhs.touchableRegion.right) {
return rhs.touchableRegion.right - lhs.touchableRegion.right;
}
if (lhs.touchableRegion.bottom != rhs.touchableRegion.bottom) {
return rhs.touchableRegion.bottom - lhs.touchableRegion.bottom;
}
return 0;
}
};
public void recomputeBounds(boolean animate) {
Rect magnifiedFrame = mTempRect1;
magnifiedFrame.set(0, 0, 0, 0);
Rect notMagnifiedFrame = mTempRect2;
notMagnifiedFrame.set(0, 0, 0, 0);
ArrayList<WindowInfo> infos = mTempWindowInfoList;
infos.clear();
int windowCount = 0;
try {
mWindowManagerService.getVisibleWindowsForDisplay(
mDisplayProvider.getDisplay().getDisplayId(), infos);
Collections.sort(infos, mWindowInfoInverseComparator);
windowCount = infos.size();
for (int i = 0; i < windowCount; i++) {
WindowInfo info = infos.get(i);
if (info.type == WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY) {
continue;
}
if (isWindowMagnified(info.type)) {
Rect clippedFrame = mTempRect3;
clippedFrame.set(info.touchableRegion);
subtract(clippedFrame, notMagnifiedFrame);
magnifiedFrame.union(clippedFrame);
} else {
Rect clippedFrame = mTempRect3;
clippedFrame.set(info.touchableRegion);
subtract(clippedFrame, magnifiedFrame);
notMagnifiedFrame.union(clippedFrame);
}
if (magnifiedFrame.bottom >= notMagnifiedFrame.top) {
break;
}
}
} catch (RemoteException re) {
/* ignore */
} finally {
for (int i = windowCount - 1; i >= 0; i--) {
infos.remove(i).recycle();
}
}
final int displayWidth = mDisplayProvider.getDisplayInfo().logicalWidth;
final int displayHeight = mDisplayProvider.getDisplayInfo().logicalHeight;
magnifiedFrame.intersect(0, 0, displayWidth, displayHeight);
resize(magnifiedFrame, animate);
}
private boolean isWindowMagnified(int type) {
return (type != WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
&& type != WindowManager.LayoutParams.TYPE_INPUT_METHOD
&& type != WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG);
}
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();
}
}
}