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--);
-                }
-            }
-        }
-    }
 }