/*
 * 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.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.Message;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.MathUtils;
import android.util.Slog;
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.os.SomeArgs;
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.
 */
class MagnificationController implements Handler.Callback {
    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 = 5.0f;

    private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false;

    private static final int INVALID_ID = -1;

    private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f;

    // Messages
    private static final int MSG_SEND_SPEC_TO_ANIMATION = 1;
    private static final int MSG_SCREEN_TURNED_OFF = 2;
    private static final int MSG_ON_MAGNIFIED_BOUNDS_CHANGED = 3;
    private static final int MSG_ON_RECTANGLE_ON_SCREEN_REQUESTED = 4;
    private static final int MSG_ON_USER_CONTEXT_CHANGED = 5;

    private final Object mLock;

    /**
     * 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 AccessibilityManagerService mAms;

    private final SettingsBridge mSettingsBridge;

    private final ScreenStateObserver mScreenStateObserver;

    private final SpecAnimationBridge mSpecAnimationBridge;

    private final WindowManagerInternal.MagnificationCallbacks mWMCallbacks =
            new WindowManagerInternal.MagnificationCallbacks () {
                @Override
                public void onMagnificationRegionChanged(Region region) {
                    final SomeArgs args = SomeArgs.obtain();
                    args.arg1 = Region.obtain(region);
                    mHandler.obtainMessage(MSG_ON_MAGNIFIED_BOUNDS_CHANGED, args).sendToTarget();
                }

                @Override
                public void onRectangleOnScreenRequested(int left, int top, int right, int bottom) {
                    final SomeArgs args = SomeArgs.obtain();
                    args.argi1 = left;
                    args.argi2 = top;
                    args.argi3 = right;
                    args.argi4 = bottom;
                    mHandler.obtainMessage(MSG_ON_RECTANGLE_ON_SCREEN_REQUESTED, args)
                            .sendToTarget();
                }

                @Override
                public void onRotationChanged(int rotation) {
                    // Treat as context change and reset
                    mHandler.sendEmptyMessage(MSG_ON_USER_CONTEXT_CHANGED);
                }

                @Override
                public void onUserContextChanged() {
                    mHandler.sendEmptyMessage(MSG_ON_USER_CONTEXT_CHANGED);
                }
            };

    private int mUserId;

    private final long mMainThreadId;

    private Handler mHandler;

    private int mIdOfLastServiceToMagnify = INVALID_ID;

    private final WindowManagerInternal mWindowManager;

    // Flag indicating that we are registered with window manager.
    @VisibleForTesting boolean mRegistered;

    private boolean mUnregisterPending;

    public MagnificationController(Context context, AccessibilityManagerService ams, Object lock) {
        this(context, ams, lock, null, LocalServices.getService(WindowManagerInternal.class),
                new ValueAnimator(), new SettingsBridge(context.getContentResolver()));
        mHandler = new Handler(context.getMainLooper(), this);
    }

    public MagnificationController(
            Context context,
            AccessibilityManagerService ams,
            Object lock,
            Handler handler,
            WindowManagerInternal windowManagerInternal,
            ValueAnimator valueAnimator,
            SettingsBridge settingsBridge) {
        mHandler = handler;
        mWindowManager = windowManagerInternal;
        mMainThreadId = context.getMainLooper().getThread().getId();
        mAms = ams;
        mScreenStateObserver = new ScreenStateObserver(context, this);
        mLock = lock;
        mSpecAnimationBridge = new SpecAnimationBridge(
                context, mLock, mWindowManager, valueAnimator);
        mSettingsBridge = settingsBridge;
    }

    /**
     * Start tracking the magnification region for services that control magnification and the
     * magnification gesture handler.
     *
     * This tracking imposes a cost on the system, so we avoid tracking this data unless it's
     * required.
     */
    public void register() {
        synchronized (mLock) {
            if (!mRegistered) {
                mScreenStateObserver.register();
                mWindowManager.setMagnificationCallbacks(mWMCallbacks);
                mSpecAnimationBridge.setEnabled(true);
                // Obtain initial state.
                mWindowManager.getMagnificationRegion(mMagnificationRegion);
                mMagnificationRegion.getBounds(mMagnificationBounds);
                mRegistered = true;
            }
        }
    }

    /**
     * Stop requiring tracking the magnification region. We may remain registered while we
     * reset magnification.
     */
    public void unregister() {
        synchronized (mLock) {
            if (!isMagnifying()) {
                unregisterInternalLocked();
            } else {
                mUnregisterPending = true;
                resetLocked(true);
            }
        }
    }

    /**
     * Check if we are registered. Note that we may be planning to unregister at any moment.
     *
     * @return {@code true} if the controller is registered. {@code false} otherwise.
     */
    public boolean isRegisteredLocked() {
        return mRegistered;
    }

    private void unregisterInternalLocked() {
        if (mRegistered) {
            mSpecAnimationBridge.setEnabled(false);
            mScreenStateObserver.unregister();
            mWindowManager.setMagnificationCallbacks(null);
            mMagnificationRegion.setEmpty();
            mRegistered = false;
        }
        mUnregisterPending = false;
    }

    /**
     * @return {@code true} if magnification is active, e.g. the scale
     *         is > 1, {@code false} otherwise
     */
    public boolean isMagnifying() {
        return mCurrentMagnificationSpec.scale > 1.0f;
    }

    /**
     * Update our copy of the current magnification region
     *
     * @param magnified the magnified region
     */
    private void onMagnificationRegionChanged(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();
            }
        }
    }

    /**
     * Returns whether the magnification region 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
     *         magnified region, or {@code false} otherwise
     */
    public boolean magnificationRegionContains(float x, float y) {
        synchronized (mLock) {
            return mMagnificationRegion.contains((int) x, (int) 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 outBounds rect to populate with the bounds of the magnified
     *                  region
     */
    public void getMagnificationBounds(@NonNull Rect outBounds) {
        synchronized (mLock) {
            outBounds.set(mMagnificationBounds);
        }
    }

    /**
     * Populates the specified region with the screen-relative magnification
     * region. If magnification is not enabled, then the returned region
     * will be empty.
     *
     * @param outRegion the region to populate
     */
    public void getMagnificationRegion(@NonNull Region outRegion) {
        synchronized (mLock) {
            outRegion.set(mMagnificationRegion);
        }
    }

    /**
     * Returns the magnification scale. If an animation is in progress,
     * this reflects the end state of the animation.
     *
     * @return the scale
     */
    public float getScale() {
        return mCurrentMagnificationSpec.scale;
    }

    /**
     * Returns the X offset of the magnification viewport. If an animation
     * is in progress, this reflects the end state of the animation.
     *
     * @return the X offset
     */
    public float getOffsetX() {
        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 (mMagnificationBounds.width() / 2.0f
                    + mMagnificationBounds.left - getOffsetX()) / getScale();
        }
    }

    /**
     * Returns the Y offset of the magnification viewport. If an animation
     * is in progress, this reflects the end state of the animation.
     *
     * @return the Y offset
     */
    public float getOffsetY() {
        return mCurrentMagnificationSpec.offsetY;
    }

    /**
     * Returns the screen-relative Y coordinate of the center of the
     * magnification viewport.
     *
     * @return the Y coordinate
     */
    public float getCenterY() {
        synchronized (mLock) {
            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
     */
    private 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
     */
    private 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
     */
    private float getSentOffsetY() {
        return mSpecAnimationBridge.mSentMagnificationSpec.offsetY;
    }

    /**
     * Resets the magnification scale and center, optionally animating the
     * transition.
     *
     * @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(boolean animate) {
        synchronized (mLock) {
            return resetLocked(animate);
        }
    }

    private boolean resetLocked(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;
    }

    /**
     * Scales the magnified region around the specified pivot point,
     * optionally animating the transition. If animation is disabled, the
     * 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
     * @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(float scale, float pivotX, float pivotY, boolean animate, int id) {
        synchronized (mLock) {
            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) / 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;
            mIdOfLastServiceToMagnify = id;
            return setScaleAndCenterLocked(scale, centerX, centerY, animate, id);
        }
    }

    /**
     * Sets the center of the magnified region, optionally animating the
     * transition. If animation is disabled, the transition is immediate.
     *
     * @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(float centerX, float centerY, boolean animate, int id) {
        synchronized (mLock) {
            if (!mRegistered) {
                return false;
            }
            return setScaleAndCenterLocked(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 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(
            float scale, float centerX, float centerY, boolean animate, int id) {
        synchronized (mLock) {
            if (!mRegistered) {
                return false;
            }
            return setScaleAndCenterLocked(scale, centerX, centerY, animate, id);
        }
    }

    private boolean setScaleAndCenterLocked(float scale, float centerX, float centerY,
            boolean animate, int id) {
        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;
    }

    /**
     * Offsets the magnified region. Note that the offsetX and offsetY values actually move in the
     * opposite direction as the offsets passed in here.
     *
     * @param offsetX the amount in pixels to offset the 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(float offsetX, float offsetY, int id) {
        synchronized (mLock) {
            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);
        }
    }

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

    private void onMagnificationChangedLocked() {
        mAms.notifyMagnificationChanged(mMagnificationRegion,
                getScale(), getCenterX(), getCenterY());
        if (mUnregisterPending && !isMagnifying()) {
            unregisterInternalLocked();
        }
    }

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

    private 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(), 0);
        if (Float.compare(mCurrentMagnificationSpec.offsetX, offsetX) != 0) {
            mCurrentMagnificationSpec.offsetX = offsetX;
            changed = true;
        }
        final float offsetY = MathUtils.constrain(nonNormOffsetY, getMinOffsetYLocked(), 0);
        if (Float.compare(mCurrentMagnificationSpec.offsetY, offsetY) != 0) {
            mCurrentMagnificationSpec.offsetY = offsetY;
            changed = true;
        }
        return changed;
    }

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

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

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

            synchronized (mLock) {
                if (isMagnifying()) {
                    reset(false);
                }
            }
        }
    }

    /**
     * Resets magnification if magnification and auto-update are both enabled.
     *
     * @param animate whether the animate the transition
     * @return whether was {@link #isMagnifying magnifying}
     */
    boolean resetIfNeeded(boolean animate) {
        synchronized (mLock) {
            if (isMagnifying()) {
                reset(animate);
                return true;
            }
            return false;
        }
    }

    void setForceShowMagnifiableBounds(boolean show) {
        if (mRegistered) {
            mWindowManager.setForceShowMagnifiableBounds(show);
        }
    }

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

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

    private 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 {
            mHandler.obtainMessage(MSG_SEND_SPEC_TO_ANIMATION,
                    animate ? 1 : 0, 0, spec).sendToTarget();
        }
    }

    private void onScreenTurnedOff() {
        mHandler.sendEmptyMessage(MSG_SCREEN_TURNED_OFF);
    }

    public boolean handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_SEND_SPEC_TO_ANIMATION:
                final boolean animate = msg.arg1 == 1;
                final MagnificationSpec spec = (MagnificationSpec) msg.obj;
                mSpecAnimationBridge.updateSentSpecMainThread(spec, animate);
                break;
            case MSG_SCREEN_TURNED_OFF:
                resetIfNeeded(false);
                break;
            case MSG_ON_MAGNIFIED_BOUNDS_CHANGED: {
                final SomeArgs args = (SomeArgs) msg.obj;
                final Region magnifiedBounds = (Region) args.arg1;
                onMagnificationRegionChanged(magnifiedBounds);
                magnifiedBounds.recycle();
                args.recycle();
            } break;
            case MSG_ON_RECTANGLE_ON_SCREEN_REQUESTED: {
                final SomeArgs args = (SomeArgs) msg.obj;
                final int left = args.argi1;
                final int top = args.argi2;
                final int right = args.argi3;
                final int bottom = args.argi4;
                requestRectangleOnScreen(left, top, right, bottom);
                args.recycle();
            } break;
            case MSG_ON_USER_CONTEXT_CHANGED:
                resetIfNeeded(true);
                break;
        }
        return true;
    }

    @Override
    public String toString() {
        return "MagnificationController{" +
                "mCurrentMagnificationSpec=" + mCurrentMagnificationSpec +
                ", mMagnificationRegion=" + mMagnificationRegion +
                ", mMagnificationBounds=" + mMagnificationBounds +
                ", mUserId=" + mUserId +
                ", mIdOfLastServiceToMagnify=" + mIdOfLastServiceToMagnify +
                ", mRegistered=" + mRegistered +
                ", mUnregisterPending=" + mUnregisterPending +
                '}';
    }

    /**
     * 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 WindowManagerInternal mWindowManager;

        /**
         * 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;

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

        private SpecAnimationBridge(Context context, Object lock, WindowManagerInternal wm,
                ValueAnimator animator) {
            mLock = lock;
            mWindowManager = wm;
            final long animationDuration = context.getResources().getInteger(
                    R.integer.config_longAnimTime);
            mValueAnimator = animator;
            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();
                        mWindowManager.setMagnificationSpec(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);
                mWindowManager.setMagnificationSpec(spec);
            }
        }

        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;
                    synchronized (mLock) {
                        setMagnificationSpecLocked(mTmpMagnificationSpec);
                    }
                }
            }
        }
    }

    private static class ScreenStateObserver extends BroadcastReceiver {
        private final Context mContext;
        private final MagnificationController mController;

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

        public void register() {
            mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF));
        }

        public void unregister() {
            mContext.unregisterReceiver(this);
        }

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

    // Extra class to get settings so tests can mock it
    public static class SettingsBridge {
        private final ContentResolver mContentResolver;

        public SettingsBridge(ContentResolver contentResolver) {
            mContentResolver = contentResolver;
        }

        public void putMagnificationScale(float value, int userId) {
            Settings.Secure.putFloatForUser(mContentResolver,
                    Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, value, userId);
        }

        public float getMagnificationScale(int userId) {
            return Settings.Secure.getFloatForUser(mContentResolver,
                    Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
                    DEFAULT_MAGNIFICATION_SCALE, userId);
        }
    }
}
