Add tests for MagnificationController.
Also refactoring the class to make it easier to test and
chaning behavior where the current behavior seemed poorly
defined.
Refactoring:
- Combined all handlers into one.
- Simplified animation to use a ValueAnimator.
- Eliminated ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE
setting. Move rest of settings reading into mockable class.
- Move callbacks from WindowManager into the main class.
- Pulled out my instrumented Handler from the
MotionEventInjectorTest into its own class so I can reuse
it.
Behavior changes:
- Always constraining out-of-bounds values rather than
refusing to change them.
- Constraining offsets on bounds changes. We previously
left them alone, even if they were out of bounds.
- Keeping track of the animation starting point. We were
interpolating between the current magnification spec
and the final one. This change means the magnification
animates to a different profile.
Test: This CL adds tests. I've also run a11y CTS.
Bugs: 31855954, 30325691
Change-Id: Ie00e29ae88b75d9fe1016f9d107257c9cf6425bb
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index d3a978c..3714f62 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -5161,18 +5161,10 @@
"accessibility_display_magnification_scale";
/**
- * Setting that specifies whether the display magnification should be
- * automatically updated. If this fearture is enabled the system will
- * exit magnification mode or pan the viewport when a context change
- * occurs. For example, on staring a new activity or rotating the screen,
- * the system may zoom out so the user can see the new context he is in.
- * Another example is on showing a window that is not visible in the
- * magnified viewport the system may pan the viewport to make the window
- * the has popped up so the user knows that the context has changed.
- * Whether a screen magnification is performed is controlled by
- * {@link #ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED}
+ * Unused mangnification setting
*
* @hide
+ * @deprecated
*/
public static final String ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE =
"accessibility_display_magnification_auto_update";
@@ -6485,7 +6477,6 @@
ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED,
ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
- ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
ACCESSIBILITY_SCRIPT_INJECTION,
ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS,
ENABLED_ACCESSIBILITY_SERVICES,
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java
index d55bb4f..dd543a3 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java
@@ -1310,10 +1310,6 @@
loadFractionSetting(stmt, Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
R.fraction.def_accessibility_display_magnification_scale, 1);
stmt.close();
- stmt = db.compileStatement("INSERT INTO secure(name,value) VALUES(?,?);");
- loadBooleanSetting(stmt,
- Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
- R.bool.def_accessibility_display_magnification_auto_update);
db.setTransactionSuccessful();
} finally {
@@ -2508,10 +2504,6 @@
loadFractionSetting(stmt, Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
R.fraction.def_accessibility_display_magnification_scale, 1);
- loadBooleanSetting(stmt,
- Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
- R.bool.def_accessibility_display_magnification_auto_update);
-
loadBooleanSetting(stmt, Settings.Secure.USER_SETUP_COMPLETE,
R.bool.def_user_setup_complete);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index e7f5f4f..2093871 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -2137,7 +2137,7 @@
}
private final class UpgradeController {
- private static final int SETTINGS_VERSION = 134;
+ private static final int SETTINGS_VERSION = 135;
private final int mUserId;
@@ -2507,6 +2507,14 @@
currentVersion = 134;
}
+ if (currentVersion == 134) {
+ // Remove setting that specifies if magnification values should be preserved.
+ // This setting defaulted to true and never has a UI.
+ getSecureSettingsLocked(userId).deleteSettingLocked(
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE);
+ currentVersion = 135;
+ }
+
if (currentVersion != newVersion) {
Slog.wtf("SettingsProvider", "warning: upgrading settings database to version "
+ newVersion + " left it at "
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 4819c0a..59b99c5 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -827,9 +827,10 @@
* @param centerX the new screen-relative center X coordinate
* @param centerY the new screen-relative center Y coordinate
*/
- void notifyMagnificationChanged(@NonNull Region region,
+ public void notifyMagnificationChanged(@NonNull Region region,
float scale, float centerX, float centerY) {
synchronized (mLock) {
+ notifyClearAccessibilityCacheLocked();
notifyMagnificationChangedLocked(region, scale, centerX, centerY);
}
}
@@ -899,10 +900,6 @@
mSecurityPolicy.onTouchInteractionEnd();
}
- void onMagnificationStateChanged() {
- notifyClearAccessibilityCacheLocked();
- }
-
private void switchUser(int userId) {
synchronized (mLock) {
if (mCurrentUserId == userId && mInitialized) {
diff --git a/services/accessibility/java/com/android/server/accessibility/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/MagnificationController.java
index 7886b9e..f65046c 100644
--- a/services/accessibility/java/com/android/server/accessibility/MagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/MagnificationController.java
@@ -21,8 +21,6 @@
import com.android.internal.os.SomeArgs;
import com.android.server.LocalServices;
-import android.animation.ObjectAnimator;
-import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.content.BroadcastReceiver;
@@ -34,12 +32,10 @@
import android.graphics.Region;
import android.os.AsyncTask;
import android.os.Handler;
-import android.os.Looper;
import android.os.Message;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.MathUtils;
-import android.util.Property;
import android.util.Slog;
import android.view.MagnificationSpec;
import android.view.View;
@@ -53,27 +49,29 @@
* from the accessibility manager and related classes. It is responsible for
* holding the current state of magnification and animation, and it handles
* communication between the accessibility manager and window manager.
+ *
+ * Magnification is limited to the range [MIN_SCALE, MAX_SCALE], and can only occur inside the
+ * magnification region. If a value is out of bounds, it will be adjusted to guarantee these
+ * constraints.
*/
-class MagnificationController {
+class MagnificationController implements Handler.Callback {
private static final String LOG_TAG = "MagnificationController";
- private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false;
+ public static final float MIN_SCALE = 1.0f;
+ public static final float MAX_SCALE = 5.0f;
- private static final int DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE = 1;
+ private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false;
private static final int INVALID_ID = -1;
private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f;
- private static final float MIN_SCALE = 1.0f;
- private static final float MAX_SCALE = 5.0f;
-
- /**
- * The minimum scaling factor that can be persisted to secure settings.
- * This must be > 1.0 to ensure that magnification is actually set to an
- * enabled state when the scaling factor is restored from settings.
- */
- private static final float MIN_PERSISTED_SCALE = 2.0f;
+ // Messages
+ private static final int MSG_SEND_SPEC_TO_ANIMATION = 1;
+ private static final int MSG_SCREEN_TURNED_OFF = 2;
+ private static final int MSG_ON_MAGNIFIED_BOUNDS_CHANGED = 3;
+ private static final int MSG_ON_RECTANGLE_ON_SCREEN_REQUESTED = 4;
+ private static final int MSG_ON_USER_CONTEXT_CHANGED = 5;
private final Object mLock;
@@ -90,46 +88,95 @@
private final Rect mTempRect1 = new Rect();
private final AccessibilityManagerService mAms;
- private final ContentResolver mContentResolver;
+
+ private final SettingsBridge mSettingsBridge;
private final ScreenStateObserver mScreenStateObserver;
- private final WindowStateObserver mWindowStateObserver;
private final SpecAnimationBridge mSpecAnimationBridge;
+ private final WindowManagerInternal.MagnificationCallbacks mWMCallbacks =
+ new WindowManagerInternal.MagnificationCallbacks () {
+ @Override
+ public void onMagnificationRegionChanged(Region region) {
+ final SomeArgs args = SomeArgs.obtain();
+ args.arg1 = Region.obtain(region);
+ mHandler.obtainMessage(MSG_ON_MAGNIFIED_BOUNDS_CHANGED, args).sendToTarget();
+ }
+
+ @Override
+ public void onRectangleOnScreenRequested(int left, int top, int right, int bottom) {
+ final SomeArgs args = SomeArgs.obtain();
+ args.argi1 = left;
+ args.argi2 = top;
+ args.argi3 = right;
+ args.argi4 = bottom;
+ mHandler.obtainMessage(MSG_ON_RECTANGLE_ON_SCREEN_REQUESTED, args)
+ .sendToTarget();
+ }
+
+ @Override
+ public void onRotationChanged(int rotation) {
+ // Treat as context change and reset
+ mHandler.sendEmptyMessage(MSG_ON_USER_CONTEXT_CHANGED);
+ }
+
+ @Override
+ public void onUserContextChanged() {
+ mHandler.sendEmptyMessage(MSG_ON_USER_CONTEXT_CHANGED);
+ }
+ };
+
private int mUserId;
+ private final long mMainThreadId;
+
+ private Handler mHandler;
+
private int mIdOfLastServiceToMagnify = INVALID_ID;
+ private final WindowManagerInternal mWindowManager;
+
// Flag indicating that we are registered with window manager.
private boolean mRegistered;
private boolean mUnregisterPending;
public MagnificationController(Context context, AccessibilityManagerService ams, Object lock) {
+ this(context, ams, lock, null, LocalServices.getService(WindowManagerInternal.class),
+ new ValueAnimator(), new SettingsBridge(context.getContentResolver()));
+ mHandler = new Handler(context.getMainLooper(), this);
+ }
+
+ public MagnificationController(Context context, AccessibilityManagerService ams, Object lock,
+ Handler handler, WindowManagerInternal windowManagerInternal,
+ ValueAnimator valueAnimator, SettingsBridge settingsBridge) {
+ mHandler = handler;
+ mWindowManager = windowManagerInternal;
+ mMainThreadId = context.getMainLooper().getThread().getId();
mAms = ams;
- mContentResolver = context.getContentResolver();
mScreenStateObserver = new ScreenStateObserver(context, this);
- mWindowStateObserver = new WindowStateObserver(context, this);
mLock = lock;
- mSpecAnimationBridge = new SpecAnimationBridge(context, mLock);
+ mSpecAnimationBridge = new SpecAnimationBridge(
+ context, mLock, mWindowManager, valueAnimator);
+ mSettingsBridge = settingsBridge;
}
/**
* Start tracking the magnification region for services that control magnification and the
* magnification gesture handler.
*
- * This tracking imposes a cost on the system, so we avoid tracking this data
- * unless it's required.
+ * This tracking imposes a cost on the system, so we avoid tracking this data unless it's
+ * required.
*/
public void register() {
synchronized (mLock) {
if (!mRegistered) {
mScreenStateObserver.register();
- mWindowStateObserver.register();
+ mWindowManager.setMagnificationCallbacks(mWMCallbacks);
mSpecAnimationBridge.setEnabled(true);
// Obtain initial state.
- mWindowStateObserver.getMagnificationRegion(mMagnificationRegion);
+ mWindowManager.getMagnificationRegion(mMagnificationRegion);
mMagnificationRegion.getBounds(mMagnificationBounds);
mRegistered = true;
}
@@ -164,7 +211,7 @@
if (mRegistered) {
mSpecAnimationBridge.setEnabled(false);
mScreenStateObserver.unregister();
- mWindowStateObserver.unregister();
+ mWindowManager.setMagnificationCallbacks(null);
mMagnificationRegion.setEmpty();
mRegistered = false;
}
@@ -183,40 +230,22 @@
* Update our copy of the current magnification region
*
* @param magnified the magnified region
- * @param updateSpec {@code true} to update the scale and center based on
- * the region bounds, {@code false} to leave them as-is
*/
- private void onMagnificationRegionChanged(Region magnified, boolean updateSpec) {
+ private void onMagnificationRegionChanged(Region magnified) {
synchronized (mLock) {
if (!mRegistered) {
// Don't update if we've unregistered
return;
}
- boolean magnificationChanged = false;
- boolean boundsChanged = false;
-
if (!mMagnificationRegion.equals(magnified)) {
mMagnificationRegion.set(magnified);
mMagnificationRegion.getBounds(mMagnificationBounds);
- boundsChanged = true;
- }
- if (updateSpec) {
- final MagnificationSpec sentSpec = mSpecAnimationBridge.mSentMagnificationSpec;
- final float scale = sentSpec.scale;
- final float offsetX = sentSpec.offsetX;
- final float offsetY = sentSpec.offsetY;
-
- // Compute the new center and update spec as needed.
- final float centerX = (mMagnificationBounds.width() / 2.0f
- + mMagnificationBounds.left - offsetX) / scale;
- final float centerY = (mMagnificationBounds.height() / 2.0f
- + mMagnificationBounds.top - offsetY) / scale;
- magnificationChanged = setScaleAndCenterLocked(
- scale, centerX, centerY, false, INVALID_ID);
- }
-
- // If magnification changed we already notified for the change.
- if (boundsChanged && updateSpec && !magnificationChanged) {
+ // It's possible that our magnification spec is invalid with the new bounds.
+ // Adjust the current spec's offsets if necessary.
+ if (updateCurrentSpecWithOffsetsLocked(
+ mCurrentMagnificationSpec.offsetX, mCurrentMagnificationSpec.offsetY)) {
+ sendSpecToAnimation(mCurrentMagnificationSpec, false);
+ }
onMagnificationChangedLocked();
}
}
@@ -328,7 +357,7 @@
*
* @return the scale currently used by the window manager
*/
- public float getSentScale() {
+ private float getSentScale() {
return mSpecAnimationBridge.mSentMagnificationSpec.scale;
}
@@ -339,7 +368,7 @@
*
* @return the X offset currently used by the window manager
*/
- public float getSentOffsetX() {
+ private float getSentOffsetX() {
return mSpecAnimationBridge.mSentMagnificationSpec.offsetX;
}
@@ -350,7 +379,7 @@
*
* @return the Y offset currently used by the window manager
*/
- public float getSentOffsetY() {
+ private float getSentOffsetY() {
return mSpecAnimationBridge.mSentMagnificationSpec.offsetY;
}
@@ -380,7 +409,7 @@
onMagnificationChangedLocked();
}
mIdOfLastServiceToMagnify = INVALID_ID;
- mSpecAnimationBridge.updateSentSpec(spec, animate);
+ sendSpecToAnimation(spec, animate);
return changed;
}
@@ -475,7 +504,7 @@
private boolean setScaleAndCenterLocked(float scale, float centerX, float centerY,
boolean animate, int id) {
final boolean changed = updateMagnificationSpecLocked(scale, centerX, centerY);
- mSpecAnimationBridge.updateSentSpec(mCurrentMagnificationSpec, animate);
+ sendSpecToAnimation(mCurrentMagnificationSpec, animate);
if (isMagnifying() && (id != INVALID_ID)) {
mIdOfLastServiceToMagnify = id;
}
@@ -483,27 +512,28 @@
}
/**
- * Offsets the center of the magnified region.
+ * Offsets the magnified region. Note that the offsetX and offsetY values actually move in the
+ * opposite direction as the offsets passed in here.
*
- * @param offsetX the amount in pixels to offset the X center
- * @param offsetY the amount in pixels to offset the Y center
+ * @param offsetX the amount in pixels to offset the region in the X direction, in current
+ * screen pixels.
+ * @param offsetY the amount in pixels to offset the region in the Y direction, in current
+ * screen pixels.
* @param id the ID of the service requesting the change
*/
- public void offsetMagnifiedRegionCenter(float offsetX, float offsetY, int id) {
+ public void offsetMagnifiedRegion(float offsetX, float offsetY, int id) {
synchronized (mLock) {
if (!mRegistered) {
return;
}
- final MagnificationSpec currSpec = mCurrentMagnificationSpec;
- final float nonNormOffsetX = currSpec.offsetX - offsetX;
- currSpec.offsetX = MathUtils.constrain(nonNormOffsetX, getMinOffsetXLocked(), 0);
- final float nonNormOffsetY = currSpec.offsetY - offsetY;
- currSpec.offsetY = MathUtils.constrain(nonNormOffsetY, getMinOffsetYLocked(), 0);
+ final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX;
+ final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY;
+ updateCurrentSpecWithOffsetsLocked(nonNormOffsetX, nonNormOffsetY);
if (id != INVALID_ID) {
mIdOfLastServiceToMagnify = id;
}
- mSpecAnimationBridge.updateSentSpec(currSpec, false);
+ sendSpecToAnimation(mCurrentMagnificationSpec, false);
}
}
@@ -517,7 +547,6 @@
}
private void onMagnificationChangedLocked() {
- mAms.onMagnificationStateChanged();
mAms.notifyMagnificationChanged(mMagnificationRegion,
getScale(), getCenterX(), getCenterY());
if (mUnregisterPending && !isMagnifying()) {
@@ -535,8 +564,7 @@
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
- Settings.Secure.putFloatForUser(mContentResolver,
- Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale, userId);
+ mSettingsBridge.putMagnificationScale(scale, userId);
return null;
}
}.execute();
@@ -550,9 +578,7 @@
* scale if none is available
*/
public float getPersistedScale() {
- return Settings.Secure.getFloatForUser(mContentResolver,
- Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
- DEFAULT_MAGNIFICATION_SCALE, mUserId);
+ return mSettingsBridge.getMagnificationScale(mUserId);
}
/**
@@ -578,36 +604,20 @@
scale = getScale();
}
- // Ensure requested center is within the magnification region.
- if (!magnificationRegionContains(centerX, centerY)) {
- return false;
- }
-
// Compute changes.
- final MagnificationSpec currSpec = mCurrentMagnificationSpec;
boolean changed = false;
final float normScale = MathUtils.constrain(scale, MIN_SCALE, MAX_SCALE);
- if (Float.compare(currSpec.scale, normScale) != 0) {
- currSpec.scale = normScale;
+ if (Float.compare(mCurrentMagnificationSpec.scale, normScale) != 0) {
+ mCurrentMagnificationSpec.scale = normScale;
changed = true;
}
final float nonNormOffsetX = mMagnificationBounds.width() / 2.0f
- + mMagnificationBounds.left - centerX * scale;
- final float offsetX = MathUtils.constrain(nonNormOffsetX, getMinOffsetXLocked(), 0);
- if (Float.compare(currSpec.offsetX, offsetX) != 0) {
- currSpec.offsetX = offsetX;
- changed = true;
- }
-
+ + mMagnificationBounds.left - centerX * normScale;
final float nonNormOffsetY = mMagnificationBounds.height() / 2.0f
- + mMagnificationBounds.top - centerY * scale;
- final float offsetY = MathUtils.constrain(nonNormOffsetY, getMinOffsetYLocked(), 0);
- if (Float.compare(currSpec.offsetY, offsetY) != 0) {
- currSpec.offsetY = offsetY;
- changed = true;
- }
+ + mMagnificationBounds.top - centerY * normScale;
+ changed |= updateCurrentSpecWithOffsetsLocked(nonNormOffsetX, nonNormOffsetY);
if (changed) {
onMagnificationChangedLocked();
@@ -616,6 +626,21 @@
return changed;
}
+ private boolean updateCurrentSpecWithOffsetsLocked(float nonNormOffsetX, float nonNormOffsetY) {
+ boolean changed = false;
+ final float offsetX = MathUtils.constrain(nonNormOffsetX, getMinOffsetXLocked(), 0);
+ if (Float.compare(mCurrentMagnificationSpec.offsetX, offsetX) != 0) {
+ mCurrentMagnificationSpec.offsetX = offsetX;
+ changed = true;
+ }
+ final float offsetY = MathUtils.constrain(nonNormOffsetY, getMinOffsetYLocked(), 0);
+ if (Float.compare(mCurrentMagnificationSpec.offsetY, offsetY) != 0) {
+ mCurrentMagnificationSpec.offsetY = offsetY;
+ changed = true;
+ }
+ return changed;
+ }
+
private float getMinOffsetXLocked() {
final float viewportWidth = mMagnificationBounds.width();
return viewportWidth - viewportWidth * mCurrentMagnificationSpec.scale;
@@ -643,12 +668,6 @@
}
}
- private boolean isScreenMagnificationAutoUpdateEnabled() {
- return (Settings.Secure.getInt(mContentResolver,
- Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
- DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1);
- }
-
/**
* Resets magnification if magnification and auto-update are both enabled.
*
@@ -658,7 +677,7 @@
*/
boolean resetIfNeeded(boolean animate) {
synchronized (mLock) {
- if (isMagnifying() && isScreenMagnificationAutoUpdateEnabled()) {
+ if (isMagnifying()) {
reset(animate);
return true;
}
@@ -715,18 +734,61 @@
}
final float scale = getScale();
- offsetMagnifiedRegionCenter(scrollX * scale, scrollY * scale, INVALID_ID);
+ offsetMagnifiedRegion(scrollX * scale, scrollY * scale, INVALID_ID);
}
}
+ private void sendSpecToAnimation(MagnificationSpec spec, boolean animate) {
+ if (Thread.currentThread().getId() == mMainThreadId) {
+ mSpecAnimationBridge.updateSentSpecMainThread(spec, animate);
+ } else {
+ mHandler.obtainMessage(MSG_SEND_SPEC_TO_ANIMATION,
+ animate ? 1 : 0, 0, spec).sendToTarget();
+ }
+ }
+
+ private void onScreenTurnedOff() {
+ mHandler.sendEmptyMessage(MSG_SCREEN_TURNED_OFF);
+ }
+
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_SEND_SPEC_TO_ANIMATION:
+ final boolean animate = msg.arg1 == 1;
+ final MagnificationSpec spec = (MagnificationSpec) msg.obj;
+ mSpecAnimationBridge.updateSentSpecMainThread(spec, animate);
+ break;
+ case MSG_SCREEN_TURNED_OFF:
+ resetIfNeeded(false);
+ break;
+ case MSG_ON_MAGNIFIED_BOUNDS_CHANGED: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final Region magnifiedBounds = (Region) args.arg1;
+ onMagnificationRegionChanged(magnifiedBounds);
+ magnifiedBounds.recycle();
+ args.recycle();
+ } break;
+ case MSG_ON_RECTANGLE_ON_SCREEN_REQUESTED: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final int left = args.argi1;
+ final int top = args.argi2;
+ final int right = args.argi3;
+ final int bottom = args.argi4;
+ requestRectangleOnScreen(left, top, right, bottom);
+ args.recycle();
+ } break;
+ case MSG_ON_USER_CONTEXT_CHANGED:
+ resetIfNeeded(true);
+ break;
+ }
+ return true;
+ }
+
/**
* Class responsible for animating spec on the main thread and sending spec
* updates to the window manager.
*/
- private static class SpecAnimationBridge {
- private static final int ACTION_UPDATE_SPEC = 1;
-
- private final Handler mHandler;
+ private static class SpecAnimationBridge implements ValueAnimator.AnimatorUpdateListener {
private final WindowManagerInternal mWindowManager;
/**
@@ -735,34 +797,33 @@
*/
private final MagnificationSpec mSentMagnificationSpec = MagnificationSpec.obtain();
- /**
- * The animator that updates the sent spec. This should only be accessed
- * and modified on the main (e.g. animation) thread.
- */
- private final ValueAnimator mTransformationAnimator;
+ private final MagnificationSpec mStartMagnificationSpec = MagnificationSpec.obtain();
- private final long mMainThreadId;
+ private final MagnificationSpec mEndMagnificationSpec = MagnificationSpec.obtain();
+
+ private final MagnificationSpec mTmpMagnificationSpec = MagnificationSpec.obtain();
+
+ /**
+ * The animator should only be accessed and modified on the main (e.g. animation) thread.
+ */
+ private final ValueAnimator mValueAnimator;
+
private final Object mLock;
@GuardedBy("mLock")
private boolean mEnabled = false;
- private SpecAnimationBridge(Context context, Object lock) {
+ private SpecAnimationBridge(Context context, Object lock, WindowManagerInternal wm,
+ ValueAnimator animator) {
mLock = lock;
- final Looper mainLooper = context.getMainLooper();
- mMainThreadId = mainLooper.getThread().getId();
-
- mHandler = new UpdateHandler(context);
- mWindowManager = LocalServices.getService(WindowManagerInternal.class);
-
- final MagnificationSpecProperty property = new MagnificationSpecProperty();
- final MagnificationSpecEvaluator evaluator = new MagnificationSpecEvaluator();
+ mWindowManager = wm;
final long animationDuration = context.getResources().getInteger(
R.integer.config_longAnimTime);
- mTransformationAnimator = ObjectAnimator.ofObject(this, property, evaluator,
- mSentMagnificationSpec);
- mTransformationAnimator.setDuration(animationDuration);
- mTransformationAnimator.setInterpolator(new DecelerateInterpolator(2.5f));
+ mValueAnimator = animator;
+ mValueAnimator.setDuration(animationDuration);
+ mValueAnimator.setInterpolator(new DecelerateInterpolator(2.5f));
+ mValueAnimator.setFloatValues(0.0f, 1.0f);
+ mValueAnimator.addUpdateListener(this);
}
/**
@@ -781,22 +842,9 @@
}
}
- public void updateSentSpec(MagnificationSpec spec, boolean animate) {
- if (Thread.currentThread().getId() == mMainThreadId) {
- // Already on the main thread, don't bother proxying.
- updateSentSpecInternal(spec, animate);
- } else {
- mHandler.obtainMessage(ACTION_UPDATE_SPEC,
- animate ? 1 : 0, 0, spec).sendToTarget();
- }
- }
-
- /**
- * Updates the sent spec.
- */
- private void updateSentSpecInternal(MagnificationSpec spec, boolean animate) {
- if (mTransformationAnimator.isRunning()) {
- mTransformationAnimator.cancel();
+ public void updateSentSpecMainThread(MagnificationSpec spec, boolean animate) {
+ if (mValueAnimator.isRunning()) {
+ mValueAnimator.cancel();
}
// If the current and sent specs don't match, update the sent spec.
@@ -812,11 +860,6 @@
}
}
- private void animateMagnificationSpecLocked(MagnificationSpec toSpec) {
- mTransformationAnimator.setObjectValues(mSentMagnificationSpec, toSpec);
- mTransformationAnimator.start();
- }
-
private void setMagnificationSpecLocked(MagnificationSpec spec) {
if (mEnabled) {
if (DEBUG_SET_MAGNIFICATION_SPEC) {
@@ -828,71 +871,40 @@
}
}
- private class UpdateHandler extends Handler {
- public UpdateHandler(Context context) {
- super(context.getMainLooper());
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case ACTION_UPDATE_SPEC:
- final boolean animate = msg.arg1 == 1;
- final MagnificationSpec spec = (MagnificationSpec) msg.obj;
- updateSentSpecInternal(spec, animate);
- break;
- }
- }
+ private void animateMagnificationSpecLocked(MagnificationSpec toSpec) {
+ mEndMagnificationSpec.setTo(toSpec);
+ mStartMagnificationSpec.setTo(mSentMagnificationSpec);
+ mValueAnimator.start();
}
- private static class MagnificationSpecProperty
- extends Property<SpecAnimationBridge, MagnificationSpec> {
- public MagnificationSpecProperty() {
- super(MagnificationSpec.class, "spec");
- }
-
- @Override
- public MagnificationSpec get(SpecAnimationBridge object) {
- synchronized (object.mLock) {
- return object.mSentMagnificationSpec;
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ synchronized (mLock) {
+ if (mEnabled) {
+ float fract = animation.getAnimatedFraction();
+ mTmpMagnificationSpec.scale = mStartMagnificationSpec.scale +
+ (mEndMagnificationSpec.scale - mStartMagnificationSpec.scale) * fract;
+ mTmpMagnificationSpec.offsetX = mStartMagnificationSpec.offsetX +
+ (mEndMagnificationSpec.offsetX - mStartMagnificationSpec.offsetX)
+ * fract;
+ mTmpMagnificationSpec.offsetY = mStartMagnificationSpec.offsetY +
+ (mEndMagnificationSpec.offsetY - mStartMagnificationSpec.offsetY)
+ * fract;
+ synchronized (mLock) {
+ setMagnificationSpecLocked(mTmpMagnificationSpec);
+ }
}
}
-
- @Override
- public void set(SpecAnimationBridge object, MagnificationSpec value) {
- synchronized (object.mLock) {
- object.setMagnificationSpecLocked(value);
- }
- }
- }
-
- private static class MagnificationSpecEvaluator
- implements TypeEvaluator<MagnificationSpec> {
- private final MagnificationSpec mTempSpec = MagnificationSpec.obtain();
-
- @Override
- public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec,
- MagnificationSpec toSpec) {
- final MagnificationSpec result = mTempSpec;
- result.scale = fromSpec.scale + (toSpec.scale - fromSpec.scale) * fraction;
- result.offsetX = fromSpec.offsetX + (toSpec.offsetX - fromSpec.offsetX) * fraction;
- result.offsetY = fromSpec.offsetY + (toSpec.offsetY - fromSpec.offsetY) * fraction;
- return result;
- }
}
}
private static class ScreenStateObserver extends BroadcastReceiver {
- private static final int MESSAGE_ON_SCREEN_STATE_CHANGE = 1;
-
private final Context mContext;
private final MagnificationController mController;
- private final Handler mHandler;
public ScreenStateObserver(Context context, MagnificationController controller) {
mContext = context;
mController = controller;
- mHandler = new StateChangeHandler(context);
}
public void register() {
@@ -905,151 +917,27 @@
@Override
public void onReceive(Context context, Intent intent) {
- mHandler.obtainMessage(MESSAGE_ON_SCREEN_STATE_CHANGE,
- intent.getAction()).sendToTarget();
- }
-
- private void handleOnScreenStateChange() {
- mController.resetIfNeeded(false);
- }
-
- private class StateChangeHandler extends Handler {
- public StateChangeHandler(Context context) {
- super(context.getMainLooper());
- }
-
- @Override
- public void handleMessage(Message message) {
- switch (message.what) {
- case MESSAGE_ON_SCREEN_STATE_CHANGE:
- handleOnScreenStateChange();
- break;
- }
- }
+ mController.onScreenTurnedOff();
}
}
- /**
- * This class handles the screen magnification when accessibility is enabled.
- */
- private static class WindowStateObserver
- implements WindowManagerInternal.MagnificationCallbacks {
- private static final int MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED = 1;
- private static final int MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED = 2;
- private static final int MESSAGE_ON_USER_CONTEXT_CHANGED = 3;
- private static final int MESSAGE_ON_ROTATION_CHANGED = 4;
+ // Extra class to get settings so tests can mock it
+ public static class SettingsBridge {
+ private final ContentResolver mContentResolver;
- private final MagnificationController mController;
- private final WindowManagerInternal mWindowManager;
- private final Handler mHandler;
-
- private boolean mSpecIsDirty;
-
- public WindowStateObserver(Context context, MagnificationController controller) {
- mController = controller;
- mWindowManager = LocalServices.getService(WindowManagerInternal.class);
- mHandler = new CallbackHandler(context);
+ public SettingsBridge(ContentResolver contentResolver) {
+ mContentResolver = contentResolver;
}
- public void register() {
- mWindowManager.setMagnificationCallbacks(this);
+ public void putMagnificationScale(float value, int userId) {
+ Settings.Secure.putFloatForUser(mContentResolver,
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, value, userId);
}
- public void unregister() {
- mWindowManager.setMagnificationCallbacks(null);
- }
-
- @Override
- public void onMagnificationRegionChanged(Region magnificationRegion) {
- final SomeArgs args = SomeArgs.obtain();
- args.arg1 = Region.obtain(magnificationRegion);
- mHandler.obtainMessage(MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED, args).sendToTarget();
- }
-
- private void handleOnMagnifiedBoundsChanged(Region magnificationRegion) {
- mController.onMagnificationRegionChanged(magnificationRegion, mSpecIsDirty);
- mSpecIsDirty = false;
- }
-
- @Override
- public void onRectangleOnScreenRequested(int left, int top, int right, int bottom) {
- final SomeArgs args = SomeArgs.obtain();
- args.argi1 = left;
- args.argi2 = top;
- args.argi3 = right;
- args.argi4 = bottom;
- mHandler.obtainMessage(MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED, args).sendToTarget();
- }
-
- private void handleOnRectangleOnScreenRequested(int left, int top, int right, int bottom) {
- mController.requestRectangleOnScreen(left, top, right, bottom);
- }
-
- @Override
- public void onRotationChanged(int rotation) {
- mHandler.obtainMessage(MESSAGE_ON_ROTATION_CHANGED, rotation, 0).sendToTarget();
- }
-
- private void handleOnRotationChanged() {
- // If there was a rotation and magnification is still enabled,
- // we'll need to rewrite the spec to reflect the new screen
- // configuration. Conveniently, we'll receive a callback from
- // the window manager with updated bounds for the magnified
- // region.
- mSpecIsDirty = !mController.resetIfNeeded(true);
- }
-
- @Override
- public void onUserContextChanged() {
- mHandler.sendEmptyMessage(MESSAGE_ON_USER_CONTEXT_CHANGED);
- }
-
- private void handleOnUserContextChanged() {
- mController.resetIfNeeded(true);
- }
-
- /**
- * This method is used to get the magnification region in the tiny time slice between
- * registering the callbacks and handling the message.
- * TODO: Elimiante this extra path, perhaps by processing the message immediately
- *
- * @param outMagnificationRegion
- */
- public void getMagnificationRegion(@NonNull Region outMagnificationRegion) {
- mWindowManager.getMagnificationRegion(outMagnificationRegion);
- }
-
- private class CallbackHandler extends Handler {
- public CallbackHandler(Context context) {
- super(context.getMainLooper());
- }
-
- @Override
- public void handleMessage(Message message) {
- switch (message.what) {
- case MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED: {
- final SomeArgs args = (SomeArgs) message.obj;
- final Region magnifiedBounds = (Region) args.arg1;
- handleOnMagnifiedBoundsChanged(magnifiedBounds);
- magnifiedBounds.recycle();
- } break;
- case MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED: {
- final SomeArgs args = (SomeArgs) message.obj;
- final int left = args.argi1;
- final int top = args.argi2;
- final int right = args.argi3;
- final int bottom = args.argi4;
- handleOnRectangleOnScreenRequested(left, top, right, bottom);
- args.recycle();
- } break;
- case MESSAGE_ON_USER_CONTEXT_CHANGED: {
- handleOnUserContextChanged();
- } break;
- case MESSAGE_ON_ROTATION_CHANGED: {
- handleOnRotationChanged();
- } break;
- }
- }
+ public float getMagnificationScale(int userId) {
+ return Settings.Secure.getFloatForUser(mContentResolver,
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
+ DEFAULT_MAGNIFICATION_SCALE, userId);
}
}
}
diff --git a/services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java
index 39bc809..f6e5340 100644
--- a/services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java
@@ -381,7 +381,7 @@
Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX
+ " scrollY: " + distanceY);
}
- mMagnificationController.offsetMagnifiedRegionCenter(distanceX, distanceY,
+ mMagnificationController.offsetMagnifiedRegion(distanceX, distanceY,
AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
return true;
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/MagnificationControllerTest.java
new file mode 100644
index 0000000..cb5e8bb
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MagnificationControllerTest.java
@@ -0,0 +1,827 @@
+/*
+ * Copyright (C) 2016 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.animation.ValueAnimator;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.MagnificationSpec;
+import android.view.WindowManagerInternal;
+import android.view.WindowManagerInternal.MagnificationCallbacks;
+
+import com.android.internal.R;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.Locale;
+
+@RunWith(AndroidJUnit4.class)
+public class MagnificationControllerTest {
+ static final Rect INITIAL_MAGNIFICATION_BOUNDS = new Rect(0, 0, 100, 200);
+ static final PointF INITIAL_MAGNIFICATION_BOUNDS_CENTER = new PointF(
+ INITIAL_MAGNIFICATION_BOUNDS.centerX(), INITIAL_MAGNIFICATION_BOUNDS.centerY());
+ static final PointF INITIAL_BOUNDS_UPPER_LEFT_2X_CENTER = new PointF(25, 50);
+ static final PointF INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER = new PointF(75, 150);
+ static final Rect OTHER_MAGNIFICATION_BOUNDS = new Rect(100, 200, 500, 600);
+ static final PointF OTHER_BOUNDS_LOWER_RIGHT_2X_CENTER = new PointF(400, 500);
+ static final Region INITIAL_MAGNIFICATION_REGION = new Region(INITIAL_MAGNIFICATION_BOUNDS);
+ static final Region OTHER_REGION = new Region(OTHER_MAGNIFICATION_BOUNDS);
+ static final int SERVICE_ID_1 = 1;
+ static final int SERVICE_ID_2 = 2;
+
+ final Context mMockContext = mock(Context.class);
+ final AccessibilityManagerService mMockAms = mock(AccessibilityManagerService.class);
+ final WindowManagerInternal mMockWindowManager = mock(WindowManagerInternal.class);
+ final MessageCapturingHandler mMessageCapturingHandler =
+ new MessageCapturingHandler(new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ return mMagnificationController.handleMessage(msg);
+ }
+ });
+ final ArgumentCaptor<MagnificationSpec> mMagnificationSpecCaptor =
+ ArgumentCaptor.forClass(MagnificationSpec.class);
+ final ValueAnimator mMockValueAnimator = mock(ValueAnimator.class);
+ MagnificationController.SettingsBridge mMockSettingsBridge;
+
+
+ MagnificationController mMagnificationController;
+ ValueAnimator.AnimatorUpdateListener mTargetAnimationListener;
+
+ @BeforeClass
+ public static void oneTimeInitialization() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ }
+
+ @Before
+ public void setUp() {
+ when(mMockContext.getMainLooper()).thenReturn(Looper.myLooper());
+ Resources mockResources = mock(Resources.class);
+ when(mMockContext.getResources()).thenReturn(mockResources);
+ when(mockResources.getInteger(R.integer.config_longAnimTime))
+ .thenReturn(1000);
+ mMockSettingsBridge = mock(MagnificationController.SettingsBridge.class);
+ mMagnificationController = new MagnificationController(mMockContext, mMockAms, new Object(),
+ mMessageCapturingHandler, mMockWindowManager, mMockValueAnimator,
+ mMockSettingsBridge);
+
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
+ Object[] args = invocationOnMock.getArguments();
+ Region regionArg = (Region) args[0];
+ regionArg.set(INITIAL_MAGNIFICATION_REGION);
+ return null;
+ }
+ }).when(mMockWindowManager).getMagnificationRegion((Region) anyObject());
+
+ ArgumentCaptor<ValueAnimator.AnimatorUpdateListener> listenerArgumentCaptor =
+ ArgumentCaptor.forClass(ValueAnimator.AnimatorUpdateListener.class);
+ verify(mMockValueAnimator).addUpdateListener(listenerArgumentCaptor.capture());
+ mTargetAnimationListener = listenerArgumentCaptor.getValue();
+ Mockito.reset(mMockValueAnimator); // Ignore other initialization
+ }
+
+ @Test
+ public void testRegister_WindowManagerAndContextRegisterListeners() {
+ mMagnificationController.register();
+ verify(mMockContext).registerReceiver(
+ (BroadcastReceiver) anyObject(), (IntentFilter) anyObject());
+ verify(mMockWindowManager).setMagnificationCallbacks((MagnificationCallbacks) anyObject());
+ assertTrue(mMagnificationController.isRegisteredLocked());
+ }
+
+ @Test
+ public void testRegister_WindowManagerAndContextUnregisterListeners() {
+ mMagnificationController.register();
+ mMagnificationController.unregister();
+
+ verify(mMockContext).unregisterReceiver((BroadcastReceiver) anyObject());
+ verify(mMockWindowManager).setMagnificationCallbacks(null);
+ assertFalse(mMagnificationController.isRegisteredLocked());
+ }
+
+ @Test
+ public void testInitialState_noMagnificationAndMagnificationRegionReadFromWindowManager() {
+ mMagnificationController.register();
+ MagnificationSpec expectedInitialSpec = getMagnificationSpec(1.0f, 0.0f, 0.0f);
+ Region initialMagRegion = new Region();
+ Rect initialBounds = new Rect();
+
+ assertEquals(expectedInitialSpec, getCurrentMagnificationSpec());
+ mMagnificationController.getMagnificationRegion(initialMagRegion);
+ mMagnificationController.getMagnificationBounds(initialBounds);
+ assertEquals(INITIAL_MAGNIFICATION_REGION, initialMagRegion);
+ assertEquals(INITIAL_MAGNIFICATION_BOUNDS, initialBounds);
+ assertEquals(INITIAL_MAGNIFICATION_BOUNDS.centerX(),
+ mMagnificationController.getCenterX(), 0.0f);
+ assertEquals(INITIAL_MAGNIFICATION_BOUNDS.centerY(),
+ mMagnificationController.getCenterY(), 0.0f);
+ }
+
+ @Test
+ public void testNotRegistered_publicMethodsShouldBeBenign() {
+ assertFalse(mMagnificationController.isMagnifying());
+ assertFalse(mMagnificationController.magnificationRegionContains(100, 100));
+ assertFalse(mMagnificationController.reset(true));
+ assertFalse(mMagnificationController.setScale(2, 100, 100, true, 0));
+ assertFalse(mMagnificationController.setCenter(100, 100, false, 1));
+ assertFalse(mMagnificationController.setScaleAndCenter(1.5f, 100, 100, false, 2));
+ assertTrue(mMagnificationController.getIdOfLastServiceToMagnify() < 0);
+
+ mMagnificationController.getMagnificationRegion(new Region());
+ mMagnificationController.getMagnificationBounds(new Rect());
+ mMagnificationController.getScale();
+ mMagnificationController.getOffsetX();
+ mMagnificationController.getOffsetY();
+ mMagnificationController.getCenterX();
+ mMagnificationController.getCenterY();
+ mMagnificationController.offsetMagnifiedRegion(50, 50, 1);
+ mMagnificationController.unregister();
+ }
+
+ @Test
+ public void testSetScale_noAnimation_shouldGoStraightToWindowManagerAndUpdateState() {
+ mMagnificationController.register();
+ final float scale = 2.0f;
+ final PointF center = INITIAL_MAGNIFICATION_BOUNDS_CENTER;
+ final PointF offsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, center, scale);
+ assertTrue(mMagnificationController
+ .setScale(scale, center.x, center.y, false, SERVICE_ID_1));
+
+ final MagnificationSpec expectedSpec = getMagnificationSpec(scale, offsets);
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(expectedSpec)));
+ assertThat(getCurrentMagnificationSpec(), closeTo(expectedSpec));
+ assertEquals(center.x, mMagnificationController.getCenterX(), 0.0);
+ assertEquals(center.y, mMagnificationController.getCenterY(), 0.0);
+ verify(mMockValueAnimator, times(0)).start();
+ }
+
+ @Test
+ public void testSetScale_withPivotAndAnimation_stateChangesAndAnimationHappens() {
+ mMagnificationController.register();
+ MagnificationSpec startSpec = getCurrentMagnificationSpec();
+ float scale = 2.0f;
+ PointF pivotPoint = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER;
+ assertTrue(mMagnificationController
+ .setScale(scale, pivotPoint.x, pivotPoint.y, true, SERVICE_ID_1));
+
+ // New center should be halfway between original center and pivot
+ PointF newCenter = new PointF((pivotPoint.x + INITIAL_MAGNIFICATION_BOUNDS.centerX()) / 2,
+ (pivotPoint.y + INITIAL_MAGNIFICATION_BOUNDS.centerY()) / 2);
+ PointF offsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale);
+ MagnificationSpec endSpec = getMagnificationSpec(scale, offsets);
+
+ assertEquals(newCenter.x, mMagnificationController.getCenterX(), 0.5);
+ assertEquals(newCenter.y, mMagnificationController.getCenterY(), 0.5);
+ assertThat(getCurrentMagnificationSpec(), closeTo(endSpec));
+ verify(mMockValueAnimator).start();
+
+ // Initial value
+ when(mMockValueAnimator.getAnimatedFraction()).thenReturn(0.0f);
+ mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
+ verify(mMockWindowManager).setMagnificationSpec(startSpec);
+
+ // Intermediate point
+ Mockito.reset(mMockWindowManager);
+ float fraction = 0.5f;
+ when(mMockValueAnimator.getAnimatedFraction()).thenReturn(fraction);
+ mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
+ verify(mMockWindowManager).setMagnificationSpec(
+ argThat(closeTo(getInterpolatedMagSpec(startSpec, endSpec, fraction))));
+
+ // Final value
+ Mockito.reset(mMockWindowManager);
+ when(mMockValueAnimator.getAnimatedFraction()).thenReturn(1.0f);
+ mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(endSpec)));
+ }
+
+ @Test
+ public void testSetCenter_whileMagnifying_noAnimation_centerMoves() {
+ mMagnificationController.register();
+ // First zoom in
+ float scale = 2.0f;
+ assertTrue(mMagnificationController.setScale(scale,
+ INITIAL_MAGNIFICATION_BOUNDS.centerX(), INITIAL_MAGNIFICATION_BOUNDS.centerY(),
+ false, SERVICE_ID_1));
+ Mockito.reset(mMockWindowManager);
+
+ PointF newCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER;
+ assertTrue(mMagnificationController
+ .setCenter(newCenter.x, newCenter.y, false, SERVICE_ID_1));
+ PointF expectedOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale);
+ MagnificationSpec expectedSpec = getMagnificationSpec(scale, expectedOffsets);
+
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(expectedSpec)));
+ assertEquals(newCenter.x, mMagnificationController.getCenterX(), 0.0);
+ assertEquals(newCenter.y, mMagnificationController.getCenterY(), 0.0);
+ verify(mMockValueAnimator, times(0)).start();
+ }
+
+ @Test
+ public void testSetScaleAndCenter_animated_stateChangesAndAnimationHappens() {
+ mMagnificationController.register();
+ MagnificationSpec startSpec = getCurrentMagnificationSpec();
+ float scale = 2.5f;
+ PointF newCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER;
+ PointF offsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale);
+ MagnificationSpec endSpec = getMagnificationSpec(scale, offsets);
+
+ assertTrue(mMagnificationController.setScaleAndCenter(scale, newCenter.x, newCenter.y,
+ true, SERVICE_ID_1));
+
+ assertEquals(newCenter.x, mMagnificationController.getCenterX(), 0.5);
+ assertEquals(newCenter.y, mMagnificationController.getCenterY(), 0.5);
+ assertThat(getCurrentMagnificationSpec(), closeTo(endSpec));
+ verify(mMockAms).notifyMagnificationChanged(
+ INITIAL_MAGNIFICATION_REGION, scale, newCenter.x, newCenter.y);
+ verify(mMockValueAnimator).start();
+
+ // Initial value
+ when(mMockValueAnimator.getAnimatedFraction()).thenReturn(0.0f);
+ mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
+ verify(mMockWindowManager).setMagnificationSpec(startSpec);
+
+ // Intermediate point
+ Mockito.reset(mMockWindowManager);
+ float fraction = 0.33f;
+ when(mMockValueAnimator.getAnimatedFraction()).thenReturn(fraction);
+ mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
+ verify(mMockWindowManager).setMagnificationSpec(
+ argThat(closeTo(getInterpolatedMagSpec(startSpec, endSpec, fraction))));
+
+ // Final value
+ Mockito.reset(mMockWindowManager);
+ when(mMockValueAnimator.getAnimatedFraction()).thenReturn(1.0f);
+ mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(endSpec)));
+ }
+
+ @Test
+ public void testSetScaleAndCenter_scaleOutOfBounds_cappedAtLimits() {
+ mMagnificationController.register();
+ MagnificationSpec startSpec = getCurrentMagnificationSpec();
+ PointF newCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER;
+ PointF offsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter,
+ MagnificationController.MAX_SCALE);
+ MagnificationSpec endSpec = getMagnificationSpec(
+ MagnificationController.MAX_SCALE, offsets);
+
+ assertTrue(mMagnificationController.setScaleAndCenter(
+ MagnificationController.MAX_SCALE + 1.0f,
+ newCenter.x, newCenter.y, false, SERVICE_ID_1));
+
+ assertEquals(newCenter.x, mMagnificationController.getCenterX(), 0.5);
+ assertEquals(newCenter.y, mMagnificationController.getCenterY(), 0.5);
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(endSpec)));
+ Mockito.reset(mMockWindowManager);
+
+ // Verify that we can't zoom below 1x
+ assertTrue(mMagnificationController.setScaleAndCenter(0.5f,
+ INITIAL_MAGNIFICATION_BOUNDS_CENTER.x, INITIAL_MAGNIFICATION_BOUNDS_CENTER.y,
+ false, SERVICE_ID_1));
+
+ assertEquals(INITIAL_MAGNIFICATION_BOUNDS_CENTER.x,
+ mMagnificationController.getCenterX(), 0.5);
+ assertEquals(INITIAL_MAGNIFICATION_BOUNDS_CENTER.y,
+ mMagnificationController.getCenterY(), 0.5);
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(startSpec)));
+ }
+
+ @Test
+ public void testSetScaleAndCenter_centerOutOfBounds_cappedAtLimits() {
+ mMagnificationController.register();
+ float scale = 2.0f;
+
+ // Off the edge to the top and left
+ assertTrue(mMagnificationController.setScaleAndCenter(
+ scale, -100f, -200f, false, SERVICE_ID_1));
+
+ PointF newCenter = INITIAL_BOUNDS_UPPER_LEFT_2X_CENTER;
+ PointF newOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale);
+ assertEquals(newCenter.x, mMagnificationController.getCenterX(), 0.5);
+ assertEquals(newCenter.y, mMagnificationController.getCenterY(), 0.5);
+ verify(mMockWindowManager).setMagnificationSpec(
+ argThat(closeTo(getMagnificationSpec(scale, newOffsets))));
+ Mockito.reset(mMockWindowManager);
+
+ // Off the edge to the bottom and right
+ assertTrue(mMagnificationController.setScaleAndCenter(scale,
+ INITIAL_MAGNIFICATION_BOUNDS.right + 1, INITIAL_MAGNIFICATION_BOUNDS.bottom + 1,
+ false, SERVICE_ID_1));
+ newCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER;
+ newOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale);
+ assertEquals(newCenter.x, mMagnificationController.getCenterX(), 0.5);
+ assertEquals(newCenter.y, mMagnificationController.getCenterY(), 0.5);
+ verify(mMockWindowManager).setMagnificationSpec(
+ argThat(closeTo(getMagnificationSpec(scale, newOffsets))));
+ }
+
+ @Test
+ public void testMagnificationRegionChanged_serviceNotified() {
+ mMagnificationController.register();
+ MagnificationCallbacks callbacks = getMagnificationCallbacks();
+ callbacks.onMagnificationRegionChanged(OTHER_REGION);
+ mMessageCapturingHandler.sendAllMessages();
+ verify(mMockAms).notifyMagnificationChanged(OTHER_REGION, 1.0f,
+ OTHER_MAGNIFICATION_BOUNDS.centerX(), OTHER_MAGNIFICATION_BOUNDS.centerY());
+ }
+
+ @Test
+ public void testOffsetMagnifiedRegion_whileMagnifying_offsetsMove() {
+ mMagnificationController.register();
+ PointF startCenter = INITIAL_MAGNIFICATION_BOUNDS_CENTER;
+ float scale = 2.0f;
+ PointF startOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, startCenter, scale);
+ // First zoom in
+ assertTrue(mMagnificationController
+ .setScaleAndCenter(scale, startCenter.x, startCenter.y, false, SERVICE_ID_1));
+ Mockito.reset(mMockWindowManager);
+
+ PointF newCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER;
+ PointF newOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale);
+ mMagnificationController.offsetMagnifiedRegion(
+ startOffsets.x - newOffsets.x, startOffsets.y - newOffsets.y, SERVICE_ID_1);
+
+ MagnificationSpec expectedSpec = getMagnificationSpec(scale, newOffsets);
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(expectedSpec)));
+ assertEquals(newCenter.x, mMagnificationController.getCenterX(), 0.0);
+ assertEquals(newCenter.y, mMagnificationController.getCenterY(), 0.0);
+ verify(mMockValueAnimator, times(0)).start();
+ }
+
+ @Test
+ public void testOffsetMagnifiedRegion_whileNotMagnifying_hasNoEffect() {
+ mMagnificationController.register();
+ Mockito.reset(mMockWindowManager);
+ MagnificationSpec startSpec = getCurrentMagnificationSpec();
+ mMagnificationController.offsetMagnifiedRegion(10, 10, SERVICE_ID_1);
+ assertThat(getCurrentMagnificationSpec(), closeTo(startSpec));
+ mMagnificationController.offsetMagnifiedRegion(-10, -10, SERVICE_ID_1);
+ assertThat(getCurrentMagnificationSpec(), closeTo(startSpec));
+ verifyNoMoreInteractions(mMockWindowManager);
+ }
+
+ @Test
+ public void testOffsetMagnifiedRegion_whileMagnifyingButAtEdge_hasNoEffect() {
+ mMagnificationController.register();
+ float scale = 2.0f;
+
+ // Upper left edges
+ PointF ulCenter = INITIAL_BOUNDS_UPPER_LEFT_2X_CENTER;
+ assertTrue(mMagnificationController
+ .setScaleAndCenter(scale, ulCenter.x, ulCenter.y, false, SERVICE_ID_1));
+ Mockito.reset(mMockWindowManager);
+ MagnificationSpec ulSpec = getCurrentMagnificationSpec();
+ mMagnificationController.offsetMagnifiedRegion(-10, -10, SERVICE_ID_1);
+ assertThat(getCurrentMagnificationSpec(), closeTo(ulSpec));
+ verifyNoMoreInteractions(mMockWindowManager);
+
+ // Lower right edges
+ PointF lrCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER;
+ assertTrue(mMagnificationController
+ .setScaleAndCenter(scale, lrCenter.x, lrCenter.y, false, SERVICE_ID_1));
+ Mockito.reset(mMockWindowManager);
+ MagnificationSpec lrSpec = getCurrentMagnificationSpec();
+ mMagnificationController.offsetMagnifiedRegion(10, 10, SERVICE_ID_1);
+ assertThat(getCurrentMagnificationSpec(), closeTo(lrSpec));
+ verifyNoMoreInteractions(mMockWindowManager);
+ }
+
+ @Test
+ public void testGetIdOfLastServiceToChange_returnsCorrectValue() {
+ mMagnificationController.register();
+ PointF startCenter = INITIAL_MAGNIFICATION_BOUNDS_CENTER;
+ assertTrue(mMagnificationController
+ .setScale(2.0f, startCenter.x, startCenter.y, false, SERVICE_ID_1));
+ assertEquals(SERVICE_ID_1, mMagnificationController.getIdOfLastServiceToMagnify());
+ assertTrue(mMagnificationController
+ .setScale(1.5f, startCenter.x, startCenter.y, false, SERVICE_ID_2));
+ assertEquals(SERVICE_ID_2, mMagnificationController.getIdOfLastServiceToMagnify());
+ }
+
+ @Test
+ public void testSetUserId_resetsOnlyIfIdChanges() {
+ final int userId1 = 1;
+ final int userId2 = 2;
+
+ mMagnificationController.register();
+ mMagnificationController.setUserId(userId1);
+ PointF startCenter = INITIAL_MAGNIFICATION_BOUNDS_CENTER;
+ float scale = 2.0f;
+ mMagnificationController.setScale(scale, startCenter.x, startCenter.y, false, SERVICE_ID_1);
+
+ mMagnificationController.setUserId(userId1);
+ assertTrue(mMagnificationController.isMagnifying());
+ mMagnificationController.setUserId(userId2);
+ assertFalse(mMagnificationController.isMagnifying());
+ }
+
+ @Test
+ public void testResetIfNeeded_doesWhatItSays() {
+ mMagnificationController.register();
+ zoomIn2xToMiddle();
+ assertTrue(mMagnificationController.resetIfNeeded(false));
+ verify(mMockAms).notifyMagnificationChanged(
+ eq(INITIAL_MAGNIFICATION_REGION), eq(1.0f), anyInt(), anyInt());
+ assertFalse(mMagnificationController.isMagnifying());
+ assertFalse(mMagnificationController.resetIfNeeded(false));
+ }
+
+ @Test
+ public void testTurnScreenOff_resetsMagnification() {
+ mMagnificationController.register();
+ ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor =
+ ArgumentCaptor.forClass(BroadcastReceiver.class);
+ verify(mMockContext).registerReceiver(
+ broadcastReceiverCaptor.capture(), (IntentFilter) anyObject());
+ BroadcastReceiver br = broadcastReceiverCaptor.getValue();
+ zoomIn2xToMiddle();
+ br.onReceive(mMockContext, null);
+ mMessageCapturingHandler.sendAllMessages();
+ assertFalse(mMagnificationController.isMagnifying());
+ }
+
+ @Test
+ public void testUserContextChange_resetsMagnification() {
+ mMagnificationController.register();
+ MagnificationCallbacks callbacks = getMagnificationCallbacks();
+ zoomIn2xToMiddle();
+ callbacks.onUserContextChanged();
+ mMessageCapturingHandler.sendAllMessages();
+ assertFalse(mMagnificationController.isMagnifying());
+ }
+
+ @Test
+ public void testRotation_resetsMagnification() {
+ mMagnificationController.register();
+ MagnificationCallbacks callbacks = getMagnificationCallbacks();
+ zoomIn2xToMiddle();
+ mMessageCapturingHandler.sendAllMessages();
+ assertTrue(mMagnificationController.isMagnifying());
+ callbacks.onRotationChanged(0);
+ mMessageCapturingHandler.sendAllMessages();
+ assertFalse(mMagnificationController.isMagnifying());
+ }
+
+ @Test
+ public void testBoundsChange_whileMagnifyingWithCompatibleSpec_noSpecChange() {
+ // Going from a small region to a large one leads to no issues
+ mMagnificationController.register();
+ zoomIn2xToMiddle();
+ MagnificationSpec startSpec = getCurrentMagnificationSpec();
+ MagnificationCallbacks callbacks = getMagnificationCallbacks();
+ Mockito.reset(mMockWindowManager);
+ callbacks.onMagnificationRegionChanged(OTHER_REGION);
+ mMessageCapturingHandler.sendAllMessages();
+ assertThat(getCurrentMagnificationSpec(), closeTo(startSpec));
+ verifyNoMoreInteractions(mMockWindowManager);
+ }
+
+ @Test
+ public void testBoundsChange_whileZoomingWithCompatibleSpec_noSpecChange() {
+ mMagnificationController.register();
+ PointF startCenter = INITIAL_MAGNIFICATION_BOUNDS_CENTER;
+ float scale = 2.0f;
+ mMagnificationController.setScale(scale, startCenter.x, startCenter.y, true, SERVICE_ID_1);
+ MagnificationSpec startSpec = getCurrentMagnificationSpec();
+ MagnificationCallbacks callbacks = getMagnificationCallbacks();
+ Mockito.reset(mMockWindowManager);
+ callbacks.onMagnificationRegionChanged(OTHER_REGION);
+ mMessageCapturingHandler.sendAllMessages();
+ assertThat(getCurrentMagnificationSpec(), closeTo(startSpec));
+ verifyNoMoreInteractions(mMockWindowManager);
+ }
+
+ @Test
+ public void testBoundsChange_whileMagnifyingWithIncompatibleSpec_offsetsConstrained() {
+ // In a large region, pan to the farthest point possible
+ mMagnificationController.register();
+ MagnificationCallbacks callbacks = getMagnificationCallbacks();
+ callbacks.onMagnificationRegionChanged(OTHER_REGION);
+ mMessageCapturingHandler.sendAllMessages();
+ PointF startCenter = OTHER_BOUNDS_LOWER_RIGHT_2X_CENTER;
+ float scale = 2.0f;
+ mMagnificationController.setScale(scale, startCenter.x, startCenter.y, false, SERVICE_ID_1);
+ MagnificationSpec startSpec = getCurrentMagnificationSpec();
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(startSpec)));
+ Mockito.reset(mMockWindowManager);
+
+ callbacks.onMagnificationRegionChanged(INITIAL_MAGNIFICATION_REGION);
+ mMessageCapturingHandler.sendAllMessages();
+
+ MagnificationSpec endSpec = getCurrentMagnificationSpec();
+ assertThat(endSpec, CoreMatchers.not(closeTo(startSpec)));
+ PointF expectedOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS,
+ INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER, scale);
+ assertThat(endSpec, closeTo(getMagnificationSpec(scale, expectedOffsets)));
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(endSpec)));
+ }
+
+ @Test
+ public void testBoundsChange_whileZoomingWithIncompatibleSpec_jumpsToCompatibleSpec() {
+ mMagnificationController.register();
+ MagnificationCallbacks callbacks = getMagnificationCallbacks();
+ callbacks.onMagnificationRegionChanged(OTHER_REGION);
+ mMessageCapturingHandler.sendAllMessages();
+ PointF startCenter = OTHER_BOUNDS_LOWER_RIGHT_2X_CENTER;
+ float scale = 2.0f;
+ mMagnificationController.setScale(scale, startCenter.x, startCenter.y, true, SERVICE_ID_1);
+ MagnificationSpec startSpec = getCurrentMagnificationSpec();
+ when (mMockValueAnimator.isRunning()).thenReturn(true);
+
+ callbacks.onMagnificationRegionChanged(INITIAL_MAGNIFICATION_REGION);
+ mMessageCapturingHandler.sendAllMessages();
+ verify(mMockValueAnimator).cancel();
+
+ MagnificationSpec endSpec = getCurrentMagnificationSpec();
+ assertThat(endSpec, CoreMatchers.not(closeTo(startSpec)));
+ PointF expectedOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS,
+ INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER, scale);
+ assertThat(endSpec, closeTo(getMagnificationSpec(scale, expectedOffsets)));
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(endSpec)));
+ }
+
+ @Test
+ public void testRequestRectOnScreen_rectAlreadyOnScreen_doesNothing() {
+ mMagnificationController.register();
+ zoomIn2xToMiddle();
+ MagnificationSpec startSpec = getCurrentMagnificationSpec();
+ MagnificationCallbacks callbacks = getMagnificationCallbacks();
+ Mockito.reset(mMockWindowManager);
+ int centerX = (int) INITIAL_MAGNIFICATION_BOUNDS_CENTER.x;
+ int centerY = (int) INITIAL_MAGNIFICATION_BOUNDS_CENTER.y;
+ callbacks.onRectangleOnScreenRequested(centerX - 1, centerY - 1, centerX + 1, centerY - 1);
+ mMessageCapturingHandler.sendAllMessages();
+ assertThat(getCurrentMagnificationSpec(), closeTo(startSpec));
+ verifyNoMoreInteractions(mMockWindowManager);
+ }
+
+ @Test
+ public void testRequestRectOnScreen_rectCanFitOnScreen_pansToGetRectOnScreen() {
+ mMagnificationController.register();
+ zoomIn2xToMiddle();
+ MagnificationCallbacks callbacks = getMagnificationCallbacks();
+ Mockito.reset(mMockWindowManager);
+ callbacks.onRectangleOnScreenRequested(0, 0, 1, 1);
+ mMessageCapturingHandler.sendAllMessages();
+ MagnificationSpec expectedEndSpec = getMagnificationSpec(2.0f, 0, 0);
+ assertThat(getCurrentMagnificationSpec(), closeTo(expectedEndSpec));
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(expectedEndSpec)));
+ }
+
+ @Test
+ public void testRequestRectOnScreen_garbageInput_doesNothing() {
+ mMagnificationController.register();
+ zoomIn2xToMiddle();
+ MagnificationSpec startSpec = getCurrentMagnificationSpec();
+ MagnificationCallbacks callbacks = getMagnificationCallbacks();
+ Mockito.reset(mMockWindowManager);
+ callbacks.onRectangleOnScreenRequested(0, 0, -50, -50);
+ mMessageCapturingHandler.sendAllMessages();
+ assertThat(getCurrentMagnificationSpec(), closeTo(startSpec));
+ verifyNoMoreInteractions(mMockWindowManager);
+ }
+
+
+ @Test
+ public void testRequestRectOnScreen_rectTooWide_pansToGetStartOnScreenBasedOnLocale() {
+ Locale.setDefault(new Locale("en", "us"));
+ mMagnificationController.register();
+ zoomIn2xToMiddle();
+ MagnificationCallbacks callbacks = getMagnificationCallbacks();
+ MagnificationSpec startSpec = getCurrentMagnificationSpec();
+ Mockito.reset(mMockWindowManager);
+ Rect wideRect = new Rect(0, 50, 100, 51);
+ callbacks.onRectangleOnScreenRequested(
+ wideRect.left, wideRect.top, wideRect.right, wideRect.bottom);
+ mMessageCapturingHandler.sendAllMessages();
+ MagnificationSpec expectedEndSpec = getMagnificationSpec(2.0f, 0, startSpec.offsetY);
+ assertThat(getCurrentMagnificationSpec(), closeTo(expectedEndSpec));
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(expectedEndSpec)));
+ Mockito.reset(mMockWindowManager);
+
+ // Repeat with RTL
+ Locale.setDefault(new Locale("he", "il"));
+ callbacks.onRectangleOnScreenRequested(
+ wideRect.left, wideRect.top, wideRect.right, wideRect.bottom);
+ mMessageCapturingHandler.sendAllMessages();
+ expectedEndSpec = getMagnificationSpec(2.0f, -100, startSpec.offsetY);
+ assertThat(getCurrentMagnificationSpec(), closeTo(expectedEndSpec));
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(expectedEndSpec)));
+ }
+
+ @Test
+ public void testRequestRectOnScreen_rectTooTall_pansMinimumToGetTopOnScreen() {
+ mMagnificationController.register();
+ zoomIn2xToMiddle();
+ MagnificationCallbacks callbacks = getMagnificationCallbacks();
+ MagnificationSpec startSpec = getCurrentMagnificationSpec();
+ Mockito.reset(mMockWindowManager);
+ Rect tallRect = new Rect(50, 0, 51, 100);
+ callbacks.onRectangleOnScreenRequested(
+ tallRect.left, tallRect.top, tallRect.right, tallRect.bottom);
+ mMessageCapturingHandler.sendAllMessages();
+ MagnificationSpec expectedEndSpec = getMagnificationSpec(2.0f, startSpec.offsetX, 0);
+ assertThat(getCurrentMagnificationSpec(), closeTo(expectedEndSpec));
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(expectedEndSpec)));
+ }
+
+ @Test
+ public void testChangeMagnification_duringAnimation_animatesToNewValue() {
+ mMagnificationController.register();
+ MagnificationSpec startSpec = getCurrentMagnificationSpec();
+ float scale = 2.5f;
+ PointF firstCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER;
+ MagnificationSpec firstEndSpec = getMagnificationSpec(
+ scale, computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, firstCenter, scale));
+
+ assertTrue(mMagnificationController.setScaleAndCenter(scale, firstCenter.x, firstCenter.y,
+ true, SERVICE_ID_1));
+
+ assertEquals(firstCenter.x, mMagnificationController.getCenterX(), 0.5);
+ assertEquals(firstCenter.y, mMagnificationController.getCenterY(), 0.5);
+ assertThat(getCurrentMagnificationSpec(), closeTo(firstEndSpec));
+ verify(mMockValueAnimator, times(1)).start();
+
+ // Initial value
+ when(mMockValueAnimator.getAnimatedFraction()).thenReturn(0.0f);
+ mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
+ verify(mMockWindowManager).setMagnificationSpec(startSpec);
+ verify(mMockAms).notifyMagnificationChanged(
+ INITIAL_MAGNIFICATION_REGION, scale, firstCenter.x, firstCenter.y);
+ Mockito.reset(mMockWindowManager);
+
+ // Intermediate point
+ float fraction = 0.33f;
+ when(mMockValueAnimator.getAnimatedFraction()).thenReturn(fraction);
+ mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
+ MagnificationSpec intermediateSpec1 =
+ getInterpolatedMagSpec(startSpec, firstEndSpec, fraction);
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(intermediateSpec1)));
+ Mockito.reset(mMockWindowManager);
+
+ PointF newCenter = INITIAL_BOUNDS_UPPER_LEFT_2X_CENTER;
+ MagnificationSpec newEndSpec = getMagnificationSpec(
+ scale, computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale));
+ assertTrue(mMagnificationController.setCenter(
+ newCenter.x, newCenter.y, true, SERVICE_ID_1));
+
+ // Animation should have been restarted
+ verify(mMockValueAnimator, times(2)).start();
+ verify(mMockAms).notifyMagnificationChanged(
+ INITIAL_MAGNIFICATION_REGION, scale, newCenter.x, newCenter.y);
+
+ // New starting point should be where we left off
+ when(mMockValueAnimator.getAnimatedFraction()).thenReturn(0.0f);
+ mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(intermediateSpec1)));
+ Mockito.reset(mMockWindowManager);
+
+ // Second intermediate point
+ fraction = 0.5f;
+ when(mMockValueAnimator.getAnimatedFraction()).thenReturn(fraction);
+ mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
+ verify(mMockWindowManager).setMagnificationSpec(
+ argThat(closeTo(getInterpolatedMagSpec(intermediateSpec1, newEndSpec, fraction))));
+ Mockito.reset(mMockWindowManager);
+
+ // Final value should be the new center
+ Mockito.reset(mMockWindowManager);
+ when(mMockValueAnimator.getAnimatedFraction()).thenReturn(1.0f);
+ mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
+ verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(newEndSpec)));
+ }
+
+ private void zoomIn2xToMiddle() {
+ PointF startCenter = INITIAL_MAGNIFICATION_BOUNDS_CENTER;
+ float scale = 2.0f;
+ mMagnificationController.setScale(scale, startCenter.x, startCenter.y, false, SERVICE_ID_1);
+ assertTrue(mMagnificationController.isMagnifying());
+ }
+
+ private MagnificationCallbacks getMagnificationCallbacks() {
+ ArgumentCaptor<MagnificationCallbacks> magnificationCallbacksCaptor =
+ ArgumentCaptor.forClass(MagnificationCallbacks.class);
+ verify(mMockWindowManager)
+ .setMagnificationCallbacks(magnificationCallbacksCaptor.capture());
+ return magnificationCallbacksCaptor.getValue();
+ }
+
+ private PointF computeOffsets(Rect magnifiedBounds, PointF center, float scale) {
+ return new PointF(
+ magnifiedBounds.centerX() - scale * center.x,
+ magnifiedBounds.centerY() - scale * center.y);
+ }
+
+ private MagnificationSpec getInterpolatedMagSpec(MagnificationSpec start, MagnificationSpec end,
+ float fraction) {
+ MagnificationSpec interpolatedSpec = MagnificationSpec.obtain();
+ interpolatedSpec.scale = start.scale + fraction * (end.scale - start.scale);
+ interpolatedSpec.offsetX = start.offsetX + fraction * (end.offsetX - start.offsetX);
+ interpolatedSpec.offsetY = start.offsetY + fraction * (end.offsetY - start.offsetY);
+ return interpolatedSpec;
+ }
+
+ private MagnificationSpec getMagnificationSpec(float scale, PointF offsets) {
+ return getMagnificationSpec(scale, offsets.x, offsets.y);
+ }
+
+ private MagnificationSpec getMagnificationSpec(float scale, float offsetX, float offsetY) {
+ MagnificationSpec spec = MagnificationSpec.obtain();
+ spec.scale = scale;
+ spec.offsetX = offsetX;
+ spec.offsetY = offsetY;
+ return spec;
+ }
+
+ private MagnificationSpec getCurrentMagnificationSpec() {
+ return getMagnificationSpec(mMagnificationController.getScale(),
+ mMagnificationController.getOffsetX(), mMagnificationController.getOffsetY());
+ }
+
+ private MagSpecMatcher closeTo(MagnificationSpec spec) {
+ return new MagSpecMatcher(spec, 0.01f, 0.5f);
+ }
+
+ private class MagSpecMatcher extends TypeSafeMatcher<MagnificationSpec> {
+ final MagnificationSpec mMagSpec;
+ final float mScaleTolerance;
+ final float mOffsetTolerance;
+
+ MagSpecMatcher(MagnificationSpec spec, float scaleTolerance, float offsetTolerance) {
+ mMagSpec = spec;
+ mScaleTolerance = scaleTolerance;
+ mOffsetTolerance = offsetTolerance;
+ }
+
+ @Override
+ protected boolean matchesSafely(MagnificationSpec magnificationSpec) {
+ if (Math.abs(mMagSpec.scale - magnificationSpec.scale) > mScaleTolerance) {
+ return false;
+ }
+ if (Math.abs(mMagSpec.offsetX - magnificationSpec.offsetX) > mOffsetTolerance) {
+ return false;
+ }
+ if (Math.abs(mMagSpec.offsetY - magnificationSpec.offsetY) > mOffsetTolerance) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("Match spec: " + mMagSpec);
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MessageCapturingHandler.java b/services/tests/servicestests/src/com/android/server/accessibility/MessageCapturingHandler.java
new file mode 100644
index 0000000..003f7ab
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MessageCapturingHandler.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 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.os.Handler;
+import android.os.Message;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class to capture messages dispatched through a handler and control when they arrive
+ * at their target.
+ */
+public class MessageCapturingHandler extends Handler {
+ List<Pair<Message, Long>> timedMessages = new ArrayList<>();
+
+ Handler.Callback mCallback;
+
+ public MessageCapturingHandler(Handler.Callback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public boolean sendMessageAtTime(Message message, long uptimeMillis) {
+ timedMessages.add(new Pair<>(Message.obtain(message), uptimeMillis));
+ return super.sendMessageAtTime(message, uptimeMillis);
+ }
+
+ public void sendOneMessage() {
+ Message message = timedMessages.remove(0).first;
+ removeMessages(message.what, message.obj);
+ mCallback.handleMessage(message);
+ removeStaleMessages();
+ }
+
+ public void sendAllMessages() {
+ while (!timedMessages.isEmpty()) {
+ sendOneMessage();
+ }
+ }
+
+ public void sendLastMessage() {
+ Message message = timedMessages.remove(timedMessages.size() - 1).first;
+ removeMessages(message.what, message.obj);
+ mCallback.handleMessage(message);
+ removeStaleMessages();
+ }
+
+ public boolean hasMessages() {
+ removeStaleMessages();
+ return !timedMessages.isEmpty();
+ }
+
+ private void removeStaleMessages() {
+ for (int i = 0; i < timedMessages.size(); i++) {
+ Message message = timedMessages.get(i).first;
+ if (!hasMessages(message.what, message.obj)) {
+ timedMessages.remove(i--);
+ }
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java b/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java
index 5920fef..d5305d9 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java
@@ -92,7 +92,12 @@
@Before
public void setUp() {
- mMessageCapturingHandler = new MessageCapturingHandler();
+ mMessageCapturingHandler = new MessageCapturingHandler(new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ return mMotionEventInjector.handleMessage(msg);
+ }
+ });
mMotionEventInjector = new MotionEventInjector(mMessageCapturingHandler);
mClickList.add(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, CLICK_X, CLICK_Y_START, 0));
@@ -501,48 +506,4 @@
return false;
}
}
-
- private class MessageCapturingHandler extends Handler {
- List<Pair<Message, Long>> timedMessages = new ArrayList<>();
-
- @Override
- public boolean sendMessageAtTime(Message message, long uptimeMillis) {
- timedMessages.add(new Pair<>(Message.obtain(message), uptimeMillis));
- return super.sendMessageAtTime(message, uptimeMillis);
- }
-
- void sendOneMessage() {
- Message message = timedMessages.remove(0).first;
- removeMessages(message.what, message.obj);
- mMotionEventInjector.handleMessage(message);
- removeStaleMessages();
- }
-
- void sendAllMessages() {
- while (!timedMessages.isEmpty()) {
- sendOneMessage();
- }
- }
-
- void sendLastMessage() {
- Message message = timedMessages.remove(timedMessages.size() - 1).first;
- removeMessages(message.what, message.obj);
- mMotionEventInjector.handleMessage(message);
- removeStaleMessages();
- }
-
- boolean hasMessages() {
- removeStaleMessages();
- return !timedMessages.isEmpty();
- }
-
- private void removeStaleMessages() {
- for (int i = 0; i < timedMessages.size(); i++) {
- Message message = timedMessages.get(i).first;
- if (!hasMessages(message.what, message.obj)) {
- timedMessages.remove(i--);
- }
- }
- }
- }
}