content: /*
   * 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;
          }
      }
  }
  