Merge "Move DecorView out of PhoneWindow into its own class file."
diff --git a/api/current.txt b/api/current.txt
index de9abd0..cca8293 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -332,6 +332,7 @@
     field public static final int calendarTextColor = 16843931; // 0x101049b
     field public static final int calendarViewShown = 16843596; // 0x101034c
     field public static final int calendarViewStyle = 16843613; // 0x101035d
+    field public static final int canControlMagnification = 16844040; // 0x1010508
     field public static final int canRequestEnhancedWebAccessibility = 16843736; // 0x10103d8
     field public static final int canRequestFilterKeyEvents = 16843737; // 0x10103d9
     field public static final int canRequestTouchExplorationMode = 16843735; // 0x10103d7
@@ -2619,6 +2620,7 @@
   public abstract class AccessibilityService extends android.app.Service {
     ctor public AccessibilityService();
     method public android.view.accessibility.AccessibilityNodeInfo findFocus(int);
+    method public final android.accessibilityservice.AccessibilityService.MagnificationController getMagnificationController();
     method public android.view.accessibility.AccessibilityNodeInfo getRootInActiveWindow();
     method public final android.accessibilityservice.AccessibilityServiceInfo getServiceInfo();
     method public java.util.List<android.view.accessibility.AccessibilityWindowInfo> getWindows();
@@ -2656,6 +2658,23 @@
     field public static final java.lang.String SERVICE_META_DATA = "android.accessibilityservice";
   }
 
+  public static final class AccessibilityService.MagnificationController {
+    method public void addListener(android.accessibilityservice.AccessibilityService.MagnificationController.OnMagnificationChangedListener);
+    method public void addListener(android.accessibilityservice.AccessibilityService.MagnificationController.OnMagnificationChangedListener, android.os.Handler);
+    method public float getCenterX();
+    method public float getCenterY();
+    method public android.graphics.Region getMagnifiedRegion();
+    method public float getScale();
+    method public boolean removeListener(android.accessibilityservice.AccessibilityService.MagnificationController.OnMagnificationChangedListener);
+    method public boolean reset(boolean);
+    method public boolean setCenter(float, float, boolean);
+    method public boolean setScale(float, boolean);
+  }
+
+  public static abstract interface AccessibilityService.MagnificationController.OnMagnificationChangedListener {
+    method public abstract void onMagnificationChanged(android.accessibilityservice.AccessibilityService.MagnificationController, android.graphics.Region, float, float, float);
+  }
+
   public class AccessibilityServiceInfo implements android.os.Parcelable {
     ctor public AccessibilityServiceInfo();
     method public static java.lang.String capabilityToString(int);
@@ -2670,6 +2689,7 @@
     method public java.lang.String getSettingsActivityName();
     method public java.lang.String loadDescription(android.content.pm.PackageManager);
     method public void writeToParcel(android.os.Parcel, int);
+    field public static final int CAPABILITY_CAN_CONTROL_MAGNIFICATION = 16; // 0x10
     field public static final int CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 4; // 0x4
     field public static final int CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS = 8; // 0x8
     field public static final int CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION = 2; // 0x2
diff --git a/api/system-current.txt b/api/system-current.txt
index 65ecdc1..cd6c58e 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -424,6 +424,7 @@
     field public static final int calendarTextColor = 16843931; // 0x101049b
     field public static final int calendarViewShown = 16843596; // 0x101034c
     field public static final int calendarViewStyle = 16843613; // 0x101035d
+    field public static final int canControlMagnification = 16844040; // 0x1010508
     field public static final int canRequestEnhancedWebAccessibility = 16843736; // 0x10103d8
     field public static final int canRequestFilterKeyEvents = 16843737; // 0x10103d9
     field public static final int canRequestTouchExplorationMode = 16843735; // 0x10103d7
@@ -2718,6 +2719,7 @@
   public abstract class AccessibilityService extends android.app.Service {
     ctor public AccessibilityService();
     method public android.view.accessibility.AccessibilityNodeInfo findFocus(int);
+    method public final android.accessibilityservice.AccessibilityService.MagnificationController getMagnificationController();
     method public android.view.accessibility.AccessibilityNodeInfo getRootInActiveWindow();
     method public final android.accessibilityservice.AccessibilityServiceInfo getServiceInfo();
     method public java.util.List<android.view.accessibility.AccessibilityWindowInfo> getWindows();
@@ -2755,6 +2757,23 @@
     field public static final java.lang.String SERVICE_META_DATA = "android.accessibilityservice";
   }
 
+  public static final class AccessibilityService.MagnificationController {
+    method public void addListener(android.accessibilityservice.AccessibilityService.MagnificationController.OnMagnificationChangedListener);
+    method public void addListener(android.accessibilityservice.AccessibilityService.MagnificationController.OnMagnificationChangedListener, android.os.Handler);
+    method public float getCenterX();
+    method public float getCenterY();
+    method public android.graphics.Region getMagnifiedRegion();
+    method public float getScale();
+    method public boolean removeListener(android.accessibilityservice.AccessibilityService.MagnificationController.OnMagnificationChangedListener);
+    method public boolean reset(boolean);
+    method public boolean setCenter(float, float, boolean);
+    method public boolean setScale(float, boolean);
+  }
+
+  public static abstract interface AccessibilityService.MagnificationController.OnMagnificationChangedListener {
+    method public abstract void onMagnificationChanged(android.accessibilityservice.AccessibilityService.MagnificationController, android.graphics.Region, float, float, float);
+  }
+
   public class AccessibilityServiceInfo implements android.os.Parcelable {
     ctor public AccessibilityServiceInfo();
     method public static java.lang.String capabilityToString(int);
@@ -2769,6 +2788,7 @@
     method public java.lang.String getSettingsActivityName();
     method public java.lang.String loadDescription(android.content.pm.PackageManager);
     method public void writeToParcel(android.os.Parcel, int);
+    field public static final int CAPABILITY_CAN_CONTROL_MAGNIFICATION = 16; // 0x10
     field public static final int CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 4; // 0x4
     field public static final int CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS = 8; // 0x8
     field public static final int CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION = 2; // 0x2
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index 9d6aa13..273483a 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -17,14 +17,19 @@
 package android.accessibilityservice;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.Region;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
+import android.util.ArrayMap;
 import android.util.Log;
+import android.util.Slog;
 import android.view.KeyEvent;
 import android.view.WindowManager;
 import android.view.WindowManagerImpl;
@@ -36,7 +41,10 @@
 import com.android.internal.os.HandlerCaller;
 import com.android.internal.os.SomeArgs;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
 
 /**
  * An accessibility service runs in the background and receives callbacks by the system
@@ -373,6 +381,8 @@
         public void init(int connectionId, IBinder windowToken);
         public boolean onGesture(int gestureId);
         public boolean onKeyEvent(KeyEvent event);
+        public void onMagnificationChanged(@NonNull Region region,
+                float scale, float centerX, float centerY);
     }
 
     private int mConnectionId;
@@ -383,6 +393,8 @@
 
     private WindowManager mWindowManager;
 
+    private MagnificationController mMagnificationController;
+
     /**
      * Callback for {@link android.view.accessibility.AccessibilityEvent}s.
      *
@@ -396,6 +408,20 @@
     public abstract void onInterrupt();
 
     /**
+     * Dispatches service connection to internal components first, then the
+     * client code.
+     */
+    private void dispatchServiceConnected() {
+        if (mMagnificationController != null) {
+            mMagnificationController.onServiceConnected();
+        }
+
+        // The client gets to handle service connection last, after we've set
+        // up any state upon which their code may rely.
+        onServiceConnected();
+    }
+
+    /**
      * This method is a part of the {@link AccessibilityService} lifecycle and is
      * called after the system has successfully bound to the service. If is
      * convenient to use this method for setting the {@link AccessibilityServiceInfo}.
@@ -513,6 +539,385 @@
     }
 
     /**
+     * Returns the magnification controller, which may be used to query and
+     * modify the state of display magnification.
+     * <p>
+     * <strong>Note:</strong> In order to control magnification, your service
+     * must declare the capability by setting the
+     * {@link android.R.styleable#AccessibilityService_canControlMagnification}
+     * property in its meta-data. For more information, see
+     * {@link #SERVICE_META_DATA}.
+     *
+     * @return the magnification controller
+     */
+    @NonNull
+    public final MagnificationController getMagnificationController() {
+        if (mMagnificationController == null) {
+            mMagnificationController = new MagnificationController(this);
+        }
+        return mMagnificationController;
+    }
+
+    private void onMagnificationChanged(@NonNull Region region, float scale,
+            float centerX, float centerY) {
+        if (mMagnificationController != null) {
+            mMagnificationController.dispatchMagnificationChanged(
+                    region, scale, centerX, centerY);
+        }
+    }
+
+    /**
+     * Used to control and query the state of display magnification.
+     */
+    public static final class MagnificationController {
+        private final AccessibilityService mService;
+
+        /**
+         * Map of listeners to their handlers. Lazily created when adding the
+         * first magnification listener.
+         */
+        private ArrayMap<OnMagnificationChangedListener, Handler> mListeners;
+
+        MagnificationController(@NonNull AccessibilityService service) {
+            mService = service;
+        }
+
+        /**
+         * Called when the service is connected.
+         */
+        void onServiceConnected() {
+            if (mListeners != null && !mListeners.isEmpty()) {
+                setMagnificationCallbackEnabled(true);
+            }
+        }
+
+        /**
+         * Adds the specified change listener to the list of magnification
+         * change listeners. The callback will occur on the service's main
+         * thread.
+         *
+         * @param listener the listener to add, must be non-{@code null}
+         */
+        public void addListener(@NonNull OnMagnificationChangedListener listener) {
+            addListener(listener, null);
+        }
+
+        /**
+         * Adds the specified change listener to the list of magnification
+         * change listeners. The callback will occur on the specified
+         * {@link Handler}'s thread, or on the service's main thread if the
+         * handler is {@code null}.
+         *
+         * @param listener the listener to add, must be non-null
+         * @param handler the handler on which the callback should execute, or
+         *        {@code null} to execute on the service's main thread
+         */
+        public void addListener(@NonNull OnMagnificationChangedListener listener,
+                @Nullable Handler handler) {
+            if (mListeners == null) {
+                mListeners = new ArrayMap<>();
+            }
+
+            final boolean shouldEnableCallback = mListeners.isEmpty();
+            mListeners.put(listener, handler);
+
+            if (shouldEnableCallback) {
+                // This may fail if the service is not connected yet, but if we
+                // still have listeners when it connects then we can try again.
+                setMagnificationCallbackEnabled(true);
+            }
+        }
+
+        /**
+         * Removes all instances of the specified change listener from the list
+         * of magnification change listeners.
+         *
+         * @param listener the listener to remove, must be non-null
+         * @return {@code true} if at least one instance of the listener was
+         *         removed
+         */
+        public boolean removeListener(@NonNull OnMagnificationChangedListener listener) {
+            if (mListeners == null) {
+                return false;
+            }
+
+            final int keyIndex = mListeners.indexOfKey(listener);
+            final boolean hasKey = keyIndex >= 0;
+            if (hasKey) {
+                mListeners.removeAt(keyIndex);
+            }
+
+            if (hasKey && mListeners.isEmpty()) {
+                // We just removed the last listener, so we don't need
+                // callbacks from the service anymore.
+                setMagnificationCallbackEnabled(false);
+            }
+
+            return hasKey;
+        }
+
+        private void setMagnificationCallbackEnabled(boolean enabled) {
+            final IAccessibilityServiceConnection connection =
+                    AccessibilityInteractionClient.getInstance().getConnection(
+                            mService.mConnectionId);
+            if (connection != null) {
+                try {
+                    connection.setMagnificationCallbackEnabled(enabled);
+                } catch (RemoteException re) {
+                    throw new RuntimeException(re);
+                }
+            }
+        }
+
+        /**
+         * Dispatches magnification changes to any registered listeners. This
+         * should be called on the service's main thread.
+         */
+        void dispatchMagnificationChanged(final @NonNull Region region, final float scale,
+                final float centerX, final float centerY) {
+            if (mListeners == null || mListeners.isEmpty()) {
+                Slog.d(LOG_TAG, "Received magnification changed "
+                        + "callback with no listeners registered!");
+                setMagnificationCallbackEnabled(false);
+                return;
+            }
+
+            // Listeners may remove themselves. Perform a shallow copy to avoid
+            // concurrent modification.
+            final ArrayMap<OnMagnificationChangedListener, Handler> entries =
+                    new ArrayMap<>(mListeners);
+
+            for (int i = 0, count = entries.size(); i < count; i++) {
+                final OnMagnificationChangedListener listener = entries.keyAt(i);
+                final Handler handler = entries.valueAt(i);
+                if (handler != null) {
+                    handler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            listener.onMagnificationChanged(MagnificationController.this,
+                                    region, scale, centerX, centerY);
+                        }
+                    });
+                } else {
+                    // We're already on the main thread, just run the listener.
+                    listener.onMagnificationChanged(this, region, scale, centerX, centerY);
+                }
+            }
+        }
+
+        /**
+         * Returns the current magnification scale.
+         * <p>
+         * <strong>Note:</strong> If the service is not yet connected (e.g.
+         * {@link AccessibilityService#onServiceConnected()} has not yet been
+         * called) or the service has been disconnected, this method will
+         * return a default value of {@code 1.0f}.
+         *
+         * @return the current magnification scale
+         */
+        public float getScale() {
+            final IAccessibilityServiceConnection connection =
+                    AccessibilityInteractionClient.getInstance().getConnection(
+                            mService.mConnectionId);
+            if (connection != null) {
+                try {
+                    return connection.getMagnificationScale();
+                } catch (RemoteException re) {
+                    Log.w(LOG_TAG, "Failed to obtain scale", re);
+                }
+            }
+            return 1.0f;
+        }
+
+        /**
+         * Returns the unscaled screen-relative X coordinate of the focal
+         * center of the magnified region. This is the point around which
+         * zooming occurs and is guaranteed to lie within the magnified
+         * region.
+         * <p>
+         * <strong>Note:</strong> If the service is not yet connected (e.g.
+         * {@link AccessibilityService#onServiceConnected()} has not yet been
+         * called) or the service has been disconnected, this method will
+         * return a default value of {@code 0.0f}.
+         *
+         * @return the unscaled screen-relative X coordinate of the center of
+         *         the magnified region
+         */
+        public float getCenterX() {
+            final IAccessibilityServiceConnection connection =
+                    AccessibilityInteractionClient.getInstance().getConnection(
+                            mService.mConnectionId);
+            if (connection != null) {
+                try {
+                    return connection.getMagnificationCenterX();
+                } catch (RemoteException re) {
+                    Log.w(LOG_TAG, "Failed to obtain center X", re);
+                }
+            }
+            return 0.0f;
+        }
+
+        /**
+         * Returns the unscaled screen-relative Y coordinate of the focal
+         * center of the magnified region. This is the point around which
+         * zooming occurs and is guaranteed to lie within the magnified
+         * region.
+         * <p>
+         * <strong>Note:</strong> If the service is not yet connected (e.g.
+         * {@link AccessibilityService#onServiceConnected()} has not yet been
+         * called) or the service has been disconnected, this method will
+         * return a default value of {@code 0.0f}.
+         *
+         * @return the unscaled screen-relative Y coordinate of the center of
+         *         the magnified region
+         */
+        public float getCenterY() {
+            final IAccessibilityServiceConnection connection =
+                    AccessibilityInteractionClient.getInstance().getConnection(
+                            mService.mConnectionId);
+            if (connection != null) {
+                try {
+                    return connection.getMagnificationCenterY();
+                } catch (RemoteException re) {
+                    Log.w(LOG_TAG, "Failed to obtain center Y", re);
+                }
+            }
+            return 0.0f;
+        }
+
+        /**
+         * Returns the region of the screen currently being magnified. If
+         * magnification is not enabled, the returned region will be empty.
+         * <p>
+         * <strong>Note:</strong> If the service is not yet connected (e.g.
+         * {@link AccessibilityService#onServiceConnected()} has not yet been
+         * called) or the service has been disconnected, this method will
+         * return an empty region.
+         *
+         * @return the screen-relative bounds of the magnified region
+         */
+        @NonNull
+        public Region getMagnifiedRegion() {
+            final IAccessibilityServiceConnection connection =
+                    AccessibilityInteractionClient.getInstance().getConnection(
+                            mService.mConnectionId);
+            if (connection != null) {
+                try {
+                    return connection.getMagnifiedRegion();
+                } catch (RemoteException re) {
+                    Log.w(LOG_TAG, "Failed to obtain magnified region", re);
+                }
+            }
+            return Region.obtain();
+        }
+
+        /**
+         * Resets magnification scale and center to their default (e.g. no
+         * magnification) values.
+         * <p>
+         * <strong>Note:</strong> If the service is not yet connected (e.g.
+         * {@link AccessibilityService#onServiceConnected()} has not yet been
+         * called) or the service has been disconnected, this method will have
+         * no effect and return {@code false}.
+         *
+         * @param animate {@code true} to animate from the current scale and
+         *                center or {@code false} to reset the scale and center
+         *                immediately
+         * @return {@code true} on success, {@code false} on failure
+         */
+        public boolean reset(boolean animate) {
+            final IAccessibilityServiceConnection connection =
+                    AccessibilityInteractionClient.getInstance().getConnection(
+                            mService.mConnectionId);
+            if (connection != null) {
+                try {
+                    return connection.resetMagnification(animate);
+                } catch (RemoteException re) {
+                    Log.w(LOG_TAG, "Failed to reset", re);
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Sets the magnification scale.
+         * <p>
+         * <strong>Note:</strong> If the service is not yet connected (e.g.
+         * {@link AccessibilityService#onServiceConnected()} has not yet been
+         * called) or the service has been disconnected, this method will have
+         * no effect and return {@code false}.
+         *
+         * @param scale the magnification scale to set, must be >= 1 and <= 5
+         * @param animate {@code true} to animate from the current scale or
+         *                {@code false} to set the scale immediately
+         * @return {@code true} on success, {@code false} on failure
+         */
+        public boolean setScale(float scale, boolean animate) {
+            final IAccessibilityServiceConnection connection =
+                    AccessibilityInteractionClient.getInstance().getConnection(
+                            mService.mConnectionId);
+            if (connection != null) {
+                try {
+                    return connection.setMagnificationScaleAndCenter(
+                            scale, Float.NaN, Float.NaN, animate);
+                } catch (RemoteException re) {
+                    Log.w(LOG_TAG, "Failed to set scale", re);
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Sets the center of the magnified viewport.
+         * <p>
+         * <strong>Note:</strong> If the service is not yet connected (e.g.
+         * {@link AccessibilityService#onServiceConnected()} has not yet been
+         * called) or the service has been disconnected, this method will have
+         * no effect and return {@code false}.
+         *
+         * @param centerX the unscaled screen-relative X coordinate on which to
+         *                center the viewport
+         * @param centerY the unscaled screen-relative Y coordinate on which to
+         *                center the viewport
+         * @param animate {@code true} to animate from the current viewport
+         *                center or {@code false} to set the center immediately
+         * @return {@code true} on success, {@code false} on failure
+         */
+        public boolean setCenter(float centerX, float centerY, boolean animate) {
+            final IAccessibilityServiceConnection connection =
+                    AccessibilityInteractionClient.getInstance().getConnection(
+                            mService.mConnectionId);
+            if (connection != null) {
+                try {
+                    return connection.setMagnificationScaleAndCenter(
+                            Float.NaN, centerX, centerY, animate);
+                } catch (RemoteException re) {
+                    Log.w(LOG_TAG, "Failed to set center", re);
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Listener for changes in the state of magnification.
+         */
+        public interface OnMagnificationChangedListener {
+            /**
+             * Called when the magnified region, scale, or center changes.
+             *
+             * @param controller the magnification controller
+             * @param region the new magnified region, may be empty if
+             *               magnification is not enabled (e.g. scale is 1)
+             * @param scale the new scale
+             * @param centerX the new X coordinate around which magnification is focused
+             * @param centerY the new Y coordinate around which magnification is focused
+             */
+            void onMagnificationChanged(@NonNull MagnificationController controller,
+                    @NonNull Region region, float scale, float centerX, float centerY);
+        }
+    }
+
+    /**
      * Performs a global action. Such an action can be performed
      * at any moment regardless of the current application or user
      * location in that application. For example going back, going
@@ -645,7 +1050,7 @@
         return new IAccessibilityServiceClientWrapper(this, getMainLooper(), new Callbacks() {
             @Override
             public void onServiceConnected() {
-                AccessibilityService.this.onServiceConnected();
+                AccessibilityService.this.dispatchServiceConnected();
             }
 
             @Override
@@ -678,6 +1083,12 @@
             public boolean onKeyEvent(KeyEvent event) {
                 return AccessibilityService.this.onKeyEvent(event);
             }
+
+            @Override
+            public void onMagnificationChanged(@NonNull Region region,
+                    float scale, float centerX, float centerY) {
+                AccessibilityService.this.onMagnificationChanged(region, scale, centerX, centerY);
+            }
         });
     }
 
@@ -695,6 +1106,7 @@
         private static final int DO_ON_GESTURE = 4;
         private static final int DO_CLEAR_ACCESSIBILITY_CACHE = 5;
         private static final int DO_ON_KEY_EVENT = 6;
+        private static final int DO_ON_MAGNIFICATION_CHANGED = 7;
 
         private final HandlerCaller mCaller;
 
@@ -741,6 +1153,18 @@
             mCaller.sendMessage(message);
         }
 
+        public void onMagnificationChanged(@NonNull Region region,
+                float scale, float centerX, float centerY) {
+            final SomeArgs args = SomeArgs.obtain();
+            args.arg1 = region;
+            args.arg2 = scale;
+            args.arg3 = centerX;
+            args.arg4 = centerY;
+
+            final Message message = mCaller.obtainMessageO(DO_ON_MAGNIFICATION_CHANGED, args);
+            mCaller.sendMessage(message);
+        }
+
         @Override
         public void executeMessage(Message message) {
             switch (message.what) {
@@ -816,6 +1240,15 @@
                     }
                 } return;
 
+                case DO_ON_MAGNIFICATION_CHANGED: {
+                    final SomeArgs args = (SomeArgs) message.obj;
+                    final Region region = (Region) args.arg1;
+                    final float scale = (float) args.arg2;
+                    final float centerX = (float) args.arg3;
+                    final float centerY = (float) args.arg4;
+                    mCallback.onMagnificationChanged(region, scale, centerX, centerY);
+                } return;
+
                 default :
                     Log.w(LOG_TAG, "Unknown message type " + message.what);
             }
diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
index 4edb0c6..2c98fef 100644
--- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
+++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
@@ -104,6 +104,12 @@
      */
     public static final int CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS = 0x00000008;
 
+    /**
+     * Capability: This accessibility service can control display magnification.
+     * @see android.R.styleable#AccessibilityService_canControlMagnification
+     */
+    public static final int CAPABILITY_CAN_CONTROL_MAGNIFICATION = 0x00000010;
+
     private static final SparseArray<CapabilityInfo> sAvailableCapabilityInfos =
             new SparseArray<CapabilityInfo>();
     static {
@@ -123,6 +129,10 @@
                 new CapabilityInfo(CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS,
                         R.string.capability_title_canRequestFilterKeyEvents,
                         R.string.capability_desc_canRequestFilterKeyEvents));
+        sAvailableCapabilityInfos.put(CAPABILITY_CAN_CONTROL_MAGNIFICATION,
+                new CapabilityInfo(CAPABILITY_CAN_CONTROL_MAGNIFICATION,
+                        R.string.capability_title_canControlMagnification,
+                        R.string.capability_desc_canControlMagnification));
     }
 
     /**
@@ -502,6 +512,10 @@
                     .AccessibilityService_canRequestFilterKeyEvents, false)) {
                 mCapabilities |= CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS;
             }
+            if (asAttributes.getBoolean(com.android.internal.R.styleable
+                    .AccessibilityService_canControlMagnification, false)) {
+                mCapabilities |= CAPABILITY_CAN_CONTROL_MAGNIFICATION;
+            }
             TypedValue peekedValue = asAttributes.peekValue(
                     com.android.internal.R.styleable.AccessibilityService_description);
             if (peekedValue != null) {
@@ -917,6 +931,8 @@
                 return "CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY";
             case CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS:
                 return "CAPABILITY_CAN_FILTER_KEY_EVENTS";
+            case CAPABILITY_CAN_CONTROL_MAGNIFICATION:
+                return "CAPABILITY_CAN_CONTROL_MAGNIFICATION";
             default:
                 return "UNKNOWN";
         }
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
index 8b503dd..15666bf 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
@@ -17,6 +17,7 @@
 package android.accessibilityservice;
 
 import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.graphics.Region;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityWindowInfo;
 import android.view.KeyEvent;
@@ -39,4 +40,6 @@
     void clearAccessibilityCache();
 
     void onKeyEvent(in KeyEvent event, int sequence);
+
+    void onMagnificationChanged(in Region region, float scale, float centerX, float centerY);
 }
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index 5f7a17d..6ac50bd 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -18,6 +18,7 @@
 
 import android.os.Bundle;
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.graphics.Region;
 import android.view.MagnificationSpec;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
@@ -63,4 +64,19 @@
     boolean performGlobalAction(int action);
 
     oneway void setOnKeyEventResult(boolean handled, int sequence);
+
+    float getMagnificationScale();
+
+    float getMagnificationCenterX();
+
+    float getMagnificationCenterY();
+
+    Region getMagnifiedRegion();
+
+    boolean resetMagnification(boolean animate);
+
+    boolean setMagnificationScaleAndCenter(float scale, float centerX, float centerY,
+        boolean animate);
+
+    void setMagnificationCallbackEnabled(boolean enabled);
 }
diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java
index efed2e0..f7848f9 100644
--- a/core/java/android/app/UiAutomation.java
+++ b/core/java/android/app/UiAutomation.java
@@ -21,9 +21,11 @@
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.annotation.NonNull;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Point;
+import android.graphics.Region;
 import android.hardware.display.DisplayManagerGlobal;
 import android.os.IBinder;
 import android.os.Looper;
@@ -1020,6 +1022,12 @@
                 public boolean onKeyEvent(KeyEvent event) {
                     return false;
                 }
+
+                @Override
+                public void onMagnificationChanged(@NonNull Region region,
+                        float scale, float centerX, float centerY) {
+                    /* do nothing */
+                }
             });
         }
     }
diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java
index 25d9aa9..09a15de 100644
--- a/core/java/android/bluetooth/BluetoothHeadset.java
+++ b/core/java/android/bluetooth/BluetoothHeadset.java
@@ -685,6 +685,48 @@
     }
 
     /**
+     * Sets whether audio routing is allowed. When set to {@code false}, the AG will not route any
+     * audio to the HF unless explicitly told to.
+     * This method should be used in cases where the SCO channel is shared between multiple profiles
+     * and must be delegated by a source knowledgeable
+     * Note: This is an internal function and shouldn't be exposed
+     *
+     * @param allowed {@code true} if the profile can reroute audio, {@code false} otherwise.
+     *
+     * @hide
+     */
+    public void setAudioRouteAllowed(boolean allowed) {
+        if (VDBG) log("setAudioRouteAllowed");
+        if (mService != null && isEnabled()) {
+            try {
+                mService.setAudioRouteAllowed(allowed);
+            } catch (RemoteException e) {Log.e(TAG, e.toString());}
+        } else {
+            Log.w(TAG, "Proxy not attached to service");
+            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
+        }
+    }
+
+    /**
+     * Returns whether audio routing is allowed. see {@link #setAudioRouteAllowed(boolean)}.
+     * Note: This is an internal function and shouldn't be exposed
+     *
+     * @hide
+     */
+    public boolean getAudioRouteAllowed() {
+        if (VDBG) log("getAudioRouteAllowed");
+        if (mService != null && isEnabled()) {
+            try {
+                return mService.getAudioRouteAllowed();
+            } catch (RemoteException e) {Log.e(TAG, e.toString());}
+        } else {
+            Log.w(TAG, "Proxy not attached to service");
+            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
+        }
+        return false;
+    }
+
+    /**
      * Check if Bluetooth SCO audio is connected.
      *
      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
diff --git a/core/java/android/bluetooth/IBluetoothHeadset.aidl b/core/java/android/bluetooth/IBluetoothHeadset.aidl
index 0e23fad..0bb4088 100755
--- a/core/java/android/bluetooth/IBluetoothHeadset.aidl
+++ b/core/java/android/bluetooth/IBluetoothHeadset.aidl
@@ -50,6 +50,8 @@
     boolean isAudioOn();
     boolean connectAudio();
     boolean disconnectAudio();
+    void setAudioRouteAllowed(boolean allowed);
+    boolean getAudioRouteAllowed();
     boolean startScoUsingVirtualVoiceCall(in BluetoothDevice device);
     boolean stopScoUsingVirtualVoiceCall(in BluetoothDevice device);
     void phoneStateChanged(int numActive, int numHeld, int callState, String number, int type);
diff --git a/core/java/android/view/MagnificationSpec.java b/core/java/android/view/MagnificationSpec.java
index 0ee6714..49242bb 100644
--- a/core/java/android/view/MagnificationSpec.java
+++ b/core/java/android/view/MagnificationSpec.java
@@ -28,10 +28,21 @@
 public class MagnificationSpec implements Parcelable {
     private static final int MAX_POOL_SIZE = 20;
     private static final SynchronizedPool<MagnificationSpec> sPool =
-            new SynchronizedPool<MagnificationSpec>(MAX_POOL_SIZE);
+            new SynchronizedPool<>(MAX_POOL_SIZE);
 
+    /** The magnification scaling factor. */
     public float scale = 1.0f;
+
+    /**
+     * The X coordinate, in unscaled screen-relative pixels, around which
+     * magnification is focused.
+     */
     public float offsetX;
+
+    /**
+     * The Y coordinate, in unscaled screen-relative pixels, around which
+     * magnification is focused.
+     */
     public float offsetY;
 
     private MagnificationSpec() {
@@ -75,6 +86,12 @@
        offsetY = 0.0f;
     }
 
+    public void setTo(MagnificationSpec other) {
+        scale = other.scale;
+        offsetX = other.offsetX;
+        offsetY = other.offsetY;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -89,6 +106,28 @@
     }
 
     @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+
+        final MagnificationSpec s = (MagnificationSpec) other;
+        return scale == s.scale && offsetX == s.offsetX && offsetY == s.offsetY;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = (scale != +0.0f ? Float.floatToIntBits(scale) : 0);
+        result = 31 * result + (offsetX != +0.0f ? Float.floatToIntBits(offsetX) : 0);
+        result = 31 * result + (offsetY != +0.0f ? Float.floatToIntBits(offsetY) : 0);
+        return result;
+    }
+
+    @Override
     public String toString() {
         StringBuilder builder = new StringBuilder();
         builder.append("<scale:");
diff --git a/core/java/android/view/WindowManagerInternal.java b/core/java/android/view/WindowManagerInternal.java
index 3882bca..89b1eb9 100644
--- a/core/java/android/view/WindowManagerInternal.java
+++ b/core/java/android/view/WindowManagerInternal.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import android.annotation.Nullable;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.hardware.display.DisplayManagerInternal;
@@ -55,9 +56,10 @@
          * Called when the bounds of the screen content that is magnified changed.
          * Note that not the entire screen is magnified.
          *
-         * @param bounds The bounds.
+         * @param magnifiedBounds the currently magnified region
+         * @param availableBounds the region available for magnification
          */
-        public void onMagnifedBoundsChanged(Region bounds);
+        public void onMagnifiedBoundsChanged(Region magnifiedBounds, Region availableBounds);
 
         /**
          * Called when an application requests a rectangle on the screen to allow
@@ -142,7 +144,7 @@
      *
      * @param callbacks The callbacks to invoke.
      */
-    public abstract void setMagnificationCallbacks(MagnificationCallbacks callbacks);
+    public abstract void setMagnificationCallbacks(@Nullable MagnificationCallbacks callbacks);
 
     /**
      * Set by the accessibility layer to specify the magnification and panning to
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 50cf302..90fc22b 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -3242,6 +3242,14 @@
              </p>
          -->
         <attr name="canRequestFilterKeyEvents" format="boolean" />
+        <!-- Attribute whether the accessibility service wants to be able to control
+             display magnification.
+             <p>
+             Required to allow setting the {@link android.accessibilityservice
+             #AccessibilityServiceInfo#FLAG_CAN_CONTROL_MAGNIFICATION} flag.
+             </p>
+         -->
+        <attr name="canControlMagnification" format="boolean" />
         <!-- Short description of the accessibility serivce purpose or behavior.-->
         <attr name="description" />
     </declare-styleable>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 54e43c8..b6b2e20 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2682,6 +2682,7 @@
     <public type="attr" name="forceDeviceEncrypted" />
     <public type="attr" name="encryptionAware" />
     <public type="attr" name="preferenceFragmentStyle" />
+    <public type="attr" name="canControlMagnification" />
 
     <public type="style" name="Theme.Material.DayNight" />
     <public type="style" name="Theme.Material.DayNight.DarkActionBar" />
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index faa76f2..00c0fe8 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -613,6 +613,12 @@
     <string name="capability_desc_canRequestFilterKeyEvents">Includes personal data such as credit
         card numbers and passwords.</string>
 
+    <!-- Title for the capability of an accessibility service to control display magnification. -->
+    <string name="capability_title_canControlMagnification">Control display magnification</string>
+    <!-- Description for the capability of an accessibility service to control display magnification. -->
+    <string name="capability_desc_canControlMagnification">Control the display\'s zoom level and
+        positioning.</string>
+
     <!--  Permissions -->
 
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 6820c25..e8f6b46 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -564,6 +564,8 @@
   <java-symbol type="string" name="capability_desc_canRequestFilterKeyEvents" />
   <java-symbol type="string" name="capability_title_canRequestTouchExploration" />
   <java-symbol type="string" name="capability_title_canRetrieveWindowContent" />
+  <java-symbol type="string" name="capability_desc_canControlMagnification" />
+  <java-symbol type="string" name="capability_title_canControlMagnification" />
   <java-symbol type="string" name="cfTemplateForwarded" />
   <java-symbol type="string" name="cfTemplateForwardedTime" />
   <java-symbol type="string" name="cfTemplateNotForwarded" />
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index b52687a..5f6cbf9 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -21,7 +21,6 @@
 import android.util.Pools.SimplePool;
 import android.util.Slog;
 import android.view.Choreographer;
-import android.view.Display;
 import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.InputFilter;
@@ -103,7 +102,7 @@
 
     private TouchExplorer mTouchExplorer;
 
-    private ScreenMagnifier mScreenMagnifier;
+    private MagnificationGestureHandler mMagnificationGestureHandler;
 
     private AutoclickController mAutoclickController;
 
@@ -363,14 +362,13 @@
         }
 
         if ((mEnabledFeatures & FLAG_FEATURE_SCREEN_MAGNIFIER) != 0) {
-            mScreenMagnifier = new ScreenMagnifier(mContext, mUserId,
-                    Display.DEFAULT_DISPLAY, mAms);
-            addFirstEventHandler(mScreenMagnifier);
+            mMagnificationGestureHandler = new MagnificationGestureHandler(mContext, mAms);
+            addFirstEventHandler(mMagnificationGestureHandler);
         }
 
         if ((mEnabledFeatures & FLAG_FEATURE_FILTER_KEY_EVENTS) != 0) {
-           mKeyboardInterceptor = new KeyboardInterceptor(mAms);
-           addFirstEventHandler(mKeyboardInterceptor);
+            mKeyboardInterceptor = new KeyboardInterceptor(mAms);
+            addFirstEventHandler(mKeyboardInterceptor);
         }
     }
 
@@ -398,9 +396,9 @@
             mTouchExplorer.onDestroy();
             mTouchExplorer = null;
         }
-        if (mScreenMagnifier != null) {
-            mScreenMagnifier.onDestroy();
-            mScreenMagnifier = null;
+        if (mMagnificationGestureHandler != null) {
+            mMagnificationGestureHandler.onDestroy();
+            mMagnificationGestureHandler = null;
         }
         if (mKeyboardInterceptor != null) {
             mKeyboardInterceptor.onDestroy();
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 535a8ef..9f1dc0a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -23,6 +23,7 @@
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.annotation.NonNull;
 import android.app.AlertDialog;
 import android.app.PendingIntent;
 import android.app.StatusBarManager;
@@ -90,6 +91,7 @@
 
 import com.android.internal.R;
 import com.android.internal.content.PackageMonitor;
+import com.android.internal.os.SomeArgs;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.server.LocalServices;
 
@@ -181,6 +183,8 @@
 
     private MagnificationController mMagnificationController;
 
+    private boolean mUnregisterMagnificationOnReset;
+
     private InteractionBridge mInteractionBridge;
 
     private AlertDialog mEnableTouchExplorationDialog;
@@ -762,6 +766,28 @@
     }
 
     /**
+     * Called by the MagnificationController when the state of display
+     * magnification changes.
+     *
+     * @param region the new magnified region, may be empty if
+     *               magnification is not enabled (e.g. scale is 1)
+     * @param scale the new scale
+     * @param centerX the new screen-relative center X coordinate
+     * @param centerY the new screen-relative center Y coordinate
+     */
+    void notifyMagnificationChanged(@NonNull Region region,
+            float scale, float centerX, float centerY) {
+        synchronized (mLock) {
+            notifyMagnificationChangedLocked(region, scale, centerX, centerY);
+
+            if (mUnregisterMagnificationOnReset && scale == 1.0f) {
+                mUnregisterMagnificationOnReset = false;
+                mMagnificationController.unregister();
+            }
+        }
+    }
+
+    /**
      * Gets a point within the accessibility focused node where we can send down
      * and up events to perform a click.
      *
@@ -942,6 +968,15 @@
         }
     }
 
+    private void notifyMagnificationChangedLocked(@NonNull Region region,
+            float scale, float centerX, float centerY) {
+        final UserState state = getCurrentUserStateLocked();
+        for (int i = state.mBoundServices.size() - 1; i >= 0; i--) {
+            final Service service = state.mBoundServices.get(i);
+            service.notifyMagnificationChanged(region, scale, centerX, centerY);
+        }
+    }
+
     /**
      * Removes an AccessibilityInteractionConnection.
      *
@@ -1363,6 +1398,11 @@
         }
     }
 
+    /**
+     * Called when any property of the user state has changed.
+     *
+     * @param userState the new user state
+     */
     private void onUserStateChangedLocked(UserState userState) {
         // TODO: Remove this hack
         mInitialized = true;
@@ -1374,6 +1414,7 @@
         updateTouchExplorationLocked(userState);
         updateEnhancedWebAccessibilityLocked(userState);
         updateDisplayColorAdjustmentSettingsLocked(userState);
+        updateMagnificationLocked(userState);
         scheduleUpdateInputFilter(userState);
         scheduleUpdateClientsIfNeededLocked(userState);
     }
@@ -1663,6 +1704,44 @@
         DisplayAdjustmentUtils.applyAdjustments(mContext, userState.mUserId);
     }
 
+    private void updateMagnificationLocked(UserState userState) {
+        final int userId = userState.mUserId;
+        if (userId == mCurrentUserId && mMagnificationController != null) {
+            if (userHasMagnificationServicesLocked(userState)) {
+                mMagnificationController.setUserId(userState.mUserId);
+            } else {
+                // If the user no longer has any magnification-controlling
+                // services and is not using magnification gestures, then
+                // reset the state to normal.
+                if (!userState.mIsDisplayMagnificationEnabled
+                        && mMagnificationController.resetIfNeeded(true)) {
+                    // Animations are still running, so wait until we receive a
+                    // callback verifying that we've reset magnification.
+                    mUnregisterMagnificationOnReset = true;
+                } else {
+                    mUnregisterMagnificationOnReset = false;
+                    mMagnificationController.unregister();
+                    mMagnificationController = null;
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns whether the specified user has any services that are capable of
+     * controlling magnification.
+     */
+    private boolean userHasMagnificationServicesLocked(UserState userState) {
+        final List<Service> services = userState.mBoundServices;
+        for (int i = 0, count = services.size(); i < count; i++) {
+            final Service service = services.get(i);
+            if (mSecurityPolicy.canControlMagnification(service)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private MagnificationSpec getCompatibleMagnificationSpecLocked(int windowId) {
         IBinder windowToken = mGlobalWindowTokens.get(windowId);
         if (windowToken == null) {
@@ -1938,10 +2017,14 @@
     }
 
     MagnificationController getMagnificationController() {
-        if (mMagnificationController == null) {
-            mMagnificationController = new MagnificationController(mContext, this);
+        synchronized (mLock) {
+            if (mMagnificationController == null) {
+                mMagnificationController = new MagnificationController(mContext, this);
+                mMagnificationController.register();
+                mMagnificationController.setUserId(mCurrentUserId);
+            }
+            return mMagnificationController;
         }
-        return mMagnificationController;
     }
 
     /**
@@ -2625,6 +2708,149 @@
         }
 
         @Override
+        public float getMagnificationScale() {
+            synchronized (mLock) {
+                // We treat calls from a profile as if made by its parent as profiles
+                // share the accessibility state of the parent. The call below
+                // performs the current profile parent resolution.
+                final int resolvedUserId = mSecurityPolicy
+                        .resolveCallingUserIdEnforcingPermissionsLocked(
+                                UserHandle.USER_CURRENT);
+                if (resolvedUserId != mCurrentUserId) {
+                    return 1.0f;
+                }
+            }
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                return getMagnificationController().getScale();
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public Region getMagnifiedRegion() {
+            synchronized (mLock) {
+                // We treat calls from a profile as if made by its parent as profiles
+                // share the accessibility state of the parent. The call below
+                // performs the current profile parent resolution.
+                final int resolvedUserId = mSecurityPolicy
+                        .resolveCallingUserIdEnforcingPermissionsLocked(
+                                UserHandle.USER_CURRENT);
+                if (resolvedUserId != mCurrentUserId) {
+                    return Region.obtain();
+                }
+            }
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                final Region region = Region.obtain();
+                getMagnificationController().getMagnifiedRegion(region);
+                return region;
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public float getMagnificationCenterX() {
+            synchronized (mLock) {
+                // We treat calls from a profile as if made by its parent as profiles
+                // share the accessibility state of the parent. The call below
+                // performs the current profile parent resolution.
+                final int resolvedUserId = mSecurityPolicy
+                        .resolveCallingUserIdEnforcingPermissionsLocked(
+                                UserHandle.USER_CURRENT);
+                if (resolvedUserId != mCurrentUserId) {
+                    return 0.0f;
+                }
+            }
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                return getMagnificationController().getCenterX();
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public float getMagnificationCenterY() {
+            synchronized (mLock) {
+                // We treat calls from a profile as if made by its parent as profiles
+                // share the accessibility state of the parent. The call below
+                // performs the current profile parent resolution.
+                final int resolvedUserId = mSecurityPolicy
+                        .resolveCallingUserIdEnforcingPermissionsLocked(
+                                UserHandle.USER_CURRENT);
+                if (resolvedUserId != mCurrentUserId) {
+                    return 0.0f;
+                }
+            }
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                return getMagnificationController().getCenterY();
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public boolean resetMagnification(boolean animate) {
+            synchronized (mLock) {
+                // We treat calls from a profile as if made by its parent as profiles
+                // share the accessibility state of the parent. The call below
+                // performs the current profile parent resolution.
+                final int resolvedUserId = mSecurityPolicy
+                        .resolveCallingUserIdEnforcingPermissionsLocked(
+                                UserHandle.USER_CURRENT);
+                if (resolvedUserId != mCurrentUserId) {
+                    return false;
+                }
+                final boolean permissionGranted = mSecurityPolicy.canControlMagnification(this);
+                if (!permissionGranted) {
+                    return false;
+                }
+            }
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                return getMagnificationController().reset(animate);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public boolean setMagnificationScaleAndCenter(float scale, float centerX, float centerY,
+                boolean animate) {
+            synchronized (mLock) {
+                // We treat calls from a profile as if made by its parent as profiles
+                // share the accessibility state of the parent. The call below
+                // performs the current profile parent resolution.
+                final int resolvedUserId = mSecurityPolicy
+                        .resolveCallingUserIdEnforcingPermissionsLocked(
+                                UserHandle.USER_CURRENT);
+                if (resolvedUserId != mCurrentUserId) {
+                    return false;
+                }
+                final boolean permissionGranted = mSecurityPolicy.canControlMagnification(this);
+                if (!permissionGranted) {
+                    return false;
+                }
+            }
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                return getMagnificationController().setScaleAndCenter(
+                        scale, centerX, centerY, animate);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void setMagnificationCallbackEnabled(boolean enabled) {
+            mInvocationHandler.setMagnificationCallbackEnabled(enabled);
+        }
+
+        @Override
         public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
             mSecurityPolicy.enforceCallingPermission(Manifest.permission.DUMP, FUNCTION_DUMP);
             synchronized (mLock) {
@@ -2819,6 +3045,30 @@
                     InvocationHandler.MSG_CLEAR_ACCESSIBILITY_CACHE);
         }
 
+        public void notifyMagnificationChanged(@NonNull Region region,
+                float scale, float centerX, float centerY) {
+            mInvocationHandler.notifyMagnificationChanged(region, scale, centerX, centerY);
+        }
+
+        /**
+         * Called by the invocation handler to notify the service that the
+         * state of magnification has changed.
+         */
+        private void notifyMagnificationChangedInternal(@NonNull Region region,
+                float scale, float centerX, float centerY) {
+            final IAccessibilityServiceClient listener;
+            synchronized (mLock) {
+                listener = mServiceInterface;
+            }
+            if (listener != null) {
+                try {
+                    listener.onMagnificationChanged(region, scale, centerX, centerY);
+                } catch (RemoteException re) {
+                    Slog.e(LOG_TAG, "Error sending magnification changes to " + mService, re);
+                }
+            }
+        }
+
         private void notifyGestureInternal(int gestureId) {
             final IAccessibilityServiceClient listener;
             synchronized (mLock) {
@@ -2959,6 +3209,10 @@
             public static final int MSG_CLEAR_ACCESSIBILITY_CACHE = 3;
             public static final int MSG_ON_KEY_EVENT_TIMEOUT = 4;
 
+            private static final int MSG_ON_MAGNIFICATION_CHANGED = 5;
+
+            private boolean mIsMagnificationCallbackEnabled = false;
+
             public InvocationHandler(Looper looper) {
                 super(looper, null, true);
             }
@@ -2987,11 +3241,41 @@
                         setOnKeyEventResult(false, eventState.sequence);
                     } break;
 
+                    case MSG_ON_MAGNIFICATION_CHANGED: {
+                        final SomeArgs args = (SomeArgs) message.obj;
+                        final Region region = (Region) args.arg1;
+                        final float scale = (float) args.arg2;
+                        final float centerX = (float) args.arg3;
+                        final float centerY = (float) args.arg4;
+                        notifyMagnificationChangedInternal(region, scale, centerX, centerY);
+                    } break;
+
                     default: {
                         throw new IllegalArgumentException("Unknown message: " + type);
                     }
                 }
             }
+
+            public void notifyMagnificationChanged(@NonNull Region region, float scale,
+                    float centerX, float centerY) {
+                if (!mIsMagnificationCallbackEnabled) {
+                    // Callback is disabled, don't bother packing args.
+                    return;
+                }
+
+                final SomeArgs args = SomeArgs.obtain();
+                args.arg1 = region;
+                args.arg2 = scale;
+                args.arg3 = centerX;
+                args.arg4 = centerY;
+
+                final Message msg = obtainMessage(MSG_ON_MAGNIFICATION_CHANGED, args);
+                msg.sendToTarget();
+            }
+
+            public void setMagnificationCallbackEnabled(boolean enabled) {
+                mIsMagnificationCallbackEnabled = enabled;
+            }
         }
 
         private final class KeyEventDispatcher {
@@ -3660,6 +3944,11 @@
                     & AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT) != 0;
         }
 
+        public boolean canControlMagnification(Service service) {
+            return (service.mAccessibilityServiceInfo.getCapabilities()
+                    & AccessibilityServiceInfo.CAPABILITY_CAN_CONTROL_MAGNIFICATION) != 0;
+        }
+
         private int resolveProfileParentLocked(int userId) {
             if (userId != mCurrentUserId) {
                 final long identity = Binder.clearCallingIdentity();
diff --git a/services/accessibility/java/com/android/server/accessibility/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/MagnificationController.java
index 781d134..a093d92 100644
--- a/services/accessibility/java/com/android/server/accessibility/MagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/MagnificationController.java
@@ -17,20 +17,36 @@
 package com.android.server.accessibility;
 
 import com.android.internal.R;
+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;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.graphics.Rect;
 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;
 import android.view.WindowManagerInternal;
 import android.view.animation.DecelerateInterpolator;
 
+import java.util.Locale;
+
 /**
  * This class is used to control and query the state of display magnification
  * from the accessibility manager and related classes. It is responsible for
@@ -38,37 +54,71 @@
  * communication between the accessibility manager and window manager.
  */
 class MagnificationController {
-    private static final String LOG_TAG = ScreenMagnifier.class.getSimpleName();
+    private static final String LOG_TAG = "MagnificationController";
 
     private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false;
-    private static final boolean DEBUG_MAGNIFICATION_CONTROLLER = false;
 
-    private static final String PROPERTY_NAME_MAGNIFICATION_SPEC = "magnificationSpec";
+    private static final int DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE = 1;
 
-    private final MagnificationSpec mSentMagnificationSpec = MagnificationSpec.obtain();
+    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;
+
+    private final Object mLock = new Object();
+
+    /**
+     * The current magnification spec. If an animation is running, this
+     * reflects the end state.
+     */
     private final MagnificationSpec mCurrentMagnificationSpec = MagnificationSpec.obtain();
 
-    private final Region mMagnifiedBounds = new Region();
+    private final Region mMagnifiedRegion = Region.obtain();
+    private final Region mAvailableRegion = Region.obtain();
+    private final Rect mMagnifiedBounds = new Rect();
+
     private final Rect mTempRect = new Rect();
+    private final Rect mTempRect1 = new Rect();
 
     private final AccessibilityManagerService mAms;
-    private final WindowManagerInternal mWindowManager;
-    private final ValueAnimator mTransformationAnimator;
+    private final ContentResolver mContentResolver;
+
+    private final ScreenStateObserver mScreenStateObserver;
+    private final WindowStateObserver mWindowStateObserver;
+
+    private final SpecAnimationBridge mSpecAnimationBridge;
+
+    private int mUserId;
 
     public MagnificationController(Context context, AccessibilityManagerService ams) {
         mAms = ams;
-        mWindowManager = LocalServices.getService(WindowManagerInternal.class);
+        mContentResolver = context.getContentResolver();
+        mScreenStateObserver = new ScreenStateObserver(context, this);
+        mWindowStateObserver = new WindowStateObserver(context, this);
+        mSpecAnimationBridge = new SpecAnimationBridge(context);
+    }
 
-        final Property<MagnificationController, MagnificationSpec> property =
-                Property.of(MagnificationController.class, MagnificationSpec.class,
-                        PROPERTY_NAME_MAGNIFICATION_SPEC);
-        final MagnificationSpecEvaluator evaluator = new MagnificationSpecEvaluator();
-        final long animationDuration = context.getResources().getInteger(
-                R.integer.config_longAnimTime);
-        mTransformationAnimator = ObjectAnimator.ofObject(this, property, evaluator,
-                mSentMagnificationSpec, mCurrentMagnificationSpec);
-        mTransformationAnimator.setDuration(animationDuration);
-        mTransformationAnimator.setInterpolator(new DecelerateInterpolator(2.5f));
+    /**
+     * Registers magnification-related observers.
+     */
+    public void register() {
+        mScreenStateObserver.register();
+        mWindowStateObserver.register();
+    }
+
+    /**
+     * Unregisters magnification-related observers.
+     */
+    public void unregister() {
+        mScreenStateObserver.unregister();
+        mWindowStateObserver.unregister();
     }
 
     /**
@@ -80,26 +130,33 @@
     }
 
     /**
-     * Sets the magnified region.
+     * Sets the magnified and available regions.
      *
-     * @param region the region to set
-     *  @param updateSpec {@code true} to update the scale and center based on
-     *                    the region bounds, {@code false} to leave them as-is
+     * @param magnified the magnified region
+     * @param available the region available for magnification
+     * @param updateSpec {@code true} to update the scale and center based on
+     *                   the region bounds, {@code false} to leave them as-is
      */
-    public void setMagnifiedRegion(Region region, boolean updateSpec) {
-        mMagnifiedBounds.set(region);
+    public void setMagnifiedRegion(Region magnified, Region available, boolean updateSpec) {
+        synchronized (mLock) {
+            mMagnifiedRegion.set(magnified);
+            mMagnifiedRegion.getBounds(mMagnifiedBounds);
+            mAvailableRegion.set(available);
 
-        if (updateSpec) {
-            final Rect magnifiedFrame = mTempRect;
-            region.getBounds(magnifiedFrame);
-            final float scale = mSentMagnificationSpec.scale;
-            final float offsetX = mSentMagnificationSpec.offsetX;
-            final float offsetY = mSentMagnificationSpec.offsetY;
-            final float centerX = (-offsetX + magnifiedFrame.width() / 2) / scale;
-            final float centerY = (-offsetY + magnifiedFrame.height() / 2) / scale;
-            setScaleAndMagnifiedRegionCenter(scale, centerX, centerY, false);
-        } else {
-            mAms.onMagnificationStateChanged();
+            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 = (mMagnifiedBounds.width() / 2.0f - offsetX) / scale;
+            final float centerY = (mMagnifiedBounds.height() / 2.0f - offsetY) / scale;
+            if (updateSpec) {
+                setScaleAndCenter(scale, centerX, centerY, false);
+            } else {
+                mAms.onMagnificationStateChanged();
+                mAms.notifyMagnificationChanged(mMagnifiedRegion, scale, centerX, centerY);
+            }
         }
     }
 
@@ -113,18 +170,51 @@
      *         magnified region, or {@code false} otherwise
      */
     public boolean magnifiedRegionContains(float x, float y) {
-        return mMagnifiedBounds.contains((int) x, (int) y);
+        synchronized (mLock) {
+            return mMagnifiedRegion.contains((int) x, (int) y);
+        }
     }
 
     /**
-     * Populates the specified rect with the bounds of the magnified
-     * region.
+     * Returns whether the region available for magnification contains the
+     * specified screen-relative coordinates.
+     *
+     * @param x the screen-relative X coordinate to check
+     * @param y the screen-relative Y coordinate to check
+     * @return {@code true} if the coordinate is contained within the
+     *         region available for magnification, or {@code false} otherwise
+     */
+    private boolean availableRegionContains(float x, float y) {
+        synchronized (mLock) {
+            return mAvailableRegion.contains((int) x, (int) y);
+        }
+    }
+
+    /**
+     * Populates the specified rect with the screen-relative bounds of the
+     * magnified region. If magnification is not enabled, the returned
+     * bounds will be empty.
      *
      * @param outBounds rect to populate with the bounds of the magnified
      *                  region
      */
-    public void getMagnifiedBounds(Rect outBounds) {
-        mMagnifiedBounds.getBounds(outBounds);
+    public void getMagnifiedBounds(@NonNull Rect outBounds) {
+        synchronized (mLock) {
+            outBounds.set(mMagnifiedBounds);
+        }
+    }
+
+    /**
+     * Populates the specified region with the screen-relative magnified
+     * region. If magnification is not enabled, then the returned region
+     * will be empty.
+     *
+     * @param outRegion the region to populate
+     */
+    public void getMagnifiedRegion(@NonNull Region outRegion) {
+        synchronized (mLock) {
+            outRegion.set(mMagnifiedRegion);
+        }
     }
 
     /**
@@ -147,6 +237,19 @@
         return mCurrentMagnificationSpec.offsetX;
     }
 
+
+    /**
+     * Returns the screen-relative X coordinate of the center of the
+     * magnification viewport.
+     *
+     * @return the X coordinate
+     */
+    public float getCenterX() {
+        synchronized (mLock) {
+            return  (mMagnifiedBounds.width() / 2.0f - getOffsetX()) / getScale();
+        }
+    }
+
     /**
      * Returns the Y offset of the magnification viewport. If an animation
      * is in progress, this reflects the end state of the animation.
@@ -158,6 +261,18 @@
     }
 
     /**
+     * Returns the screen-relative Y coordinate of the center of the
+     * magnification viewport.
+     *
+     * @return the Y coordinate
+     */
+    public float getCenterY() {
+        synchronized (mLock) {
+            return (mMagnifiedBounds.height() / 2.0f - getOffsetY()) / getScale();
+        }
+    }
+
+    /**
      * Returns the scale currently used by the window manager. If an
      * animation is in progress, this reflects the current state of the
      * animation.
@@ -165,7 +280,7 @@
      * @return the scale currently used by the window manager
      */
     public float getSentScale() {
-        return mSentMagnificationSpec.scale;
+        return mSpecAnimationBridge.mSentMagnificationSpec.scale;
     }
 
     /**
@@ -176,7 +291,7 @@
      * @return the X offset currently used by the window manager
      */
     public float getSentOffsetX() {
-        return mSentMagnificationSpec.offsetX;
+        return mSpecAnimationBridge.mSentMagnificationSpec.offsetX;
     }
 
     /**
@@ -187,7 +302,7 @@
      * @return the Y offset currently used by the window manager
      */
     public float getSentOffsetY() {
-        return mSentMagnificationSpec.offsetY;
+        return mSpecAnimationBridge.mSentMagnificationSpec.offsetY;
     }
 
     /**
@@ -196,21 +311,24 @@
      *
      * @param animate {@code true} to animate the transition, {@code false}
      *                to transition immediately
+     * @return {@code true} if the magnification spec changed, {@code false} if
+     *         the spec did not change
      */
-    public void reset(boolean animate) {
-        if (mTransformationAnimator.isRunning()) {
-            mTransformationAnimator.cancel();
+    public boolean reset(boolean animate) {
+        synchronized (mLock) {
+            return resetLocked(animate);
         }
-        mCurrentMagnificationSpec.clear();
-        if (animate) {
-            animateMagnificationSpec(mSentMagnificationSpec,
-                    mCurrentMagnificationSpec);
-        } else {
-            setMagnificationSpec(mCurrentMagnificationSpec);
+    }
+
+    private boolean resetLocked(boolean animate) {
+        final MagnificationSpec spec = mCurrentMagnificationSpec;
+        final boolean changed = !spec.isNop();
+        if (changed) {
+            spec.clear();
         }
-        final Rect bounds = mTempRect;
-        bounds.setEmpty();
-        mAms.onMagnificationStateChanged();
+
+        mSpecAnimationBridge.updateSentSpec(spec, animate);
+        return changed;
     }
 
     /**
@@ -219,23 +337,32 @@
      * transition is immediate.
      *
      * @param scale the target scale, must be >= 1
+     * @param pivotX the screen-relative X coordinate around which to scale
+     * @param pivotY the screen-relative Y coordinate around which to scale
      * @param animate {@code true} to animate the transition, {@code false}
      *                to transition immediately
+     * @return {@code true} if the magnification spec changed, {@code false} if
+     *         the spec did not change
      */
-    public void setScale(float scale, float pivotX, float pivotY, boolean animate) {
-        final Rect magnifiedFrame = mTempRect;
-        mMagnifiedBounds.getBounds(magnifiedFrame);
-        final MagnificationSpec spec = mCurrentMagnificationSpec;
-        final float oldScale = spec.scale;
-        final float oldCenterX = (-spec.offsetX + magnifiedFrame.width() / 2) / oldScale;
-        final float oldCenterY = (-spec.offsetY + magnifiedFrame.height() / 2) / oldScale;
-        final float normPivotX = (-spec.offsetX + pivotX) / oldScale;
-        final float normPivotY = (-spec.offsetY + pivotY) / oldScale;
-        final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale);
-        final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale);
-        final float centerX = normPivotX + offsetX;
-        final float centerY = normPivotY + offsetY;
-        setScaleAndMagnifiedRegionCenter(scale, centerX, centerY, animate);
+    public boolean setScale(float scale, float pivotX, float pivotY, boolean animate) {
+        synchronized (mLock) {
+            // Constrain scale immediately for use in the pivot calculations.
+            scale = MathUtils.constrain(scale, MIN_SCALE, MAX_SCALE);
+
+            final Rect viewport = mTempRect;
+            mMagnifiedRegion.getBounds(viewport);
+            final MagnificationSpec spec = mCurrentMagnificationSpec;
+            final float oldScale = spec.scale;
+            final float oldCenterX = (viewport.width() / 2.0f - spec.offsetX) / oldScale;
+            final float oldCenterY = (viewport.height() / 2.0f - spec.offsetY) / oldScale;
+            final float normPivotX = (pivotX - spec.offsetX) / oldScale;
+            final float normPivotY = (pivotY - spec.offsetY) / oldScale;
+            final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale);
+            final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale);
+            final float centerX = normPivotX + offsetX;
+            final float centerY = normPivotY + offsetY;
+            return setScaleAndCenterLocked(scale, centerX, centerY, animate);
+        }
     }
 
     /**
@@ -248,10 +375,13 @@
      *                center
      * @param animate {@code true} to animate the transition, {@code false}
      *                to transition immediately
+     * @return {@code true} if the magnification spec changed, {@code false} if
+     *         the spec did not change
      */
-    public void setMagnifiedRegionCenter(float centerX, float centerY, boolean animate) {
-        setScaleAndMagnifiedRegionCenter(mCurrentMagnificationSpec.scale, centerX, centerY,
-                animate);
+    public boolean setCenter(float centerX, float centerY, boolean animate) {
+        synchronized (mLock) {
+            return setScaleAndCenterLocked(Float.NaN, centerX, centerY, animate);
+        }
     }
 
     /**
@@ -259,35 +389,27 @@
      * animating the transition. If animation is disabled, the transition
      * is immediate.
      *
-     * @param scale the target scale, must be >= 1
+     * @param scale the target scale, or {@link Float#NaN} to leave unchanged
      * @param centerX the screen-relative X coordinate around which to
-     *                center and scale
+     *                center and scale, or {@link Float#NaN} to leave unchanged
      * @param centerY the screen-relative Y coordinate around which to
-     *                center and scale
+     *                center and scale, or {@link Float#NaN} to leave unchanged
      * @param animate {@code true} to animate the transition, {@code false}
      *                to transition immediately
+     * @return {@code true} if the magnification spec changed, {@code false} if
+     *         the spec did not change
      */
-    public void setScaleAndMagnifiedRegionCenter(float scale, float centerX, float centerY,
-            boolean animate) {
-        if (Float.compare(mCurrentMagnificationSpec.scale, scale) == 0
-                && Float.compare(mCurrentMagnificationSpec.offsetX, centerX) == 0
-                && Float.compare(mCurrentMagnificationSpec.offsetY, centerY) == 0) {
-            return;
+    public boolean setScaleAndCenter(float scale, float centerX, float centerY, boolean animate) {
+        synchronized (mLock) {
+            return setScaleAndCenterLocked(scale, centerX, centerY, animate);
         }
-        if (mTransformationAnimator.isRunning()) {
-            mTransformationAnimator.cancel();
-        }
-        if (DEBUG_MAGNIFICATION_CONTROLLER) {
-            Slog.i(LOG_TAG, "scale: " + scale + " offsetX: " + centerX + " offsetY: " + centerY);
-        }
-        updateMagnificationSpec(scale, centerX, centerY);
-        if (animate) {
-            animateMagnificationSpec(mSentMagnificationSpec,
-                    mCurrentMagnificationSpec);
-        } else {
-            setMagnificationSpec(mCurrentMagnificationSpec);
-        }
-        mAms.onMagnificationStateChanged();
+    }
+
+    private boolean setScaleAndCenterLocked(
+            float scale, float centerX, float centerY, boolean animate) {
+        final boolean changed = updateMagnificationSpecLocked(scale, centerX, centerY);
+        mSpecAnimationBridge.updateSentSpec(mCurrentMagnificationSpec, animate);
+        return changed;
     }
 
     /**
@@ -297,75 +419,504 @@
      * @param offsetY the amount in pixels to offset the Y center
      */
     public void offsetMagnifiedRegionCenter(float offsetX, float offsetY) {
-        final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX;
-        mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX,
-                getMinOffsetX()), 0);
-        final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY;
-        mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY,
-                getMinOffsetY()), 0);
-        setMagnificationSpec(mCurrentMagnificationSpec);
+        synchronized (mLock) {
+            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);
+            mSpecAnimationBridge.updateSentSpec(currSpec, false);
+        }
     }
 
-    private void updateMagnificationSpec(float scale, float magnifiedCenterX,
-            float magnifiedCenterY) {
-        final Rect magnifiedFrame = mTempRect;
-        mMagnifiedBounds.getBounds(magnifiedFrame);
-        mCurrentMagnificationSpec.scale = scale;
-        final int viewportWidth = magnifiedFrame.width();
-        final float nonNormOffsetX = viewportWidth / 2 - magnifiedCenterX * scale;
-        mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX,
-                getMinOffsetX()), 0);
-        final int viewportHeight = magnifiedFrame.height();
-        final float nonNormOffsetY = viewportHeight / 2 - magnifiedCenterY * scale;
-        mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY,
-                getMinOffsetY()), 0);
+    /**
+     * Persists the current magnification scale to the current user's settings.
+     */
+    public void persistScale() {
+        final float scale = mCurrentMagnificationSpec.scale;
+        final int userId = mUserId;
+
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                Settings.Secure.putFloatForUser(mContentResolver,
+                        Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale, userId);
+                return null;
+            }
+        }.execute();
     }
 
-    private float getMinOffsetX() {
-        final Rect magnifiedFrame = mTempRect;
-        mMagnifiedBounds.getBounds(magnifiedFrame);
-        final float viewportWidth = magnifiedFrame.width();
+    /**
+     * Retrieves a previously persisted magnification scale from the current
+     * user's settings.
+     *
+     * @return the previously persisted magnification scale, or the default
+     *         scale if none is available
+     */
+    public float getPersistedScale() {
+        return Settings.Secure.getFloatForUser(mContentResolver,
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
+                DEFAULT_MAGNIFICATION_SCALE, mUserId);
+    }
+
+    /**
+     * Updates the current magnification spec.
+     *
+     * @param scale the magnification scale
+     * @param centerX the unscaled, screen-relative X coordinate of the center
+     *                of the viewport, or {@link Float#NaN} to leave unchanged
+     * @param centerY the unscaled, screen-relative Y coordinate of the center
+     *                of the viewport, or {@link Float#NaN} to leave unchanged
+     * @return {@code true} if the magnification spec changed or {@code false}
+     *         otherwise
+     */
+    private boolean updateMagnificationSpecLocked(float scale, float centerX, float centerY) {
+        if (!availableRegionContains(centerX, centerY)) {
+            return false;
+        }
+
+        boolean changed = false;
+
+        final MagnificationSpec currSpec = mCurrentMagnificationSpec;
+
+        // Handle scale.
+        if (Float.isNaN(scale)) {
+            scale = getScale();
+        }
+
+        final float normScale = MathUtils.constrain(scale, MIN_SCALE, MAX_SCALE);
+        if (Float.compare(currSpec.scale, normScale) != 0) {
+            currSpec.scale = normScale;
+            changed = true;
+        }
+
+        // Handle X offset.
+        if (Float.isNaN(centerX)) {
+            centerX = getCenterX();
+        }
+
+        final float nonNormOffsetX = mMagnifiedBounds.width() / 2.0f - centerX * scale;
+        final float offsetX = MathUtils.constrain(nonNormOffsetX, getMinOffsetXLocked(), 0);
+        if (Float.compare(currSpec.offsetX, offsetX) != 0) {
+            currSpec.offsetX = offsetX;
+            changed = true;
+        }
+
+        // Handle Y offset.
+        if (Float.isNaN(centerY)) {
+            centerY = getCenterY();
+        }
+
+        final float nonNormOffsetY = mMagnifiedBounds.height() / 2.0f - centerY * scale;
+        final float offsetY = MathUtils.constrain(nonNormOffsetY, getMinOffsetYLocked(), 0);
+        if (Float.compare(currSpec.offsetY, offsetY) != 0) {
+            currSpec.offsetY = offsetY;
+            changed = true;
+        }
+
+        return changed;
+    }
+
+    private float getMinOffsetXLocked() {
+        final float viewportWidth = mMagnifiedBounds.width();
         return viewportWidth - viewportWidth * mCurrentMagnificationSpec.scale;
     }
 
-    private float getMinOffsetY() {
-        final Rect magnifiedFrame = mTempRect;
-        mMagnifiedBounds.getBounds(magnifiedFrame);
-        final float viewportHeight = magnifiedFrame.height();
+    private float getMinOffsetYLocked() {
+        final float viewportHeight = mMagnifiedBounds.height();
         return viewportHeight - viewportHeight * mCurrentMagnificationSpec.scale;
     }
 
-    private void animateMagnificationSpec(MagnificationSpec fromSpec,
-            MagnificationSpec toSpec) {
-        mTransformationAnimator.setObjectValues(fromSpec, toSpec);
-        mTransformationAnimator.start();
-    }
+    /**
+     * Sets the currently active user ID.
+     *
+     * @param userId the currently active user ID
+     */
+    public void setUserId(int userId) {
+        if (mUserId != userId) {
+            mUserId = userId;
 
-    public void setMagnificationSpec(MagnificationSpec spec) {
-        if (DEBUG_SET_MAGNIFICATION_SPEC) {
-            Slog.i(LOG_TAG, "Sending: " + spec);
+            synchronized (mLock) {
+                if (isMagnifying()) {
+                    reset(false);
+                }
+            }
         }
-        mSentMagnificationSpec.scale = spec.scale;
-        mSentMagnificationSpec.offsetX = spec.offsetX;
-        mSentMagnificationSpec.offsetY = spec.offsetY;
-        mWindowManager.setMagnificationSpec(MagnificationSpec.obtain(spec));
     }
 
-    public MagnificationSpec getMagnificationSpec() {
-        return mSentMagnificationSpec;
+    private boolean isScreenMagnificationAutoUpdateEnabled() {
+        return (Settings.Secure.getInt(mContentResolver,
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
+                DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1);
     }
 
-    private static class MagnificationSpecEvaluator implements TypeEvaluator<MagnificationSpec> {
-        private final MagnificationSpec mTempTransformationSpec = MagnificationSpec.obtain();
+    /**
+     * Resets magnification if magnification and auto-update are both enabled.
+     *
+     * @param animate whether the animate the transition
+     * @return {@code true} if magnification was reset to the disabled state,
+     *         {@code false} if magnification is still active
+     */
+    boolean resetIfNeeded(boolean animate) {
+        synchronized (mLock) {
+            if (isMagnifying() && isScreenMagnificationAutoUpdateEnabled()) {
+                reset(animate);
+                return true;
+            }
+            return false;
+        }
+    }
+
+    private void getMagnifiedFrameInContentCoordsLocked(Rect outFrame) {
+        final float scale = getSentScale();
+        final float offsetX = getSentOffsetX();
+        final float offsetY = getSentOffsetY();
+        getMagnifiedBounds(outFrame);
+        outFrame.offset((int) -offsetX, (int) -offsetY);
+        outFrame.scale(1.0f / scale);
+    }
+
+    private void requestRectangleOnScreen(int left, int top, int right, int bottom) {
+        synchronized (mLock) {
+            final Rect magnifiedFrame = mTempRect;
+            getMagnifiedBounds(magnifiedFrame);
+            if (!magnifiedFrame.intersects(left, top, right, bottom)) {
+                return;
+            }
+
+            final Rect magnifFrameInScreenCoords = mTempRect1;
+            getMagnifiedFrameInContentCoordsLocked(magnifFrameInScreenCoords);
+
+            final float scrollX;
+            final float scrollY;
+            if (right - left > magnifFrameInScreenCoords.width()) {
+                final int direction = TextUtils
+                        .getLayoutDirectionFromLocale(Locale.getDefault());
+                if (direction == View.LAYOUT_DIRECTION_LTR) {
+                    scrollX = left - magnifFrameInScreenCoords.left;
+                } else {
+                    scrollX = right - magnifFrameInScreenCoords.right;
+                }
+            } else if (left < magnifFrameInScreenCoords.left) {
+                scrollX = left - magnifFrameInScreenCoords.left;
+            } else if (right > magnifFrameInScreenCoords.right) {
+                scrollX = right - magnifFrameInScreenCoords.right;
+            } else {
+                scrollX = 0;
+            }
+
+            if (bottom - top > magnifFrameInScreenCoords.height()) {
+                scrollY = top - magnifFrameInScreenCoords.top;
+            } else if (top < magnifFrameInScreenCoords.top) {
+                scrollY = top - magnifFrameInScreenCoords.top;
+            } else if (bottom > magnifFrameInScreenCoords.bottom) {
+                scrollY = bottom - magnifFrameInScreenCoords.bottom;
+            } else {
+                scrollY = 0;
+            }
+
+            final float scale = getScale();
+            offsetMagnifiedRegionCenter(scrollX * scale, scrollY * scale);
+        }
+    }
+
+    /**
+     * 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 final WindowManagerInternal mWindowManager;
+
+        /**
+         * The magnification spec that was sent to the window manager. This should
+         * only be accessed and modified on the main (e.g. animation) thread.
+         */
+        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 long mMainThreadId;
+
+        private SpecAnimationBridge(Context context) {
+            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();
+            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));
+        }
+
+        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();
+            }
+
+            // If the current and sent specs don't match, update the sent spec.
+            final boolean changed = !mSentMagnificationSpec.equals(spec);
+            if (changed) {
+                if (animate) {
+                    animateMagnificationSpec(spec);
+                } else {
+                    setMagnificationSpec(spec);
+                }
+            }
+        }
+
+        private void animateMagnificationSpec(MagnificationSpec toSpec) {
+            mTransformationAnimator.setObjectValues(mSentMagnificationSpec, toSpec);
+            mTransformationAnimator.start();
+        }
+
+        private void setMagnificationSpec(MagnificationSpec spec) {
+            if (DEBUG_SET_MAGNIFICATION_SPEC) {
+                Slog.i(LOG_TAG, "Sending: " + spec);
+            }
+
+            mSentMagnificationSpec.setTo(spec);
+            mWindowManager.setMagnificationSpec(spec);
+        }
+
+        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 static class MagnificationSpecProperty
+                extends Property<SpecAnimationBridge, MagnificationSpec> {
+            public MagnificationSpecProperty() {
+                super(MagnificationSpec.class, "spec");
+            }
+
+            @Override
+            public MagnificationSpec get(SpecAnimationBridge object) {
+                return object.mSentMagnificationSpec;
+            }
+
+            @Override
+            public void set(SpecAnimationBridge object, MagnificationSpec value) {
+                object.setMagnificationSpec(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() {
+            mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF));
+        }
+
+        public void unregister() {
+            mContext.unregisterReceiver(this);
+        }
 
         @Override
-        public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec,
-                MagnificationSpec toSpec) {
-            final MagnificationSpec result = mTempTransformationSpec;
-            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;
+        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;
+                }
+            }
+        }
+    }
+
+    /**
+     * 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;
+
+        private final Rect mTempRect = new Rect();
+        private final Rect mTempRect1 = new Rect();
+
+        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 void register() {
+            mWindowManager.setMagnificationCallbacks(this);
+        }
+
+        public void unregister() {
+            mWindowManager.setMagnificationCallbacks(null);
+        }
+
+        @Override
+        public void onMagnifiedBoundsChanged(Region magnified, Region available) {
+            final SomeArgs args = SomeArgs.obtain();
+            args.arg1 = Region.obtain(magnified);
+            args.arg2 = Region.obtain(available);
+            mHandler.obtainMessage(MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED, args).sendToTarget();
+        }
+
+        private void handleOnMagnifiedBoundsChanged(Region magnified, Region available) {
+            mController.setMagnifiedRegion(magnified, available, 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);
+        }
+
+        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;
+                        final Region availableBounds = (Region) args.arg2;
+                        handleOnMagnifiedBoundsChanged(magnifiedBounds, availableBounds);
+                        magnifiedBounds.recycle();
+                        availableBounds.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;
+                }
+            }
         }
     }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/ScreenMagnifier.java b/services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java
similarity index 65%
rename from services/accessibility/java/com/android/server/accessibility/ScreenMagnifier.java
rename to services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java
index 8feb167..51c8ab5 100644
--- a/services/accessibility/java/com/android/server/accessibility/ScreenMagnifier.java
+++ b/services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012 The Android Open Source Project
+ * Copyright (C) 2015 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.
@@ -16,18 +16,10 @@
 
 package com.android.server.accessibility;
 
-import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.os.AsyncTask;
-import android.os.Binder;
 import android.os.Handler;
 import android.os.Message;
-import android.provider.Settings;
-import android.text.TextUtils;
+import android.util.MathUtils;
 import android.util.Slog;
 import android.util.TypedValue;
 import android.view.GestureDetector;
@@ -39,18 +31,12 @@
 import android.view.MotionEvent.PointerProperties;
 import android.view.ScaleGestureDetector;
 import android.view.ScaleGestureDetector.OnScaleGestureListener;
-import android.view.View;
 import android.view.ViewConfiguration;
-import android.view.WindowManagerInternal;
 import android.view.accessibility.AccessibilityEvent;
 
-import com.android.internal.os.SomeArgs;
-import com.android.server.LocalServices;
-
-import java.util.Locale;
-
 /**
- * This class handles the screen magnification when accessibility is enabled.
+ * This class handles magnification in response to touch events.
+ *
  * The behavior is as follows:
  *
  * 1. Triple tap toggles permanent screen magnification which is magnifying
@@ -88,10 +74,8 @@
  *
  * 6. The magnification scale will be persisted in settings and in the cloud.
  */
-public final class ScreenMagnifier implements WindowManagerInternal.MagnificationCallbacks,
-        EventStreamTransformation {
-
-    private static final String LOG_TAG = ScreenMagnifier.class.getSimpleName();
+class MagnificationGestureHandler implements EventStreamTransformation {
+    private static final String LOG_TAG = "MagnificationEventHandler";
 
     private static final boolean DEBUG_STATE_TRANSITIONS = false;
     private static final boolean DEBUG_DETECTING = false;
@@ -103,40 +87,19 @@
     private static final int STATE_VIEWPORT_DRAGGING = 3;
     private static final int STATE_MAGNIFIED_INTERACTION = 4;
 
-    private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f;
+    private static final float MIN_SCALE = 2.0f;
+    private static final float MAX_SCALE = 5.0f;
 
-    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;
-
-    private static final int DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE = 1;
-
-    private static final int MY_PID = android.os.Process.myPid();
-
-    private final Rect mTempRect = new Rect();
-    private final Rect mTempRect1 = new Rect();
-
-    private final Context mContext;
-    private final WindowManagerInternal mWindowManager;
     private final MagnificationController mMagnificationController;
-    private final ScreenStateObserver mScreenStateObserver;
-
     private final DetectingStateHandler mDetectingStateHandler;
-    private final MagnifiedContentInteractonStateHandler mMagnifiedContentInteractonStateHandler;
+    private final MagnifiedContentInteractionStateHandler mMagnifiedContentInteractionStateHandler;
     private final StateViewportDraggingHandler mStateViewportDraggingHandler;
 
-    private final int mUserId;
-
-    private final int mTapTimeSlop = ViewConfiguration.getJumpTapTimeout();
-    private final int mMultiTapTimeSlop;
-    private final int mTapDistanceSlop;
-    private final int mMultiTapDistanceSlop;
-
     private EventStreamTransformation mNext;
 
     private int mCurrentState;
     private int mPreviousState;
+
     private boolean mTranslationEnabledBeforePan;
 
     private PointerCoords[] mTempPointerCoords;
@@ -144,189 +107,44 @@
 
     private long mDelegatingStateDownTime;
 
-    private boolean mUpdateMagnificationSpecOnNextBoundsChange;
-
-    private final Handler mHandler = new Handler() {
-        @Override
-        public void handleMessage(Message message) {
-            switch (message.what) {
-                case MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED: {
-                    Region bounds = (Region) message.obj;
-                    handleOnMagnifiedBoundsChanged(bounds);
-                    bounds.recycle();
-                } break;
-                case MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED: {
-                    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: {
-                    final int rotation = message.arg1;
-                    handleOnRotationChanged(rotation);
-                } break;
-            }
-        }
-    };
-
-    public ScreenMagnifier(Context context, int userId, int displayId,
-            AccessibilityManagerService service) {
-        mContext = context;
-        mUserId = userId;
-        mWindowManager = LocalServices.getService(WindowManagerInternal.class);
-
-        mMultiTapTimeSlop = ViewConfiguration.getDoubleTapTimeout()
-                + mContext.getResources().getInteger(
-                com.android.internal.R.integer.config_screen_magnification_multi_tap_adjustment);
-        mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop();
-        mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
-
-        mDetectingStateHandler = new DetectingStateHandler();
+    public MagnificationGestureHandler(Context context, AccessibilityManagerService ams) {
+        mMagnificationController = ams.getMagnificationController();
+        mDetectingStateHandler = new DetectingStateHandler(context);
         mStateViewportDraggingHandler = new StateViewportDraggingHandler();
-        mMagnifiedContentInteractonStateHandler = new MagnifiedContentInteractonStateHandler(
-                context);
-
-        mMagnificationController = service.getMagnificationController();
-        mScreenStateObserver = new ScreenStateObserver(context, mMagnificationController);
-
-        mWindowManager.setMagnificationCallbacks(this);
+        mMagnifiedContentInteractionStateHandler =
+                new MagnifiedContentInteractionStateHandler(context);
 
         transitionToState(STATE_DETECTING);
     }
 
     @Override
-    public void onMagnifedBoundsChanged(Region bounds) {
-        Region newBounds = Region.obtain(bounds);
-        mHandler.obtainMessage(MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED, newBounds).sendToTarget();
-        if (MY_PID != Binder.getCallingPid()) {
-            bounds.recycle();
-        }
-    }
-
-    private void handleOnMagnifiedBoundsChanged(Region bounds) {
-        // If there was a rotation we have to update the center of the magnified
-        // region since the old offset X/Y may be out of its acceptable range for
-        // the new display width and height.
-        mMagnificationController.setMagnifiedRegion(
-                bounds, mUpdateMagnificationSpecOnNextBoundsChange);
-        mUpdateMagnificationSpecOnNextBoundsChange = false;
-    }
-
-    @Override
-    public void onRectangleOnScreenRequested(int left, int top, int right, int bottom) {
-        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) {
-        Rect magnifiedFrame = mTempRect;
-        mMagnificationController.getMagnifiedBounds(magnifiedFrame);
-        if (!magnifiedFrame.intersects(left, top, right, bottom)) {
-            return;
-        }
-        Rect magnifFrameInScreenCoords = mTempRect1;
-        getMagnifiedFrameInContentCoords(magnifFrameInScreenCoords);
-        final float scrollX;
-        final float scrollY;
-        if (right - left > magnifFrameInScreenCoords.width()) {
-            final int direction = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault());
-            if (direction == View.LAYOUT_DIRECTION_LTR) {
-                scrollX = left - magnifFrameInScreenCoords.left;
-            } else {
-                scrollX = right - magnifFrameInScreenCoords.right;
-            }
-        } else if (left < magnifFrameInScreenCoords.left) {
-            scrollX = left - magnifFrameInScreenCoords.left;
-        } else if (right > magnifFrameInScreenCoords.right) {
-            scrollX = right - magnifFrameInScreenCoords.right;
-        } else {
-            scrollX = 0;
-        }
-        if (bottom - top > magnifFrameInScreenCoords.height()) {
-            scrollY = top - magnifFrameInScreenCoords.top;
-        } else if (top < magnifFrameInScreenCoords.top) {
-            scrollY = top - magnifFrameInScreenCoords.top;
-        } else if (bottom > magnifFrameInScreenCoords.bottom) {
-            scrollY = bottom - magnifFrameInScreenCoords.bottom;
-        } else {
-            scrollY = 0;
-        }
-        final float scale = mMagnificationController.getScale();
-        mMagnificationController.offsetMagnifiedRegionCenter(scrollX * scale, scrollY * scale);
-    }
-
-    @Override
-    public void onRotationChanged(int rotation) {
-        mHandler.obtainMessage(MESSAGE_ON_ROTATION_CHANGED, rotation, 0).sendToTarget();
-    }
-
-    private void handleOnRotationChanged(int rotation) {
-        resetMagnificationIfNeeded();
-        if (mMagnificationController.isMagnifying()) {
-            mUpdateMagnificationSpecOnNextBoundsChange = true;
-        }
-    }
-
-    @Override
-    public void onUserContextChanged() {
-        mHandler.sendEmptyMessage(MESSAGE_ON_USER_CONTEXT_CHANGED);
-    }
-
-    private void handleOnUserContextChanged() {
-        resetMagnificationIfNeeded();
-    }
-
-    private void getMagnifiedFrameInContentCoords(Rect rect) {
-        final float scale = mMagnificationController.getSentScale();
-        final float offsetX = mMagnificationController.getSentOffsetX();
-        final float offsetY = mMagnificationController.getSentOffsetY();
-        mMagnificationController.getMagnifiedBounds(rect);
-        rect.offset((int) -offsetX, (int) -offsetY);
-        rect.scale(1.0f / scale);
-    }
-
-    private void resetMagnificationIfNeeded() {
-        if (mMagnificationController.isMagnifying()
-                && isScreenMagnificationAutoUpdateEnabled(mContext)) {
-            mMagnificationController.reset(true);
-        }
-    }
-
-    @Override
-    public void onMotionEvent(MotionEvent event, MotionEvent rawEvent,
-            int policyFlags) {
+    public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
         if (!event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {
             if (mNext != null) {
                 mNext.onMotionEvent(event, rawEvent, policyFlags);
             }
             return;
         }
-        mMagnifiedContentInteractonStateHandler.onMotionEvent(event);
+        mMagnifiedContentInteractionStateHandler.onMotionEvent(event, rawEvent, policyFlags);
         switch (mCurrentState) {
             case STATE_DELEGATING: {
                 handleMotionEventStateDelegating(event, rawEvent, policyFlags);
-            } break;
+            }
+            break;
             case STATE_DETECTING: {
                 mDetectingStateHandler.onMotionEvent(event, rawEvent, policyFlags);
-            } break;
+            }
+            break;
             case STATE_VIEWPORT_DRAGGING: {
-                mStateViewportDraggingHandler.onMotionEvent(event, policyFlags);
-            } break;
+                mStateViewportDraggingHandler.onMotionEvent(event, rawEvent, policyFlags);
+            }
+            break;
             case STATE_MAGNIFIED_INTERACTION: {
                 // mMagnifiedContentInteractonStateHandler handles events only
                 // if this is the current state since it uses ScaleGestureDetecotr
                 // and a GestureDetector which need well formed event stream.
-            } break;
+            }
+            break;
             default: {
                 throw new IllegalStateException("Unknown state: " + mCurrentState);
             }
@@ -336,7 +154,7 @@
     @Override
     public void onKeyEvent(KeyEvent event, int policyFlags) {
         if (mNext != null) {
-          mNext.onKeyEvent(event, policyFlags);
+            mNext.onKeyEvent(event, policyFlags);
         }
     }
 
@@ -366,15 +184,13 @@
     @Override
     public void onDestroy() {
         clear();
-        mScreenStateObserver.destroy();
-        mWindowManager.setMagnificationCallbacks(null);
     }
 
     private void clear() {
         mCurrentState = STATE_DETECTING;
         mDetectingStateHandler.clear();
         mStateViewportDraggingHandler.clear();
-        mMagnifiedContentInteractonStateHandler.clear();
+        mMagnifiedContentInteractionStateHandler.clear();
     }
 
     private void handleMotionEventStateDelegating(MotionEvent event,
@@ -382,12 +198,14 @@
         switch (event.getActionMasked()) {
             case MotionEvent.ACTION_DOWN: {
                 mDelegatingStateDownTime = event.getDownTime();
-            } break;
+            }
+            break;
             case MotionEvent.ACTION_UP: {
                 if (mDetectingStateHandler.mDelayedEventQueue == null) {
                     transitionToState(STATE_DETECTING);
                 }
-            } break;
+            }
+            break;
         }
         if (mNext != null) {
             // If the event is within the magnified portion of the screen we have
@@ -402,7 +220,8 @@
                 final float scaledOffsetY = mMagnificationController.getOffsetY();
                 final int pointerCount = event.getPointerCount();
                 PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount);
-                PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount);
+                PointerProperties[] properties = getTempPointerPropertiesWithMinSize(
+                        pointerCount);
                 for (int i = 0; i < pointerCount; i++) {
                     event.getPointerCoords(i, coords[i]);
                     coords[i].x = (coords[i].x - scaledOffsetX) / scale;
@@ -441,12 +260,14 @@
     }
 
     private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) {
-        final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length : 0;
+        final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length
+                : 0;
         if (oldSize < size) {
             PointerProperties[] oldTempPointerProperties = mTempPointerProperties;
             mTempPointerProperties = new PointerProperties[size];
             if (oldTempPointerProperties != null) {
-                System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, oldSize);
+                System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0,
+                        oldSize);
             }
         }
         for (int i = oldSize; i < size; i++) {
@@ -460,16 +281,20 @@
             switch (state) {
                 case STATE_DELEGATING: {
                     Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING");
-                } break;
+                }
+                break;
                 case STATE_DETECTING: {
                     Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING");
-                } break;
+                }
+                break;
                 case STATE_VIEWPORT_DRAGGING: {
                     Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING");
-                } break;
+                }
+                break;
                 case STATE_MAGNIFIED_INTERACTION: {
                     Slog.i(LOG_TAG, "mCurrentState: STATE_MAGNIFIED_INTERACTION");
-                } break;
+                }
+                break;
                 default: {
                     throw new IllegalArgumentException("Unknown state: " + state);
                 }
@@ -479,20 +304,30 @@
         mCurrentState = state;
     }
 
-    private final class MagnifiedContentInteractonStateHandler
-            extends SimpleOnGestureListener implements OnScaleGestureListener {
-        private static final float MIN_SCALE = 1.3f;
-        private static final float MAX_SCALE = 5.0f;
+    private interface MotionEventHandler {
+
+        void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags);
+
+        void clear();
+    }
+
+    /**
+     * This class determines if the user is performing a scale or pan gesture.
+     */
+    private final class MagnifiedContentInteractionStateHandler extends SimpleOnGestureListener
+            implements OnScaleGestureListener, MotionEventHandler {
 
         private final ScaleGestureDetector mScaleGestureDetector;
+
         private final GestureDetector mGestureDetector;
 
         private final float mScalingThreshold;
 
         private float mInitialScaleFactor = -1;
+
         private boolean mScaling;
 
-        public MagnifiedContentInteractonStateHandler(Context context) {
+        public MagnifiedContentInteractionStateHandler(Context context) {
             final TypedValue scaleValue = new TypedValue();
             context.getResources().getValue(
                     com.android.internal.R.dimen.config_screen_magnification_scaling_threshold,
@@ -503,7 +338,8 @@
             mGestureDetector = new GestureDetector(context, this);
         }
 
-        public void onMotionEvent(MotionEvent event) {
+        @Override
+        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
             mScaleGestureDetector.onTouchEvent(event);
             mGestureDetector.onTouchEvent(event);
             if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {
@@ -511,11 +347,7 @@
             }
             if (event.getActionMasked() == MotionEvent.ACTION_UP) {
                 clear();
-                final float scale = Math.min(Math.max(mMagnificationController.getScale(),
-                        MIN_SCALE), MAX_SCALE);
-                if (scale != getPersistedScale()) {
-                    persistScale(scale);
-                }
+                mMagnificationController.persistScale();
                 if (mPreviousState == STATE_VIEWPORT_DRAGGING) {
                     transitionToState(STATE_VIEWPORT_DRAGGING);
                 } else {
@@ -552,14 +384,29 @@
                 }
                 return false;
             }
-            final float newScale = mMagnificationController.getScale()
-                    * detector.getScaleFactor();
-            final float normalizedNewScale = Math.min(Math.max(newScale, MIN_SCALE), MAX_SCALE);
-            if (DEBUG_SCALING) {
-                Slog.i(LOG_TAG, "normalizedNewScale: " + normalizedNewScale);
+
+            final float initialScale = mMagnificationController.getScale();
+            final float targetScale = initialScale * detector.getScaleFactor();
+
+            // Don't allow a gesture to move the user further outside the
+            // desired bounds for gesture-controlled scaling.
+            final float scale;
+            if (targetScale > MAX_SCALE && targetScale > initialScale) {
+                // The target scale is too big and getting bigger.
+                scale = MAX_SCALE;
+            } else if (targetScale < MIN_SCALE && targetScale < initialScale) {
+                // The target scale is too small and getting smaller.
+                scale = MIN_SCALE;
+            } else {
+                // The target scale may be outside our bounds, but at least
+                // it's moving in the right direction. This avoids a "jump" if
+                // we're at odds with some other service's desired bounds.
+                scale = targetScale;
             }
-            mMagnificationController.setScale(normalizedNewScale, detector.getFocusX(),
-                    detector.getFocusY(), false);
+
+            final float pivotX = detector.getFocusX();
+            final float pivotY = detector.getFocusY();
+            mMagnificationController.setScale(scale, pivotX, pivotY, false);
             return true;
         }
 
@@ -573,16 +420,24 @@
             clear();
         }
 
-        private void clear() {
+        @Override
+        public void clear() {
             mInitialScaleFactor = -1;
             mScaling = false;
         }
     }
 
-    private final class StateViewportDraggingHandler {
+    /**
+     * This class handles motion events when the event dispatcher has
+     * determined that the user is performing a single-finger drag of the
+     * magnification viewport.
+     */
+    private final class StateViewportDraggingHandler implements MotionEventHandler {
+
         private boolean mLastMoveOutsideMagnifiedRegion;
 
-        private void onMotionEvent(MotionEvent event, int policyFlags) {
+        @Override
+        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
             final int action = event.getActionMasked();
             switch (action) {
                 case MotionEvent.ACTION_DOWN: {
@@ -591,7 +446,8 @@
                 case MotionEvent.ACTION_POINTER_DOWN: {
                     clear();
                     transitionToState(STATE_MAGNIFIED_INTERACTION);
-                } break;
+                }
+                break;
                 case MotionEvent.ACTION_MOVE: {
                     if (event.getPointerCount() != 1) {
                         throw new IllegalStateException("Should have one pointer down.");
@@ -601,35 +457,43 @@
                     if (mMagnificationController.magnifiedRegionContains(eventX, eventY)) {
                         if (mLastMoveOutsideMagnifiedRegion) {
                             mLastMoveOutsideMagnifiedRegion = false;
-                            mMagnificationController.setMagnifiedRegionCenter(eventX,
+                            mMagnificationController.setCenter(eventX,
                                     eventY, true);
                         } else {
-                            mMagnificationController.setMagnifiedRegionCenter(eventX,
+                            mMagnificationController.setCenter(eventX,
                                     eventY, false);
                         }
                     } else {
                         mLastMoveOutsideMagnifiedRegion = true;
                     }
-                } break;
+                }
+                break;
                 case MotionEvent.ACTION_UP: {
                     if (!mTranslationEnabledBeforePan) {
                         mMagnificationController.reset(true);
                     }
                     clear();
                     transitionToState(STATE_DETECTING);
-                } break;
+                }
+                break;
                 case MotionEvent.ACTION_POINTER_UP: {
-                    throw new IllegalArgumentException("Unexpected event type: ACTION_POINTER_UP");
+                    throw new IllegalArgumentException(
+                            "Unexpected event type: ACTION_POINTER_UP");
                 }
             }
         }
 
+        @Override
         public void clear() {
             mLastMoveOutsideMagnifiedRegion = false;
         }
     }
 
-    private final class DetectingStateHandler {
+    /**
+     * This class handles motion events when the event dispatch has not yet
+     * determined what the user is doing. It watches for various tap events.
+     */
+    private final class DetectingStateHandler implements MotionEventHandler {
 
         private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1;
 
@@ -637,12 +501,30 @@
 
         private static final int ACTION_TAP_COUNT = 3;
 
+        private final int mTapTimeSlop = ViewConfiguration.getJumpTapTimeout();
+
+        private final int mMultiTapTimeSlop;
+
+        private final int mTapDistanceSlop;
+
+        private final int mMultiTapDistanceSlop;
+
         private MotionEventInfo mDelayedEventQueue;
 
         private MotionEvent mLastDownEvent;
+
         private MotionEvent mLastTapUpEvent;
+
         private int mTapCount;
 
+        public DetectingStateHandler(Context context) {
+            mMultiTapTimeSlop = ViewConfiguration.getDoubleTapTimeout()
+                    + context.getResources().getInteger(
+                    com.android.internal.R.integer.config_screen_magnification_multi_tap_adjustment);
+            mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+            mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
+        }
+
         private final Handler mHandler = new Handler() {
             @Override
             public void handleMessage(Message message) {
@@ -652,12 +534,14 @@
                         MotionEvent event = (MotionEvent) message.obj;
                         final int policyFlags = message.arg1;
                         onActionTapAndHold(event, policyFlags);
-                    } break;
+                    }
+                    break;
                     case MESSAGE_TRANSITION_TO_DELEGATING_STATE: {
                         transitionToState(STATE_DELEGATING);
                         sendDelayedMotionEvents();
                         clear();
-                    } break;
+                    }
+                    break;
                     default: {
                         throw new IllegalArgumentException("Unknown message type: " + type);
                     }
@@ -665,6 +549,7 @@
             }
         };
 
+        @Override
         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
             cacheDelayedMotionEvent(event, rawEvent, policyFlags);
             final int action = event.getActionMasked();
@@ -678,7 +563,7 @@
                     }
                     if (mTapCount == ACTION_TAP_COUNT - 1 && mLastDownEvent != null
                             && GestureUtils.isMultiTap(mLastDownEvent, event,
-                                    mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
+                            mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
                         Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD,
                                 policyFlags, 0, event);
                         mHandler.sendMessageDelayed(message,
@@ -690,7 +575,8 @@
                     }
                     clearLastDownEvent();
                     mLastDownEvent = MotionEvent.obtain(event);
-                } break;
+                }
+                break;
                 case MotionEvent.ACTION_POINTER_DOWN: {
                     if (mMagnificationController.isMagnifying()) {
                         transitionToState(STATE_MAGNIFIED_INTERACTION);
@@ -698,7 +584,8 @@
                     } else {
                         transitionToDelegatingStateAndClear();
                     }
-                } break;
+                }
+                break;
                 case MotionEvent.ACTION_MOVE: {
                     if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) {
                         final double distance = GestureUtils.computeDistance(mLastDownEvent,
@@ -707,7 +594,8 @@
                             transitionToDelegatingStateAndClear();
                         }
                     }
-                } break;
+                }
+                break;
                 case MotionEvent.ACTION_UP: {
                     if (mLastDownEvent == null) {
                         return;
@@ -715,8 +603,8 @@
                     mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
                     if (!mMagnificationController.magnifiedRegionContains(
                             event.getX(), event.getY())) {
-                         transitionToDelegatingStateAndClear();
-                         return;
+                        transitionToDelegatingStateAndClear();
+                        return;
                     }
                     if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop,
                             mTapDistanceSlop, 0)) {
@@ -739,13 +627,16 @@
                     }
                     clearLastTapUpEvent();
                     mLastTapUpEvent = MotionEvent.obtain(event);
-                } break;
+                }
+                break;
                 case MotionEvent.ACTION_POINTER_UP: {
                     /* do nothing */
-                } break;
+                }
+                break;
             }
         }
 
+        @Override
         public void clear() {
             mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
             mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
@@ -792,7 +683,7 @@
             while (mDelayedEventQueue != null) {
                 MotionEventInfo info = mDelayedEventQueue;
                 mDelayedEventQueue = info.mNext;
-                ScreenMagnifier.this.onMotionEvent(info.mEvent, info.mRawEvent,
+                MagnificationGestureHandler.this.onMotionEvent(info.mEvent, info.mRawEvent,
                         info.mPolicyFlags);
                 info.recycle();
             }
@@ -816,9 +707,11 @@
             if (DEBUG_DETECTING) {
                 Slog.i(LOG_TAG, "onActionTap()");
             }
+
             if (!mMagnificationController.isMagnifying()) {
-                mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
-                        up.getX(), up.getY(), true);
+                final float targetScale = mMagnificationController.getPersistedScale();
+                final float scale = MathUtils.constrain(targetScale, MIN_SCALE, MAX_SCALE);
+                mMagnificationController.setScaleAndCenter(scale, up.getX(), up.getY(), true);
             } else {
                 mMagnificationController.reset(true);
             }
@@ -828,50 +721,36 @@
             if (DEBUG_DETECTING) {
                 Slog.i(LOG_TAG, "onActionTapAndHold()");
             }
+
             clear();
             mTranslationEnabledBeforePan = mMagnificationController.isMagnifying();
-            mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
-                    down.getX(), down.getY(), true);
+
+            final float targetScale = mMagnificationController.getPersistedScale();
+            final float scale = MathUtils.constrain(targetScale, MIN_SCALE, MAX_SCALE);
+            mMagnificationController.setScaleAndCenter(scale, down.getX(), down.getY(), true);
+
             transitionToState(STATE_VIEWPORT_DRAGGING);
         }
     }
 
-    private void persistScale(final float scale) {
-        new AsyncTask<Void, Void, Void>() {
-            @Override
-            protected Void doInBackground(Void... params) {
-                Settings.Secure.putFloatForUser(mContext.getContentResolver(),
-                        Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale, mUserId);
-                return null;
-            }
-        }.execute();
-    }
-
-    private float getPersistedScale() {
-        return Settings.Secure.getFloatForUser(mContext.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
-                DEFAULT_MAGNIFICATION_SCALE, mUserId);
-    }
-
-    private static boolean isScreenMagnificationAutoUpdateEnabled(Context context) {
-        return (Settings.Secure.getInt(context.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
-                DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1);
-    }
-
     private static final class MotionEventInfo {
 
         private static final int MAX_POOL_SIZE = 10;
 
         private static final Object sLock = new Object();
+
         private static MotionEventInfo sPool;
+
         private static int sPoolSize;
 
         private MotionEventInfo mNext;
+
         private boolean mInPool;
 
         public MotionEvent mEvent;
+
         public MotionEvent mRawEvent;
+
         public int mPolicyFlags;
 
         public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent,
@@ -922,47 +801,4 @@
             mPolicyFlags = 0;
         }
     }
-
-    private final class ScreenStateObserver extends BroadcastReceiver {
-        private static final int MESSAGE_ON_SCREEN_STATE_CHANGE = 1;
-
-        private final Context mContext;
-        private final MagnificationController mMagnificationController;
-
-        private final Handler mHandler = new Handler() {
-            @Override
-            public void handleMessage(Message message) {
-                 switch (message.what) {
-                    case MESSAGE_ON_SCREEN_STATE_CHANGE: {
-                        String action = (String) message.obj;
-                        handleOnScreenStateChange(action);
-                    } break;
-                }
-            }
-        };
-
-        public ScreenStateObserver(Context context,
-                MagnificationController magnificationController) {
-            mContext = context;
-            mMagnificationController = magnificationController;
-            mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF));
-        }
-
-        public void destroy() {
-            mContext.unregisterReceiver(this);
-        }
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            mHandler.obtainMessage(MESSAGE_ON_SCREEN_STATE_CHANGE,
-                    intent.getAction()).sendToTarget();
-        }
-
-        private void handleOnScreenStateChange(String action) {
-            if (mMagnificationController.isMagnifying()
-                    && isScreenMagnificationAutoUpdateEnabled(mContext)) {
-                mMagnificationController.reset(false);
-            }
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index d713751..c246609 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -409,6 +409,7 @@
 
             private final Region mMagnifiedBounds = new Region();
             private final Region mOldMagnifiedBounds = new Region();
+            private final Region mOldAvailableBounds = new Region();
 
             private final Path mCircularPath;
 
@@ -537,29 +538,39 @@
                         screenWidth - mDrawBorderInset, screenHeight - mDrawBorderInset,
                         Region.Op.INTERSECT);
 
-                if (!mOldMagnifiedBounds.equals(magnifiedBounds)) {
-                    Region bounds = Region.obtain();
-                    bounds.set(magnifiedBounds);
-                    mHandler.obtainMessage(MyHandler.MESSAGE_NOTIFY_MAGNIFIED_BOUNDS_CHANGED,
-                            bounds).sendToTarget();
+                final boolean magnifiedChanged = !mOldMagnifiedBounds.equals(magnifiedBounds);
+                final boolean availableChanged = !mOldAvailableBounds.equals(availableBounds);
+                if (magnifiedChanged || availableChanged) {
+                    if (magnifiedChanged) {
+                        mWindow.setBounds(magnifiedBounds);
+                        Rect dirtyRect = mTempRect1;
+                        if (mFullRedrawNeeded) {
+                            mFullRedrawNeeded = false;
+                            dirtyRect.set(mDrawBorderInset, mDrawBorderInset,
+                                    screenWidth - mDrawBorderInset,
+                                    screenHeight - mDrawBorderInset);
+                            mWindow.invalidate(dirtyRect);
+                        } else {
+                            Region dirtyRegion = mTempRegion3;
+                            dirtyRegion.set(magnifiedBounds);
+                            dirtyRegion.op(mOldMagnifiedBounds, Region.Op.UNION);
+                            dirtyRegion.op(nonMagnifiedBounds, Region.Op.INTERSECT);
+                            dirtyRegion.getBounds(dirtyRect);
+                            mWindow.invalidate(dirtyRect);
+                        }
 
-                    mWindow.setBounds(magnifiedBounds);
-                    Rect dirtyRect = mTempRect1;
-                    if (mFullRedrawNeeded) {
-                        mFullRedrawNeeded = false;
-                        dirtyRect.set(mDrawBorderInset, mDrawBorderInset,
-                                screenWidth - mDrawBorderInset, screenHeight - mDrawBorderInset);
-                        mWindow.invalidate(dirtyRect);
-                    } else {
-                        Region dirtyRegion = mTempRegion3;
-                        dirtyRegion.set(magnifiedBounds);
-                        dirtyRegion.op(mOldMagnifiedBounds, Region.Op.UNION);
-                        dirtyRegion.op(nonMagnifiedBounds, Region.Op.INTERSECT);
-                        dirtyRegion.getBounds(dirtyRect);
-                        mWindow.invalidate(dirtyRect);
+                        mOldMagnifiedBounds.set(magnifiedBounds);
                     }
 
-                    mOldMagnifiedBounds.set(magnifiedBounds);
+                    if (availableChanged) {
+                        mOldAvailableBounds.set(availableBounds);
+                    }
+
+                    final SomeArgs args = SomeArgs.obtain();
+                    args.arg1 = Region.obtain(magnifiedBounds);
+                    args.arg2 = Region.obtain(availableBounds);
+                    mHandler.obtainMessage(
+                            MyHandler.MESSAGE_NOTIFY_MAGNIFIED_BOUNDS_CHANGED, args).sendToTarget();
                 }
             }
 
@@ -867,9 +878,12 @@
             public void handleMessage(Message message) {
                 switch (message.what) {
                     case MESSAGE_NOTIFY_MAGNIFIED_BOUNDS_CHANGED: {
-                        Region bounds = (Region) message.obj;
-                        mCallbacks.onMagnifedBoundsChanged(bounds);
-                        bounds.recycle();
+                        final SomeArgs args = (SomeArgs) message.obj;
+                        final Region magnifiedBounds = (Region) args.arg1;
+                        final Region availableBounds = (Region) args.arg2;
+                        mCallbacks.onMagnifiedBoundsChanged(magnifiedBounds, availableBounds);
+                        magnifiedBounds.recycle();
+                        availableBounds.recycle();
                     } break;
 
                     case MESSAGE_NOTIFY_RECTANGLE_ON_SCREEN_REQUESTED: {
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index c6b6c77..c359c53 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -52,6 +52,7 @@
 
 import android.Manifest;
 import android.animation.ValueAnimator;
+import android.annotation.Nullable;
 import android.app.ActivityManagerNative;
 import android.app.AppOpsManager;
 import android.app.IActivityManager;
@@ -10178,7 +10179,7 @@
         }
 
         @Override
-        public void setMagnificationCallbacks(MagnificationCallbacks callbacks) {
+        public void setMagnificationCallbacks(@Nullable MagnificationCallbacks callbacks) {
             synchronized (mWindowMap) {
                 if (mAccessibilityController == null) {
                     mAccessibilityController = new AccessibilityController(
diff --git a/wifi/java/android/net/wifi/WifiEnterpriseConfig.java b/wifi/java/android/net/wifi/WifiEnterpriseConfig.java
index e611ea4..6e42391 100644
--- a/wifi/java/android/net/wifi/WifiEnterpriseConfig.java
+++ b/wifi/java/android/net/wifi/WifiEnterpriseConfig.java
@@ -737,7 +737,9 @@
     public String toString() {
         StringBuffer sb = new StringBuffer();
         for (String key : mFields.keySet()) {
-            sb.append(key).append(" ").append(mFields.get(key)).append("\n");
+            // Don't display password in toString().
+            String value = (key == PASSWORD_KEY) ? "<removed>" : mFields.get(key);
+            sb.append(key).append(" ").append(value).append("\n");
         }
         return sb.toString();
     }