/*
 * 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.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.accessibility;

import android.animation.ValueAnimator;
import android.annotation.NonNull;
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.Handler;
import android.os.Message;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.MathUtils;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.MagnificationSpec;
import android.view.View;
import android.view.animation.DecelerateInterpolator;

import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.server.LocalServices;
import com.android.server.wm.WindowManagerInternal;

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
 * holding the current state of magnification and animation, and it handles
 * communication between the accessibility manager and window manager.
 *
 * Magnification is limited to the range [MIN_SCALE, MAX_SCALE], and can only occur inside the
 * magnification region. If a value is out of bounds, it will be adjusted to guarantee these
 * constraints.
 */
public class MagnificationController {
    private static final boolean DEBUG = false;
    private static final String LOG_TAG = "MagnificationController";

    public static final float MIN_SCALE = 1.0f;
    public static final float MAX_SCALE = 8.0f;

    private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false;

    private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f;

    private final Object mLock;

    private final ControllerContext mControllerCtx;

    private final ScreenStateObserver mScreenStateObserver;

    private int mUserId;

    private final long mMainThreadId;

    /** List of display Magnification, mapping from displayId -> DisplayMagnification. */
    @GuardedBy("mLock")
    private final SparseArray<DisplayMagnification> mDisplays = new SparseArray<>(0);

    /**
     * This class implements {@link WindowManagerInternal.MagnificationCallbacks} and holds
     * magnification information per display.
     */
    private final class DisplayMagnification implements
            WindowManagerInternal.MagnificationCallbacks {
        /**
         * The current magnification spec. If an animation is running, this
         * reflects the end state.
         */
        private final MagnificationSpec mCurrentMagnificationSpec = MagnificationSpec.obtain();

        private final Region mMagnificationRegion = Region.obtain();
        private final Rect mMagnificationBounds = new Rect();

        private final Rect mTempRect = new Rect();
        private final Rect mTempRect1 = new Rect();

        private final SpecAnimationBridge mSpecAnimationBridge;

        // Flag indicating that we are registered with window manager.
        private boolean mRegistered;
        private boolean mUnregisterPending;
        private boolean mDeleteAfterUnregister;

        private final int mDisplayId;

        private static final int INVALID_ID = -1;
        private int mIdOfLastServiceToMagnify = INVALID_ID;

        DisplayMagnification(int displayId) {
            mDisplayId = displayId;
            mSpecAnimationBridge = new SpecAnimationBridge(mControllerCtx, mLock, mDisplayId);
        }

        /**
         * Registers magnification callback and get current magnification region from
         * window manager.
         *
         * @return true if callback registers successful.
         */
        @GuardedBy("mLock")
        boolean register() {
            mRegistered = mControllerCtx.getWindowManager().setMagnificationCallbacks(
                    mDisplayId, this);
            if (!mRegistered) {
                Slog.w(LOG_TAG, "set magnification callbacks fail, displayId:" + mDisplayId);
                return false;
            }
            mSpecAnimationBridge.setEnabled(true);
            // Obtain initial state.
            mControllerCtx.getWindowManager().getMagnificationRegion(
                    mDisplayId, mMagnificationRegion);
            mMagnificationRegion.getBounds(mMagnificationBounds);
            return true;
        }

        /**
         * Unregisters magnification callback from window manager. Callbacks to
         * {@link MagnificationController#unregisterCallbackLocked(int, boolean)} after
         * unregistered.
         *
         * @param delete true if this instance should be removed from the SparseArray in
         *               MagnificationController after unregistered, for example, display removed.
         */
        @GuardedBy("mLock")
        void unregister(boolean delete) {
            if (mRegistered) {
                mSpecAnimationBridge.setEnabled(false);
                mControllerCtx.getWindowManager().setMagnificationCallbacks(
                        mDisplayId, null);
                mMagnificationRegion.setEmpty();
                mRegistered = false;
                unregisterCallbackLocked(mDisplayId, delete);
            }
            mUnregisterPending = false;
        }

        /**
         * Reset magnification status with animation enabled. {@link #unregister(boolean)} will be
         * called after animation finished.
         *
         * @param delete true if this instance should be removed from the SparseArray in
         *               MagnificationController after unregistered, for example, display removed.
         */
        @GuardedBy("mLock")
        void unregisterPending(boolean delete) {
            mDeleteAfterUnregister = delete;
            mUnregisterPending = true;
            reset(true);
        }

        boolean isRegistered() {
            return mRegistered;
        }

        boolean isMagnifying() {
            return mCurrentMagnificationSpec.scale > 1.0f;
        }

        float getScale() {
            return mCurrentMagnificationSpec.scale;
        }

        float getOffsetX() {
            return mCurrentMagnificationSpec.offsetX;
        }

        float getOffsetY() {
            return mCurrentMagnificationSpec.offsetY;
        }

        @GuardedBy("mLock")
        float getCenterX() {
            return (mMagnificationBounds.width() / 2.0f
                    + mMagnificationBounds.left - getOffsetX()) / getScale();
        }

        @GuardedBy("mLock")
        float getCenterY() {
            return (mMagnificationBounds.height() / 2.0f
                    + mMagnificationBounds.top - 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.
         *
         * @return the scale currently used by the window manager
         */
        float getSentScale() {
            return mSpecAnimationBridge.mSentMagnificationSpec.scale;
        }

        /**
         * Returns the X offset currently used by the window manager. If an
         * animation is in progress, this reflects the current state of the
         * animation.
         *
         * @return the X offset currently used by the window manager
         */
        float getSentOffsetX() {
            return mSpecAnimationBridge.mSentMagnificationSpec.offsetX;
        }

        /**
         * Returns the Y offset currently used by the window manager. If an
         * animation is in progress, this reflects the current state of the
         * animation.
         *
         * @return the Y offset currently used by the window manager
         */
        float getSentOffsetY() {
            return mSpecAnimationBridge.mSentMagnificationSpec.offsetY;
        }

        @Override
        public void onMagnificationRegionChanged(Region magnificationRegion) {
            final Message m = PooledLambda.obtainMessage(
                    DisplayMagnification::updateMagnificationRegion, this,
                    Region.obtain(magnificationRegion));
            mControllerCtx.getHandler().sendMessage(m);
        }

        @Override
        public void onRectangleOnScreenRequested(int left, int top, int right, int bottom) {
            final Message m = PooledLambda.obtainMessage(
                    DisplayMagnification::requestRectangleOnScreen, this,
                    left, top, right, bottom);
            mControllerCtx.getHandler().sendMessage(m);
        }

        @Override
        public void onRotationChanged(int rotation) {
            // Treat as context change and reset
            final Message m = PooledLambda.obtainMessage(MagnificationController::resetIfNeeded,
                    MagnificationController.this, mDisplayId, true);
            mControllerCtx.getHandler().sendMessage(m);
        }

        @Override
        public void onUserContextChanged() {
            final Message m = PooledLambda.obtainMessage(MagnificationController::resetIfNeeded,
                    MagnificationController.this, mDisplayId, true);
            mControllerCtx.getHandler().sendMessage(m);
        }

        /**
         * Update our copy of the current magnification region
         *
         * @param magnified the magnified region
         */
        void updateMagnificationRegion(Region magnified) {
            synchronized (mLock) {
                if (!mRegistered) {
                    // Don't update if we've unregistered
                    return;
                }
                if (!mMagnificationRegion.equals(magnified)) {
                    mMagnificationRegion.set(magnified);
                    mMagnificationRegion.getBounds(mMagnificationBounds);
                    // It's possible that our magnification spec is invalid with the new bounds.
                    // Adjust the current spec's offsets if necessary.
                    if (updateCurrentSpecWithOffsetsLocked(
                            mCurrentMagnificationSpec.offsetX, mCurrentMagnificationSpec.offsetY)) {
                        sendSpecToAnimation(mCurrentMagnificationSpec, false);
                    }
                    onMagnificationChangedLocked();
                }
                magnified.recycle();
            }
        }

        void sendSpecToAnimation(MagnificationSpec spec, boolean animate) {
            if (DEBUG) {
                Slog.i(LOG_TAG,
                        "sendSpecToAnimation(spec = " + spec + ", animate = " + animate + ")");
            }
            if (Thread.currentThread().getId() == mMainThreadId) {
                mSpecAnimationBridge.updateSentSpecMainThread(spec, animate);
            } else {
                final Message m = PooledLambda.obtainMessage(
                        SpecAnimationBridge::updateSentSpecMainThread,
                        mSpecAnimationBridge, spec, animate);
                mControllerCtx.getHandler().sendMessage(m);
            }
        }

        /**
         * Get the ID of the last service that changed the magnification spec.
         *
         * @return The id
         */
        int getIdOfLastServiceToMagnify() {
            return mIdOfLastServiceToMagnify;
        }

        void onMagnificationChangedLocked() {
            mControllerCtx.getAms().notifyMagnificationChanged(mDisplayId, mMagnificationRegion,
                    getScale(), getCenterX(), getCenterY());
            if (mUnregisterPending && !isMagnifying()) {
                unregister(mDeleteAfterUnregister);
            }
        }

        @GuardedBy("mLock")
        boolean magnificationRegionContains(float x, float y) {
            return mMagnificationRegion.contains((int) x, (int) y);
        }

        @GuardedBy("mLock")
        void getMagnificationBounds(@NonNull Rect outBounds) {
            outBounds.set(mMagnificationBounds);
        }

        @GuardedBy("mLock")
        void getMagnificationRegion(@NonNull Region outRegion) {
            outRegion.set(mMagnificationRegion);
        }

        void requestRectangleOnScreen(int left, int top, int right, int bottom) {
            synchronized (mLock) {
                final Rect magnifiedFrame = mTempRect;
                getMagnificationBounds(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();
                offsetMagnifiedRegion(scrollX * scale, scrollY * scale, INVALID_ID);
            }
        }

        void getMagnifiedFrameInContentCoordsLocked(Rect outFrame) {
            final float scale = getSentScale();
            final float offsetX = getSentOffsetX();
            final float offsetY = getSentOffsetY();
            getMagnificationBounds(outFrame);
            outFrame.offset((int) -offsetX, (int) -offsetY);
            outFrame.scale(1.0f / scale);
        }

        @GuardedBy("mLock")
        void setForceShowMagnifiableBounds(boolean show) {
            if (mRegistered) {
                mControllerCtx.getWindowManager().setForceShowMagnifiableBounds(
                        mDisplayId, show);
            }
        }

        @GuardedBy("mLock")
        boolean reset(boolean animate) {
            if (!mRegistered) {
                return false;
            }
            final MagnificationSpec spec = mCurrentMagnificationSpec;
            final boolean changed = !spec.isNop();
            if (changed) {
                spec.clear();
                onMagnificationChangedLocked();
            }
            mIdOfLastServiceToMagnify = INVALID_ID;
            sendSpecToAnimation(spec, animate);
            return changed;
        }

        @GuardedBy("mLock")
        boolean setScale(float scale, float pivotX, float pivotY,
                boolean animate, int id) {
            if (!mRegistered) {
                return false;
            }
            // Constrain scale immediately for use in the pivot calculations.
            scale = MathUtils.constrain(scale, MIN_SCALE, MAX_SCALE);

            final Rect viewport = mTempRect;
            mMagnificationRegion.getBounds(viewport);
            final MagnificationSpec spec = mCurrentMagnificationSpec;
            final float oldScale = spec.scale;
            final float oldCenterX =
                    (viewport.width() / 2.0f - spec.offsetX + viewport.left) / oldScale;
            final float oldCenterY =
                    (viewport.height() / 2.0f - spec.offsetY + viewport.top) / 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;
            mIdOfLastServiceToMagnify = id;
            return setScaleAndCenter(scale, centerX, centerY, animate, id);
        }

        @GuardedBy("mLock")
        boolean setScaleAndCenter(float scale, float centerX, float centerY,
                boolean animate, int id) {
            if (!mRegistered) {
                return false;
            }
            if (DEBUG) {
                Slog.i(LOG_TAG,
                        "setScaleAndCenterLocked(scale = " + scale + ", centerX = " + centerX
                                + ", centerY = " + centerY + ", animate = " + animate
                                + ", id = " + id
                                + ")");
            }
            final boolean changed = updateMagnificationSpecLocked(scale, centerX, centerY);
            sendSpecToAnimation(mCurrentMagnificationSpec, animate);
            if (isMagnifying() && (id != INVALID_ID)) {
                mIdOfLastServiceToMagnify = id;
            }
            return changed;
        }

        /**
         * 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
         */
        boolean updateMagnificationSpecLocked(float scale, float centerX, float centerY) {
            // Handle defaults.
            if (Float.isNaN(centerX)) {
                centerX = getCenterX();
            }
            if (Float.isNaN(centerY)) {
                centerY = getCenterY();
            }
            if (Float.isNaN(scale)) {
                scale = getScale();
            }

            // Compute changes.
            boolean changed = false;

            final float normScale = MathUtils.constrain(scale, MIN_SCALE, MAX_SCALE);
            if (Float.compare(mCurrentMagnificationSpec.scale, normScale) != 0) {
                mCurrentMagnificationSpec.scale = normScale;
                changed = true;
            }

            final float nonNormOffsetX = mMagnificationBounds.width() / 2.0f
                    + mMagnificationBounds.left - centerX * normScale;
            final float nonNormOffsetY = mMagnificationBounds.height() / 2.0f
                    + mMagnificationBounds.top - centerY * normScale;
            changed |= updateCurrentSpecWithOffsetsLocked(nonNormOffsetX, nonNormOffsetY);

            if (changed) {
                onMagnificationChangedLocked();
            }

            return changed;
        }

        @GuardedBy("mLock")
        void offsetMagnifiedRegion(float offsetX, float offsetY, int id) {
            if (!mRegistered) {
                return;
            }

            final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX;
            final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY;
            if (updateCurrentSpecWithOffsetsLocked(nonNormOffsetX, nonNormOffsetY)) {
                onMagnificationChangedLocked();
            }
            if (id != INVALID_ID) {
                mIdOfLastServiceToMagnify = id;
            }
            sendSpecToAnimation(mCurrentMagnificationSpec, false);
        }

        boolean updateCurrentSpecWithOffsetsLocked(float nonNormOffsetX, float nonNormOffsetY) {
            if (DEBUG) {
                Slog.i(LOG_TAG,
                        "updateCurrentSpecWithOffsetsLocked(nonNormOffsetX = " + nonNormOffsetX
                                + ", nonNormOffsetY = " + nonNormOffsetY + ")");
            }
            boolean changed = false;
            final float offsetX = MathUtils.constrain(
                    nonNormOffsetX, getMinOffsetXLocked(), getMaxOffsetXLocked());
            if (Float.compare(mCurrentMagnificationSpec.offsetX, offsetX) != 0) {
                mCurrentMagnificationSpec.offsetX = offsetX;
                changed = true;
            }
            final float offsetY = MathUtils.constrain(
                    nonNormOffsetY, getMinOffsetYLocked(), getMaxOffsetYLocked());
            if (Float.compare(mCurrentMagnificationSpec.offsetY, offsetY) != 0) {
                mCurrentMagnificationSpec.offsetY = offsetY;
                changed = true;
            }
            return changed;
        }

        float getMinOffsetXLocked() {
            final float viewportWidth = mMagnificationBounds.width();
            final float viewportLeft = mMagnificationBounds.left;
            return (viewportLeft + viewportWidth) -
                    (viewportLeft + viewportWidth) * mCurrentMagnificationSpec.scale;
        }

        float getMaxOffsetXLocked() {
            return mMagnificationBounds.left -
                    mMagnificationBounds.left * mCurrentMagnificationSpec.scale;
        }

        float getMinOffsetYLocked() {
            final float viewportHeight = mMagnificationBounds.height();
            final float viewportTop = mMagnificationBounds.top;
            return (viewportTop + viewportHeight) -
                    (viewportTop + viewportHeight) * mCurrentMagnificationSpec.scale;
        }

        float getMaxOffsetYLocked() {
            return mMagnificationBounds.top -
                    mMagnificationBounds.top * mCurrentMagnificationSpec.scale;
        }

        @Override
        public String toString() {
            return "DisplayMagnification["
                    + "mCurrentMagnificationSpec=" + mCurrentMagnificationSpec
                    + ", mMagnificationRegion=" + mMagnificationRegion
                    + ", mMagnificationBounds=" + mMagnificationBounds
                    + ", mDisplayId=" + mDisplayId
                    + ", mIdOfLastServiceToMagnify=" + mIdOfLastServiceToMagnify
                    + ", mRegistered=" + mRegistered
                    + ", mUnregisterPending=" + mUnregisterPending
                    + ']';
        }
    }

    /**
     * MagnificationController Constructor
     */
    public MagnificationController(@NonNull Context context,
            @NonNull AccessibilityManagerService ams, @NonNull Object lock) {
        this(new ControllerContext(context, ams,
                LocalServices.getService(WindowManagerInternal.class),
                new Handler(context.getMainLooper()),
                context.getResources().getInteger(R.integer.config_longAnimTime)), lock);
    }

    /**
     * Constructor for tests
     */
    @VisibleForTesting
    public MagnificationController(@NonNull ControllerContext ctx, @NonNull Object lock) {
        mControllerCtx = ctx;
        mLock = lock;
        mMainThreadId = mControllerCtx.getContext().getMainLooper().getThread().getId();
        mScreenStateObserver = new ScreenStateObserver(mControllerCtx.getContext(), this);
    }

    /**
     * Start tracking the magnification region for services that control magnification and the
     * magnification gesture handler.
     *
     * This tracking imposes a cost on the system, so we avoid tracking this data unless it's
     * required.
     *
     * @param displayId The logical display id.
     */
    public void register(int displayId) {
        synchronized (mLock) {
            DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                display = new DisplayMagnification(displayId);
            }
            if (display.isRegistered()) {
                return;
            }
            if (display.register()) {
                mDisplays.put(displayId, display);
                mScreenStateObserver.registerIfNecessary();
            }
        }
    }

    /**
     * Stop requiring tracking the magnification region. We may remain registered while we
     * reset magnification.
     *
     * @param displayId The logical display id.
     */
    public void unregister(int displayId) {
        synchronized (mLock) {
            unregisterLocked(displayId, false);
        }
    }

    /**
     * Stop tracking all displays' magnification region.
     */
    public void unregisterAll() {
        synchronized (mLock) {
            // display will be removed from array after unregister, we need to clone it to
            // prevent error.
            final SparseArray<DisplayMagnification> displays = mDisplays.clone();
            for (int i = 0; i < displays.size(); i++) {
                unregisterLocked(displays.keyAt(i), false);
            }
        }
    }

    /**
     * Remove the display magnification with given id.
     *
     * @param displayId The logical display id.
     */
    public void onDisplayRemoved(int displayId) {
        synchronized (mLock) {
            unregisterLocked(displayId, true);
        }
    }

    /**
     * Check if we are registered on specified display. Note that we may be planning to unregister
     * at any moment.
     *
     * @return {@code true} if the controller is registered on specified display.
     * {@code false} otherwise.
     *
     * @param displayId The logical display id.
     */
    public boolean isRegistered(int displayId) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return false;
            }
            return display.isRegistered();
        }
    }

    /**
     * @param displayId The logical display id.
     * @return {@code true} if magnification is active, e.g. the scale
     *         is > 1, {@code false} otherwise
     */
    public boolean isMagnifying(int displayId) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return false;
            }
            return display.isMagnifying();
        }
    }

    /**
     * Returns whether the magnification region contains the specified
     * screen-relative coordinates.
     *
     * @param displayId The logical display id.
     * @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
     *         magnified region, or {@code false} otherwise
     */
    public boolean magnificationRegionContains(int displayId, float x, float y) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return false;
            }
            return display.magnificationRegionContains(x, y);
        }
    }

    /**
     * Populates the specified rect with the screen-relative bounds of the
     * magnification region. If magnification is not enabled, the returned
     * bounds will be empty.
     *
     * @param displayId The logical display id.
     * @param outBounds rect to populate with the bounds of the magnified
     *                  region
     */
    public void getMagnificationBounds(int displayId, @NonNull Rect outBounds) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return;
            }
            display.getMagnificationBounds(outBounds);
        }
    }

    /**
     * Populates the specified region with the screen-relative magnification
     * region. If magnification is not enabled, then the returned region
     * will be empty.
     *
     * @param displayId The logical display id.
     * @param outRegion the region to populate
     */
    public void getMagnificationRegion(int displayId, @NonNull Region outRegion) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return;
            }
            display.getMagnificationRegion(outRegion);
        }
    }

    /**
     * Returns the magnification scale. If an animation is in progress,
     * this reflects the end state of the animation.
     *
     * @param displayId The logical display id.
     * @return the scale
     */
    public float getScale(int displayId) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return 1.0f;
            }
            return display.getScale();
        }
    }

    /**
     * Returns the X offset of the magnification viewport. If an animation
     * is in progress, this reflects the end state of the animation.
     *
     * @param displayId The logical display id.
     * @return the X offset
     */
    public float getOffsetX(int displayId) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return 0.0f;
            }
            return display.getOffsetX();
        }
    }

    /**
     * Returns the screen-relative X coordinate of the center of the
     * magnification viewport.
     *
     * @param displayId The logical display id.
     * @return the X coordinate
     */
    public float getCenterX(int displayId) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return 0.0f;
            }
            return display.getCenterX();
        }
    }

    /**
     * Returns the Y offset of the magnification viewport. If an animation
     * is in progress, this reflects the end state of the animation.
     *
     * @param displayId The logical display id.
     * @return the Y offset
     */
    public float getOffsetY(int displayId) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return 0.0f;
            }
            return display.getOffsetY();
        }
    }

    /**
     * Returns the screen-relative Y coordinate of the center of the
     * magnification viewport.
     *
     * @param displayId The logical display id.
     * @return the Y coordinate
     */
    public float getCenterY(int displayId) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return 0.0f;
            }
            return display.getCenterY();
        }
    }

    /**
     * Resets the magnification scale and center, optionally animating the
     * transition.
     *
     * @param displayId The logical display id.
     * @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 boolean reset(int displayId, boolean animate) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return false;
            }
            return display.reset(animate);
        }
    }

    /**
     * Scales the magnified region around the specified pivot point,
     * optionally animating the transition. If animation is disabled, the
     * transition is immediate.
     *
     * @param displayId The logical display id.
     * @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
     * @param id the ID of the service requesting the change
     * @return {@code true} if the magnification spec changed, {@code false} if
     *         the spec did not change
     */
    public boolean setScale(int displayId, float scale, float pivotX, float pivotY,
            boolean animate, int id) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return false;
            }
            return display.setScale(scale, pivotX, pivotY, animate, id);
        }
    }

    /**
     * Sets the center of the magnified region, optionally animating the
     * transition. If animation is disabled, the transition is immediate.
     *
     * @param displayId The logical display id.
     * @param centerX the screen-relative X coordinate around which to
     *                center
     * @param centerY the screen-relative Y coordinate around which to
     *                center
     * @param animate {@code true} to animate the transition, {@code false}
     *                to transition immediately
     * @param id      the ID of the service requesting the change
     * @return {@code true} if the magnification spec changed, {@code false} if
     * the spec did not change
     */
    public boolean setCenter(int displayId, float centerX, float centerY, boolean animate, int id) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return false;
            }
            return display.setScaleAndCenter(Float.NaN, centerX, centerY, animate, id);
        }
    }

    /**
     * Sets the scale and center of the magnified region, optionally
     * animating the transition. If animation is disabled, the transition
     * is immediate.
     *
     * @param displayId The logical display id.
     * @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, or {@link Float#NaN} to leave unchanged
     * @param centerY the screen-relative Y coordinate around which to
     *                center and scale, or {@link Float#NaN} to leave unchanged
     * @param animate {@code true} to animate the transition, {@code false}
     *                to transition immediately
     * @param id the ID of the service requesting the change
     * @return {@code true} if the magnification spec changed, {@code false} if
     *         the spec did not change
     */
    public boolean setScaleAndCenter(int displayId, float scale, float centerX, float centerY,
            boolean animate, int id) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return false;
            }
            return display.setScaleAndCenter(scale, centerX, centerY, animate, id);
        }
    }

    /**
     * Offsets the magnified region. Note that the offsetX and offsetY values actually move in the
     * opposite direction as the offsets passed in here.
     *
     * @param displayId The logical display id.
     * @param offsetX the amount in pixels to offset the region in the X direction, in current
     *                screen pixels.
     * @param offsetY the amount in pixels to offset the region in the Y direction, in current
     *                screen pixels.
     * @param id      the ID of the service requesting the change
     */
    public void offsetMagnifiedRegion(int displayId, float offsetX, float offsetY, int id) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return;
            }
            display.offsetMagnifiedRegion(offsetX, offsetY, id);
        }
    }

    /**
     * Get the ID of the last service that changed the magnification spec.
     *
     * @param displayId The logical display id.
     * @return The id
     */
    public int getIdOfLastServiceToMagnify(int displayId) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return -1;
            }
            return display.getIdOfLastServiceToMagnify();
        }
    }

    /**
     * Persists the default display magnification scale to the current user's settings.
     */
    public void persistScale() {
        // TODO: b/123047354, Need support multi-display?
        final float scale = getScale(Display.DEFAULT_DISPLAY);
        final int userId = mUserId;

        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                mControllerCtx.putMagnificationScale(scale, userId);
                return null;
            }
        }.execute();
    }

    /**
     * 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 mControllerCtx.getMagnificationScale(mUserId);
    }

    /**
     * Sets the currently active user ID.
     *
     * @param userId the currently active user ID
     */
    public void setUserId(int userId) {
        if (mUserId == userId) {
            return;
        }
        mUserId = userId;
        resetAllIfNeeded(false);
    }

    /**
     * Resets all displays' magnification if last magnifying service is disabled.
     *
     * @param connectionId
     */
    public void resetAllIfNeeded(int connectionId) {
        synchronized (mLock) {
            for (int i = 0; i < mDisplays.size(); i++) {
                resetIfNeeded(mDisplays.keyAt(i), connectionId);
            }
        }
    }

    /**
     * Resets magnification if magnification and auto-update are both enabled.
     *
     * @param displayId The logical display id.
     * @param animate whether the animate the transition
     * @return whether was {@link #isMagnifying(int) magnifying}
     */
    boolean resetIfNeeded(int displayId, boolean animate) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null || !display.isMagnifying()) {
                return false;
            }
            display.reset(animate);
            return true;
        }
    }

    /**
     * Resets magnification if last magnifying service is disabled.
     *
     * @param displayId The logical display id.
     * @param connectionId the connection ID be disabled.
     * @return {@code true} on success, {@code false} on failure
     */
    boolean resetIfNeeded(int displayId, int connectionId) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null || !display.isMagnifying()
                    || connectionId != display.getIdOfLastServiceToMagnify()) {
                return false;
            }
            display.reset(true);
            return true;
        }
    }

    void setForceShowMagnifiableBounds(int displayId, boolean show) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return;
            }
            display.setForceShowMagnifiableBounds(show);
        }
    }

    private void onScreenTurnedOff() {
        final Message m = PooledLambda.obtainMessage(
                MagnificationController::resetAllIfNeeded, this, false);
        mControllerCtx.getHandler().sendMessage(m);
    }

    private void resetAllIfNeeded(boolean animate) {
        synchronized (mLock) {
            for (int i = 0; i < mDisplays.size(); i++) {
                resetIfNeeded(mDisplays.keyAt(i), animate);
            }
        }
    }

    private void unregisterLocked(int displayId, boolean delete) {
        final DisplayMagnification display = mDisplays.get(displayId);
        if (display == null) {
            return;
        }
        if (!display.isRegistered()) {
            if (delete) {
                mDisplays.remove(displayId);
            }
            return;
        }
        if (!display.isMagnifying()) {
            display.unregister(delete);
        } else {
            display.unregisterPending(delete);
        }
    }

    /**
     * Callbacks from DisplayMagnification after display magnification unregistered. It will remove
     * DisplayMagnification instance if delete is true, and unregister screen state if
     * there is no registered display magnification.
     */
    private void unregisterCallbackLocked(int displayId, boolean delete) {
        if (delete) {
            mDisplays.remove(displayId);
        }
        // unregister screen state if necessary
        boolean hasRegister = false;
        for (int i = 0; i < mDisplays.size(); i++) {
            final DisplayMagnification display = mDisplays.valueAt(i);
            hasRegister = display.isRegistered();
            if (hasRegister) {
                break;
            }
        }
        if (!hasRegister) {
            mScreenStateObserver.unregister();
        }
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("MagnificationController[");
        builder.append("mUserId=").append(mUserId);
        builder.append(", mDisplays=").append(mDisplays);
        builder.append("]");
        return builder.toString();
    }

    /**
     * Class responsible for animating spec on the main thread and sending spec
     * updates to the window manager.
     */
    private static class SpecAnimationBridge implements ValueAnimator.AnimatorUpdateListener {
        private final ControllerContext mControllerCtx;

        /**
         * The magnification spec that was sent to the window manager. This should
         * only be accessed with the lock held.
         */
        private final MagnificationSpec mSentMagnificationSpec = MagnificationSpec.obtain();

        private final MagnificationSpec mStartMagnificationSpec = MagnificationSpec.obtain();

        private final MagnificationSpec mEndMagnificationSpec = MagnificationSpec.obtain();

        private final MagnificationSpec mTmpMagnificationSpec = MagnificationSpec.obtain();

        /**
         * The animator should only be accessed and modified on the main (e.g. animation) thread.
         */
        private final ValueAnimator mValueAnimator;

        private final Object mLock;

        private final int mDisplayId;

        @GuardedBy("mLock")
        private boolean mEnabled = false;

        private SpecAnimationBridge(ControllerContext ctx, Object lock, int displayId) {
            mControllerCtx = ctx;
            mLock = lock;
            mDisplayId = displayId;
            final long animationDuration = mControllerCtx.getAnimationDuration();
            mValueAnimator = mControllerCtx.newValueAnimator();
            mValueAnimator.setDuration(animationDuration);
            mValueAnimator.setInterpolator(new DecelerateInterpolator(2.5f));
            mValueAnimator.setFloatValues(0.0f, 1.0f);
            mValueAnimator.addUpdateListener(this);
        }

        /**
         * Enabled means the bridge will accept input. When not enabled, the output of the animator
         * will be ignored
         */
        public void setEnabled(boolean enabled) {
            synchronized (mLock) {
                if (enabled != mEnabled) {
                    mEnabled = enabled;
                    if (!mEnabled) {
                        mSentMagnificationSpec.clear();
                        mControllerCtx.getWindowManager().setMagnificationSpec(
                                mDisplayId, mSentMagnificationSpec);
                    }
                }
            }
        }

        public void updateSentSpecMainThread(MagnificationSpec spec, boolean animate) {
            if (mValueAnimator.isRunning()) {
                mValueAnimator.cancel();
            }

            // If the current and sent specs don't match, update the sent spec.
            synchronized (mLock) {
                final boolean changed = !mSentMagnificationSpec.equals(spec);
                if (changed) {
                    if (animate) {
                        animateMagnificationSpecLocked(spec);
                    } else {
                        setMagnificationSpecLocked(spec);
                    }
                }
            }
        }

        @GuardedBy("mLock")
        private void setMagnificationSpecLocked(MagnificationSpec spec) {
            if (mEnabled) {
                if (DEBUG_SET_MAGNIFICATION_SPEC) {
                    Slog.i(LOG_TAG, "Sending: " + spec);
                }

                mSentMagnificationSpec.setTo(spec);
                mControllerCtx.getWindowManager().setMagnificationSpec(
                        mDisplayId, mSentMagnificationSpec);
            }
        }

        private void animateMagnificationSpecLocked(MagnificationSpec toSpec) {
            mEndMagnificationSpec.setTo(toSpec);
            mStartMagnificationSpec.setTo(mSentMagnificationSpec);
            mValueAnimator.start();
        }

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            synchronized (mLock) {
                if (mEnabled) {
                    float fract = animation.getAnimatedFraction();
                    mTmpMagnificationSpec.scale = mStartMagnificationSpec.scale +
                            (mEndMagnificationSpec.scale - mStartMagnificationSpec.scale) * fract;
                    mTmpMagnificationSpec.offsetX = mStartMagnificationSpec.offsetX +
                            (mEndMagnificationSpec.offsetX - mStartMagnificationSpec.offsetX)
                                    * fract;
                    mTmpMagnificationSpec.offsetY = mStartMagnificationSpec.offsetY +
                            (mEndMagnificationSpec.offsetY - mStartMagnificationSpec.offsetY)
                                    * fract;
                    setMagnificationSpecLocked(mTmpMagnificationSpec);
                }
            }
        }
    }

    private static class ScreenStateObserver extends BroadcastReceiver {
        private final Context mContext;
        private final MagnificationController mController;
        private boolean mRegistered = false;

        public ScreenStateObserver(Context context, MagnificationController controller) {
            mContext = context;
            mController = controller;
        }

        public void registerIfNecessary() {
            if (!mRegistered) {
                mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF));
                mRegistered = true;
            }
        }

        public void unregister() {
            if (mRegistered) {
                mContext.unregisterReceiver(this);
                mRegistered = false;
            }
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            mController.onScreenTurnedOff();
        }
    }

    /**
     * This class holds resources used between the classes in MagnificationController, and
     * functions for tests to mock it.
     */
    @VisibleForTesting
    public static class ControllerContext {
        private final Context mContext;
        private final AccessibilityManagerService mAms;
        private final WindowManagerInternal mWindowManager;
        private final Handler mHandler;
        private final Long mAnimationDuration;

        /**
         * Constructor for ControllerContext.
         */
        public ControllerContext(@NonNull Context context,
                @NonNull AccessibilityManagerService ams,
                @NonNull WindowManagerInternal windowManager,
                @NonNull Handler handler,
                long animationDuration) {
            mContext = context;
            mAms = ams;
            mWindowManager = windowManager;
            mHandler = handler;
            mAnimationDuration = animationDuration;
        }

        /**
         * @return A context.
         */
        @NonNull
        public Context getContext() {
            return mContext;
        }

        /**
         * @return AccessibilityManagerService
         */
        @NonNull
        public AccessibilityManagerService getAms() {
            return mAms;
        }

        /**
         * @return WindowManagerInternal
         */
        @NonNull
        public WindowManagerInternal getWindowManager() {
            return mWindowManager;
        }

        /**
         * @return Handler for main looper
         */
        @NonNull
        public Handler getHandler() {
            return mHandler;
        }

        /**
         * Create a new ValueAnimator.
         *
         * @return ValueAnimator
         */
        @NonNull
        public ValueAnimator newValueAnimator() {
            return new ValueAnimator();
        }

        /**
         * Write Settings of magnification scale.
         */
        public void putMagnificationScale(float value, int userId) {
            Settings.Secure.putFloatForUser(mContext.getContentResolver(),
                    Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, value, userId);
        }

        /**
         * Get Settings of magnification scale.
         */
        public float getMagnificationScale(int userId) {
            return Settings.Secure.getFloatForUser(mContext.getContentResolver(),
                    Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
                    DEFAULT_MAGNIFICATION_SCALE, userId);
        }

        /**
         * @return Configuration of animation duration.
         */
        public long getAnimationDuration() {
            return mAnimationDuration;
        }
    }
}
