diff options
9 files changed, 1145 insertions, 403 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 89c6eb2fa5f9..1b905a0204a4 100755 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5161,18 +5161,10 @@ public final class Settings { "accessibility_display_magnification_scale"; /** - * Setting that specifies whether the display magnification should be - * automatically updated. If this fearture is enabled the system will - * exit magnification mode or pan the viewport when a context change - * occurs. For example, on staring a new activity or rotating the screen, - * the system may zoom out so the user can see the new context he is in. - * Another example is on showing a window that is not visible in the - * magnified viewport the system may pan the viewport to make the window - * the has popped up so the user knows that the context has changed. - * Whether a screen magnification is performed is controlled by - * {@link #ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED} + * Unused mangnification setting * * @hide + * @deprecated */ public static final String ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE = "accessibility_display_magnification_auto_update"; @@ -6491,7 +6483,6 @@ public final class Settings { ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, - ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE, ACCESSIBILITY_SCRIPT_INJECTION, ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS, ENABLED_ACCESSIBILITY_SERVICES, diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java index d55bb4f44aa5..dd543a35a579 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java @@ -1310,10 +1310,6 @@ class DatabaseHelper extends SQLiteOpenHelper { loadFractionSetting(stmt, Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, R.fraction.def_accessibility_display_magnification_scale, 1); stmt.close(); - stmt = db.compileStatement("INSERT INTO secure(name,value) VALUES(?,?);"); - loadBooleanSetting(stmt, - Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE, - R.bool.def_accessibility_display_magnification_auto_update); db.setTransactionSuccessful(); } finally { @@ -2508,10 +2504,6 @@ class DatabaseHelper extends SQLiteOpenHelper { loadFractionSetting(stmt, Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, R.fraction.def_accessibility_display_magnification_scale, 1); - loadBooleanSetting(stmt, - Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE, - R.bool.def_accessibility_display_magnification_auto_update); - loadBooleanSetting(stmt, Settings.Secure.USER_SETUP_COMPLETE, R.bool.def_user_setup_complete); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index f1d1b1f6e240..88ee33c32ad3 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -2175,7 +2175,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 134; + private static final int SETTINGS_VERSION = 135; private final int mUserId; @@ -2545,6 +2545,14 @@ public class SettingsProvider extends ContentProvider { currentVersion = 134; } + if (currentVersion == 134) { + // Remove setting that specifies if magnification values should be preserved. + // This setting defaulted to true and never has a UI. + getSecureSettingsLocked(userId).deleteSettingLocked( + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE); + currentVersion = 135; + } + if (currentVersion != newVersion) { Slog.wtf("SettingsProvider", "warning: upgrading settings database to version " + newVersion + " left it at " diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index c89f158671f9..3ea03f59d78e 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -829,9 +829,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { * @param centerX the new screen-relative center X coordinate * @param centerY the new screen-relative center Y coordinate */ - void notifyMagnificationChanged(@NonNull Region region, + public void notifyMagnificationChanged(@NonNull Region region, float scale, float centerX, float centerY) { synchronized (mLock) { + notifyClearAccessibilityCacheLocked(); notifyMagnificationChangedLocked(region, scale, centerX, centerY); } } @@ -901,10 +902,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { mSecurityPolicy.onTouchInteractionEnd(); } - void onMagnificationStateChanged() { - notifyClearAccessibilityCacheLocked(); - } - private void switchUser(int userId) { synchronized (mLock) { if (mCurrentUserId == userId && mInitialized) { diff --git a/services/accessibility/java/com/android/server/accessibility/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/MagnificationController.java index 7886b9ea7fad..f65046ce4350 100644 --- a/services/accessibility/java/com/android/server/accessibility/MagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/MagnificationController.java @@ -21,8 +21,6 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.os.SomeArgs; import com.android.server.LocalServices; -import android.animation.ObjectAnimator; -import android.animation.TypeEvaluator; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.content.BroadcastReceiver; @@ -34,12 +32,10 @@ import android.graphics.Rect; import android.graphics.Region; import android.os.AsyncTask; import android.os.Handler; -import android.os.Looper; import android.os.Message; import android.provider.Settings; import android.text.TextUtils; import android.util.MathUtils; -import android.util.Property; import android.util.Slog; import android.view.MagnificationSpec; import android.view.View; @@ -53,27 +49,29 @@ import java.util.Locale; * from the accessibility manager and related classes. It is responsible for * holding the current state of magnification and animation, and it handles * communication between the accessibility manager and window manager. + * + * Magnification is limited to the range [MIN_SCALE, MAX_SCALE], and can only occur inside the + * magnification region. If a value is out of bounds, it will be adjusted to guarantee these + * constraints. */ -class MagnificationController { +class MagnificationController implements Handler.Callback { private static final String LOG_TAG = "MagnificationController"; - private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false; + public static final float MIN_SCALE = 1.0f; + public static final float MAX_SCALE = 5.0f; - private static final int DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE = 1; + private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false; private static final int INVALID_ID = -1; private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f; - private static final float MIN_SCALE = 1.0f; - private static final float MAX_SCALE = 5.0f; - - /** - * The minimum scaling factor that can be persisted to secure settings. - * This must be > 1.0 to ensure that magnification is actually set to an - * enabled state when the scaling factor is restored from settings. - */ - private static final float MIN_PERSISTED_SCALE = 2.0f; + // Messages + private static final int MSG_SEND_SPEC_TO_ANIMATION = 1; + private static final int MSG_SCREEN_TURNED_OFF = 2; + private static final int MSG_ON_MAGNIFIED_BOUNDS_CHANGED = 3; + private static final int MSG_ON_RECTANGLE_ON_SCREEN_REQUESTED = 4; + private static final int MSG_ON_USER_CONTEXT_CHANGED = 5; private final Object mLock; @@ -90,46 +88,95 @@ class MagnificationController { private final Rect mTempRect1 = new Rect(); private final AccessibilityManagerService mAms; - private final ContentResolver mContentResolver; + + private final SettingsBridge mSettingsBridge; private final ScreenStateObserver mScreenStateObserver; - private final WindowStateObserver mWindowStateObserver; private final SpecAnimationBridge mSpecAnimationBridge; + private final WindowManagerInternal.MagnificationCallbacks mWMCallbacks = + new WindowManagerInternal.MagnificationCallbacks () { + @Override + public void onMagnificationRegionChanged(Region region) { + final SomeArgs args = SomeArgs.obtain(); + args.arg1 = Region.obtain(region); + mHandler.obtainMessage(MSG_ON_MAGNIFIED_BOUNDS_CHANGED, args).sendToTarget(); + } + + @Override + public void onRectangleOnScreenRequested(int left, int top, int right, int bottom) { + final SomeArgs args = SomeArgs.obtain(); + args.argi1 = left; + args.argi2 = top; + args.argi3 = right; + args.argi4 = bottom; + mHandler.obtainMessage(MSG_ON_RECTANGLE_ON_SCREEN_REQUESTED, args) + .sendToTarget(); + } + + @Override + public void onRotationChanged(int rotation) { + // Treat as context change and reset + mHandler.sendEmptyMessage(MSG_ON_USER_CONTEXT_CHANGED); + } + + @Override + public void onUserContextChanged() { + mHandler.sendEmptyMessage(MSG_ON_USER_CONTEXT_CHANGED); + } + }; + private int mUserId; + private final long mMainThreadId; + + private Handler mHandler; + private int mIdOfLastServiceToMagnify = INVALID_ID; + private final WindowManagerInternal mWindowManager; + // Flag indicating that we are registered with window manager. private boolean mRegistered; private boolean mUnregisterPending; public MagnificationController(Context context, AccessibilityManagerService ams, Object lock) { + this(context, ams, lock, null, LocalServices.getService(WindowManagerInternal.class), + new ValueAnimator(), new SettingsBridge(context.getContentResolver())); + mHandler = new Handler(context.getMainLooper(), this); + } + + public MagnificationController(Context context, AccessibilityManagerService ams, Object lock, + Handler handler, WindowManagerInternal windowManagerInternal, + ValueAnimator valueAnimator, SettingsBridge settingsBridge) { + mHandler = handler; + mWindowManager = windowManagerInternal; + mMainThreadId = context.getMainLooper().getThread().getId(); mAms = ams; - mContentResolver = context.getContentResolver(); mScreenStateObserver = new ScreenStateObserver(context, this); - mWindowStateObserver = new WindowStateObserver(context, this); mLock = lock; - mSpecAnimationBridge = new SpecAnimationBridge(context, mLock); + mSpecAnimationBridge = new SpecAnimationBridge( + context, mLock, mWindowManager, valueAnimator); + mSettingsBridge = settingsBridge; } /** * Start tracking the magnification region for services that control magnification and the * magnification gesture handler. * - * This tracking imposes a cost on the system, so we avoid tracking this data - * unless it's required. + * This tracking imposes a cost on the system, so we avoid tracking this data unless it's + * required. */ public void register() { synchronized (mLock) { if (!mRegistered) { mScreenStateObserver.register(); - mWindowStateObserver.register(); + mWindowManager.setMagnificationCallbacks(mWMCallbacks); mSpecAnimationBridge.setEnabled(true); // Obtain initial state. - mWindowStateObserver.getMagnificationRegion(mMagnificationRegion); + mWindowManager.getMagnificationRegion(mMagnificationRegion); mMagnificationRegion.getBounds(mMagnificationBounds); mRegistered = true; } @@ -164,7 +211,7 @@ class MagnificationController { if (mRegistered) { mSpecAnimationBridge.setEnabled(false); mScreenStateObserver.unregister(); - mWindowStateObserver.unregister(); + mWindowManager.setMagnificationCallbacks(null); mMagnificationRegion.setEmpty(); mRegistered = false; } @@ -183,40 +230,22 @@ class MagnificationController { * Update our copy of the current magnification region * * @param magnified the magnified region - * @param updateSpec {@code true} to update the scale and center based on - * the region bounds, {@code false} to leave them as-is */ - private void onMagnificationRegionChanged(Region magnified, boolean updateSpec) { + private void onMagnificationRegionChanged(Region magnified) { synchronized (mLock) { if (!mRegistered) { // Don't update if we've unregistered return; } - boolean magnificationChanged = false; - boolean boundsChanged = false; - if (!mMagnificationRegion.equals(magnified)) { mMagnificationRegion.set(magnified); mMagnificationRegion.getBounds(mMagnificationBounds); - boundsChanged = true; - } - if (updateSpec) { - final MagnificationSpec sentSpec = mSpecAnimationBridge.mSentMagnificationSpec; - final float scale = sentSpec.scale; - final float offsetX = sentSpec.offsetX; - final float offsetY = sentSpec.offsetY; - - // Compute the new center and update spec as needed. - final float centerX = (mMagnificationBounds.width() / 2.0f - + mMagnificationBounds.left - offsetX) / scale; - final float centerY = (mMagnificationBounds.height() / 2.0f - + mMagnificationBounds.top - offsetY) / scale; - magnificationChanged = setScaleAndCenterLocked( - scale, centerX, centerY, false, INVALID_ID); - } - - // If magnification changed we already notified for the change. - if (boundsChanged && updateSpec && !magnificationChanged) { + // It's possible that our magnification spec is invalid with the new bounds. + // Adjust the current spec's offsets if necessary. + if (updateCurrentSpecWithOffsetsLocked( + mCurrentMagnificationSpec.offsetX, mCurrentMagnificationSpec.offsetY)) { + sendSpecToAnimation(mCurrentMagnificationSpec, false); + } onMagnificationChangedLocked(); } } @@ -328,7 +357,7 @@ class MagnificationController { * * @return the scale currently used by the window manager */ - public float getSentScale() { + private float getSentScale() { return mSpecAnimationBridge.mSentMagnificationSpec.scale; } @@ -339,7 +368,7 @@ class MagnificationController { * * @return the X offset currently used by the window manager */ - public float getSentOffsetX() { + private float getSentOffsetX() { return mSpecAnimationBridge.mSentMagnificationSpec.offsetX; } @@ -350,7 +379,7 @@ class MagnificationController { * * @return the Y offset currently used by the window manager */ - public float getSentOffsetY() { + private float getSentOffsetY() { return mSpecAnimationBridge.mSentMagnificationSpec.offsetY; } @@ -380,7 +409,7 @@ class MagnificationController { onMagnificationChangedLocked(); } mIdOfLastServiceToMagnify = INVALID_ID; - mSpecAnimationBridge.updateSentSpec(spec, animate); + sendSpecToAnimation(spec, animate); return changed; } @@ -475,7 +504,7 @@ class MagnificationController { private boolean setScaleAndCenterLocked(float scale, float centerX, float centerY, boolean animate, int id) { final boolean changed = updateMagnificationSpecLocked(scale, centerX, centerY); - mSpecAnimationBridge.updateSentSpec(mCurrentMagnificationSpec, animate); + sendSpecToAnimation(mCurrentMagnificationSpec, animate); if (isMagnifying() && (id != INVALID_ID)) { mIdOfLastServiceToMagnify = id; } @@ -483,27 +512,28 @@ class MagnificationController { } /** - * Offsets the center of the magnified region. + * Offsets the magnified region. Note that the offsetX and offsetY values actually move in the + * opposite direction as the offsets passed in here. * - * @param offsetX the amount in pixels to offset the X center - * @param offsetY the amount in pixels to offset the Y center + * @param offsetX the amount in pixels to offset the region in the X direction, in current + * screen pixels. + * @param offsetY the amount in pixels to offset the region in the Y direction, in current + * screen pixels. * @param id the ID of the service requesting the change */ - public void offsetMagnifiedRegionCenter(float offsetX, float offsetY, int id) { + public void offsetMagnifiedRegion(float offsetX, float offsetY, int id) { synchronized (mLock) { if (!mRegistered) { return; } - final MagnificationSpec currSpec = mCurrentMagnificationSpec; - final float nonNormOffsetX = currSpec.offsetX - offsetX; - currSpec.offsetX = MathUtils.constrain(nonNormOffsetX, getMinOffsetXLocked(), 0); - final float nonNormOffsetY = currSpec.offsetY - offsetY; - currSpec.offsetY = MathUtils.constrain(nonNormOffsetY, getMinOffsetYLocked(), 0); + final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX; + final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY; + updateCurrentSpecWithOffsetsLocked(nonNormOffsetX, nonNormOffsetY); if (id != INVALID_ID) { mIdOfLastServiceToMagnify = id; } - mSpecAnimationBridge.updateSentSpec(currSpec, false); + sendSpecToAnimation(mCurrentMagnificationSpec, false); } } @@ -517,7 +547,6 @@ class MagnificationController { } private void onMagnificationChangedLocked() { - mAms.onMagnificationStateChanged(); mAms.notifyMagnificationChanged(mMagnificationRegion, getScale(), getCenterX(), getCenterY()); if (mUnregisterPending && !isMagnifying()) { @@ -535,8 +564,7 @@ class MagnificationController { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { - Settings.Secure.putFloatForUser(mContentResolver, - Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale, userId); + mSettingsBridge.putMagnificationScale(scale, userId); return null; } }.execute(); @@ -550,9 +578,7 @@ class MagnificationController { * scale if none is available */ public float getPersistedScale() { - return Settings.Secure.getFloatForUser(mContentResolver, - Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, - DEFAULT_MAGNIFICATION_SCALE, mUserId); + return mSettingsBridge.getMagnificationScale(mUserId); } /** @@ -578,36 +604,20 @@ class MagnificationController { scale = getScale(); } - // Ensure requested center is within the magnification region. - if (!magnificationRegionContains(centerX, centerY)) { - return false; - } - // Compute changes. - final MagnificationSpec currSpec = mCurrentMagnificationSpec; boolean changed = false; final float normScale = MathUtils.constrain(scale, MIN_SCALE, MAX_SCALE); - if (Float.compare(currSpec.scale, normScale) != 0) { - currSpec.scale = normScale; + if (Float.compare(mCurrentMagnificationSpec.scale, normScale) != 0) { + mCurrentMagnificationSpec.scale = normScale; changed = true; } final float nonNormOffsetX = mMagnificationBounds.width() / 2.0f - + mMagnificationBounds.left - centerX * scale; - final float offsetX = MathUtils.constrain(nonNormOffsetX, getMinOffsetXLocked(), 0); - if (Float.compare(currSpec.offsetX, offsetX) != 0) { - currSpec.offsetX = offsetX; - changed = true; - } - + + mMagnificationBounds.left - centerX * normScale; final float nonNormOffsetY = mMagnificationBounds.height() / 2.0f - + mMagnificationBounds.top - centerY * scale; - final float offsetY = MathUtils.constrain(nonNormOffsetY, getMinOffsetYLocked(), 0); - if (Float.compare(currSpec.offsetY, offsetY) != 0) { - currSpec.offsetY = offsetY; - changed = true; - } + + mMagnificationBounds.top - centerY * normScale; + changed |= updateCurrentSpecWithOffsetsLocked(nonNormOffsetX, nonNormOffsetY); if (changed) { onMagnificationChangedLocked(); @@ -616,6 +626,21 @@ class MagnificationController { return changed; } + private boolean updateCurrentSpecWithOffsetsLocked(float nonNormOffsetX, float nonNormOffsetY) { + boolean changed = false; + final float offsetX = MathUtils.constrain(nonNormOffsetX, getMinOffsetXLocked(), 0); + if (Float.compare(mCurrentMagnificationSpec.offsetX, offsetX) != 0) { + mCurrentMagnificationSpec.offsetX = offsetX; + changed = true; + } + final float offsetY = MathUtils.constrain(nonNormOffsetY, getMinOffsetYLocked(), 0); + if (Float.compare(mCurrentMagnificationSpec.offsetY, offsetY) != 0) { + mCurrentMagnificationSpec.offsetY = offsetY; + changed = true; + } + return changed; + } + private float getMinOffsetXLocked() { final float viewportWidth = mMagnificationBounds.width(); return viewportWidth - viewportWidth * mCurrentMagnificationSpec.scale; @@ -643,12 +668,6 @@ class MagnificationController { } } - private boolean isScreenMagnificationAutoUpdateEnabled() { - return (Settings.Secure.getInt(mContentResolver, - Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE, - DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1); - } - /** * Resets magnification if magnification and auto-update are both enabled. * @@ -658,7 +677,7 @@ class MagnificationController { */ boolean resetIfNeeded(boolean animate) { synchronized (mLock) { - if (isMagnifying() && isScreenMagnificationAutoUpdateEnabled()) { + if (isMagnifying()) { reset(animate); return true; } @@ -715,18 +734,61 @@ class MagnificationController { } final float scale = getScale(); - offsetMagnifiedRegionCenter(scrollX * scale, scrollY * scale, INVALID_ID); - } + offsetMagnifiedRegion(scrollX * scale, scrollY * scale, INVALID_ID); + } + } + + private void sendSpecToAnimation(MagnificationSpec spec, boolean animate) { + if (Thread.currentThread().getId() == mMainThreadId) { + mSpecAnimationBridge.updateSentSpecMainThread(spec, animate); + } else { + mHandler.obtainMessage(MSG_SEND_SPEC_TO_ANIMATION, + animate ? 1 : 0, 0, spec).sendToTarget(); + } + } + + private void onScreenTurnedOff() { + mHandler.sendEmptyMessage(MSG_SCREEN_TURNED_OFF); + } + + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_SEND_SPEC_TO_ANIMATION: + final boolean animate = msg.arg1 == 1; + final MagnificationSpec spec = (MagnificationSpec) msg.obj; + mSpecAnimationBridge.updateSentSpecMainThread(spec, animate); + break; + case MSG_SCREEN_TURNED_OFF: + resetIfNeeded(false); + break; + case MSG_ON_MAGNIFIED_BOUNDS_CHANGED: { + final SomeArgs args = (SomeArgs) msg.obj; + final Region magnifiedBounds = (Region) args.arg1; + onMagnificationRegionChanged(magnifiedBounds); + magnifiedBounds.recycle(); + args.recycle(); + } break; + case MSG_ON_RECTANGLE_ON_SCREEN_REQUESTED: { + final SomeArgs args = (SomeArgs) msg.obj; + final int left = args.argi1; + final int top = args.argi2; + final int right = args.argi3; + final int bottom = args.argi4; + requestRectangleOnScreen(left, top, right, bottom); + args.recycle(); + } break; + case MSG_ON_USER_CONTEXT_CHANGED: + resetIfNeeded(true); + break; + } + return true; } /** * Class responsible for animating spec on the main thread and sending spec * updates to the window manager. */ - private static class SpecAnimationBridge { - private static final int ACTION_UPDATE_SPEC = 1; - - private final Handler mHandler; + private static class SpecAnimationBridge implements ValueAnimator.AnimatorUpdateListener { private final WindowManagerInternal mWindowManager; /** @@ -735,34 +797,33 @@ class MagnificationController { */ 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 that updates the sent spec. This should only be accessed - * and modified on the main (e.g. animation) thread. + * The animator should only be accessed and modified on the main (e.g. animation) thread. */ - private final ValueAnimator mTransformationAnimator; + private final ValueAnimator mValueAnimator; - private final long mMainThreadId; private final Object mLock; @GuardedBy("mLock") private boolean mEnabled = false; - private SpecAnimationBridge(Context context, Object lock) { + private SpecAnimationBridge(Context context, Object lock, WindowManagerInternal wm, + ValueAnimator animator) { mLock = lock; - final Looper mainLooper = context.getMainLooper(); - mMainThreadId = mainLooper.getThread().getId(); - - mHandler = new UpdateHandler(context); - mWindowManager = LocalServices.getService(WindowManagerInternal.class); - - final MagnificationSpecProperty property = new MagnificationSpecProperty(); - final MagnificationSpecEvaluator evaluator = new MagnificationSpecEvaluator(); + mWindowManager = wm; final long animationDuration = context.getResources().getInteger( R.integer.config_longAnimTime); - mTransformationAnimator = ObjectAnimator.ofObject(this, property, evaluator, - mSentMagnificationSpec); - mTransformationAnimator.setDuration(animationDuration); - mTransformationAnimator.setInterpolator(new DecelerateInterpolator(2.5f)); + mValueAnimator = animator; + mValueAnimator.setDuration(animationDuration); + mValueAnimator.setInterpolator(new DecelerateInterpolator(2.5f)); + mValueAnimator.setFloatValues(0.0f, 1.0f); + mValueAnimator.addUpdateListener(this); } /** @@ -781,22 +842,9 @@ class MagnificationController { } } - public void updateSentSpec(MagnificationSpec spec, boolean animate) { - if (Thread.currentThread().getId() == mMainThreadId) { - // Already on the main thread, don't bother proxying. - updateSentSpecInternal(spec, animate); - } else { - mHandler.obtainMessage(ACTION_UPDATE_SPEC, - animate ? 1 : 0, 0, spec).sendToTarget(); - } - } - - /** - * Updates the sent spec. - */ - private void updateSentSpecInternal(MagnificationSpec spec, boolean animate) { - if (mTransformationAnimator.isRunning()) { - mTransformationAnimator.cancel(); + public void updateSentSpecMainThread(MagnificationSpec spec, boolean animate) { + if (mValueAnimator.isRunning()) { + mValueAnimator.cancel(); } // If the current and sent specs don't match, update the sent spec. @@ -812,11 +860,6 @@ class MagnificationController { } } - private void animateMagnificationSpecLocked(MagnificationSpec toSpec) { - mTransformationAnimator.setObjectValues(mSentMagnificationSpec, toSpec); - mTransformationAnimator.start(); - } - private void setMagnificationSpecLocked(MagnificationSpec spec) { if (mEnabled) { if (DEBUG_SET_MAGNIFICATION_SPEC) { @@ -828,71 +871,40 @@ class MagnificationController { } } - private class UpdateHandler extends Handler { - public UpdateHandler(Context context) { - super(context.getMainLooper()); - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case ACTION_UPDATE_SPEC: - final boolean animate = msg.arg1 == 1; - final MagnificationSpec spec = (MagnificationSpec) msg.obj; - updateSentSpecInternal(spec, animate); - break; - } - } + private void animateMagnificationSpecLocked(MagnificationSpec toSpec) { + mEndMagnificationSpec.setTo(toSpec); + mStartMagnificationSpec.setTo(mSentMagnificationSpec); + mValueAnimator.start(); } - private static class MagnificationSpecProperty - extends Property<SpecAnimationBridge, MagnificationSpec> { - public MagnificationSpecProperty() { - super(MagnificationSpec.class, "spec"); - } - - @Override - public MagnificationSpec get(SpecAnimationBridge object) { - synchronized (object.mLock) { - return object.mSentMagnificationSpec; - } - } - - @Override - public void set(SpecAnimationBridge object, MagnificationSpec value) { - synchronized (object.mLock) { - object.setMagnificationSpecLocked(value); + @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 MagnificationSpecEvaluator - implements TypeEvaluator<MagnificationSpec> { - private final MagnificationSpec mTempSpec = MagnificationSpec.obtain(); - - @Override - public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec, - MagnificationSpec toSpec) { - final MagnificationSpec result = mTempSpec; - result.scale = fromSpec.scale + (toSpec.scale - fromSpec.scale) * fraction; - result.offsetX = fromSpec.offsetX + (toSpec.offsetX - fromSpec.offsetX) * fraction; - result.offsetY = fromSpec.offsetY + (toSpec.offsetY - fromSpec.offsetY) * fraction; - return result; - } - } } private static class ScreenStateObserver extends BroadcastReceiver { - private static final int MESSAGE_ON_SCREEN_STATE_CHANGE = 1; - private final Context mContext; private final MagnificationController mController; - private final Handler mHandler; public ScreenStateObserver(Context context, MagnificationController controller) { mContext = context; mController = controller; - mHandler = new StateChangeHandler(context); } public void register() { @@ -905,151 +917,27 @@ class MagnificationController { @Override public void onReceive(Context context, Intent intent) { - mHandler.obtainMessage(MESSAGE_ON_SCREEN_STATE_CHANGE, - intent.getAction()).sendToTarget(); - } - - private void handleOnScreenStateChange() { - mController.resetIfNeeded(false); - } - - private class StateChangeHandler extends Handler { - public StateChangeHandler(Context context) { - super(context.getMainLooper()); - } - - @Override - public void handleMessage(Message message) { - switch (message.what) { - case MESSAGE_ON_SCREEN_STATE_CHANGE: - handleOnScreenStateChange(); - break; - } - } + mController.onScreenTurnedOff(); } } - /** - * This class handles the screen magnification when accessibility is enabled. - */ - private static class WindowStateObserver - implements WindowManagerInternal.MagnificationCallbacks { - private static final int MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED = 1; - private static final int MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED = 2; - private static final int MESSAGE_ON_USER_CONTEXT_CHANGED = 3; - private static final int MESSAGE_ON_ROTATION_CHANGED = 4; - - private final MagnificationController mController; - private final WindowManagerInternal mWindowManager; - private final Handler mHandler; - - private boolean mSpecIsDirty; - - public WindowStateObserver(Context context, MagnificationController controller) { - mController = controller; - mWindowManager = LocalServices.getService(WindowManagerInternal.class); - mHandler = new CallbackHandler(context); - } - - public void register() { - mWindowManager.setMagnificationCallbacks(this); - } - - public void unregister() { - mWindowManager.setMagnificationCallbacks(null); - } - - @Override - public void onMagnificationRegionChanged(Region magnificationRegion) { - final SomeArgs args = SomeArgs.obtain(); - args.arg1 = Region.obtain(magnificationRegion); - mHandler.obtainMessage(MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED, args).sendToTarget(); - } - - private void handleOnMagnifiedBoundsChanged(Region magnificationRegion) { - mController.onMagnificationRegionChanged(magnificationRegion, mSpecIsDirty); - mSpecIsDirty = false; - } - - @Override - public void onRectangleOnScreenRequested(int left, int top, int right, int bottom) { - final SomeArgs args = SomeArgs.obtain(); - args.argi1 = left; - args.argi2 = top; - args.argi3 = right; - args.argi4 = bottom; - mHandler.obtainMessage(MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED, args).sendToTarget(); - } + // Extra class to get settings so tests can mock it + public static class SettingsBridge { + private final ContentResolver mContentResolver; - private void handleOnRectangleOnScreenRequested(int left, int top, int right, int bottom) { - mController.requestRectangleOnScreen(left, top, right, bottom); + public SettingsBridge(ContentResolver contentResolver) { + mContentResolver = contentResolver; } - @Override - public void onRotationChanged(int rotation) { - mHandler.obtainMessage(MESSAGE_ON_ROTATION_CHANGED, rotation, 0).sendToTarget(); - } - - private void handleOnRotationChanged() { - // If there was a rotation and magnification is still enabled, - // we'll need to rewrite the spec to reflect the new screen - // configuration. Conveniently, we'll receive a callback from - // the window manager with updated bounds for the magnified - // region. - mSpecIsDirty = !mController.resetIfNeeded(true); + public void putMagnificationScale(float value, int userId) { + Settings.Secure.putFloatForUser(mContentResolver, + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, value, userId); } - @Override - public void onUserContextChanged() { - mHandler.sendEmptyMessage(MESSAGE_ON_USER_CONTEXT_CHANGED); - } - - private void handleOnUserContextChanged() { - mController.resetIfNeeded(true); - } - - /** - * This method is used to get the magnification region in the tiny time slice between - * registering the callbacks and handling the message. - * TODO: Elimiante this extra path, perhaps by processing the message immediately - * - * @param outMagnificationRegion - */ - public void getMagnificationRegion(@NonNull Region outMagnificationRegion) { - mWindowManager.getMagnificationRegion(outMagnificationRegion); - } - - private class CallbackHandler extends Handler { - public CallbackHandler(Context context) { - super(context.getMainLooper()); - } - - @Override - public void handleMessage(Message message) { - switch (message.what) { - case MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED: { - final SomeArgs args = (SomeArgs) message.obj; - final Region magnifiedBounds = (Region) args.arg1; - handleOnMagnifiedBoundsChanged(magnifiedBounds); - magnifiedBounds.recycle(); - } break; - case MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED: { - final SomeArgs args = (SomeArgs) message.obj; - final int left = args.argi1; - final int top = args.argi2; - final int right = args.argi3; - final int bottom = args.argi4; - handleOnRectangleOnScreenRequested(left, top, right, bottom); - args.recycle(); - } break; - case MESSAGE_ON_USER_CONTEXT_CHANGED: { - handleOnUserContextChanged(); - } break; - case MESSAGE_ON_ROTATION_CHANGED: { - handleOnRotationChanged(); - } break; - } - } + public float getMagnificationScale(int userId) { + return Settings.Secure.getFloatForUser(mContentResolver, + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, + DEFAULT_MAGNIFICATION_SCALE, userId); } } } diff --git a/services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java index 39bc8098f6a5..f6e5340ed7ee 100644 --- a/services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/MagnificationGestureHandler.java @@ -381,7 +381,7 @@ class MagnificationGestureHandler implements EventStreamTransformation { Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX + " scrollY: " + distanceY); } - mMagnificationController.offsetMagnifiedRegionCenter(distanceX, distanceY, + mMagnificationController.offsetMagnifiedRegion(distanceX, distanceY, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); return true; } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/MagnificationControllerTest.java new file mode 100644 index 000000000000..cb5e8bbb9863 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/MagnificationControllerTest.java @@ -0,0 +1,827 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.animation.ValueAnimator; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.Region; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.support.test.runner.AndroidJUnit4; +import android.view.MagnificationSpec; +import android.view.WindowManagerInternal; +import android.view.WindowManagerInternal.MagnificationCallbacks; + +import com.android.internal.R; +import org.hamcrest.CoreMatchers; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.Locale; + +@RunWith(AndroidJUnit4.class) +public class MagnificationControllerTest { + static final Rect INITIAL_MAGNIFICATION_BOUNDS = new Rect(0, 0, 100, 200); + static final PointF INITIAL_MAGNIFICATION_BOUNDS_CENTER = new PointF( + INITIAL_MAGNIFICATION_BOUNDS.centerX(), INITIAL_MAGNIFICATION_BOUNDS.centerY()); + static final PointF INITIAL_BOUNDS_UPPER_LEFT_2X_CENTER = new PointF(25, 50); + static final PointF INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER = new PointF(75, 150); + static final Rect OTHER_MAGNIFICATION_BOUNDS = new Rect(100, 200, 500, 600); + static final PointF OTHER_BOUNDS_LOWER_RIGHT_2X_CENTER = new PointF(400, 500); + static final Region INITIAL_MAGNIFICATION_REGION = new Region(INITIAL_MAGNIFICATION_BOUNDS); + static final Region OTHER_REGION = new Region(OTHER_MAGNIFICATION_BOUNDS); + static final int SERVICE_ID_1 = 1; + static final int SERVICE_ID_2 = 2; + + final Context mMockContext = mock(Context.class); + final AccessibilityManagerService mMockAms = mock(AccessibilityManagerService.class); + final WindowManagerInternal mMockWindowManager = mock(WindowManagerInternal.class); + final MessageCapturingHandler mMessageCapturingHandler = + new MessageCapturingHandler(new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + return mMagnificationController.handleMessage(msg); + } + }); + final ArgumentCaptor<MagnificationSpec> mMagnificationSpecCaptor = + ArgumentCaptor.forClass(MagnificationSpec.class); + final ValueAnimator mMockValueAnimator = mock(ValueAnimator.class); + MagnificationController.SettingsBridge mMockSettingsBridge; + + + MagnificationController mMagnificationController; + ValueAnimator.AnimatorUpdateListener mTargetAnimationListener; + + @BeforeClass + public static void oneTimeInitialization() { + if (Looper.myLooper() == null) { + Looper.prepare(); + } + } + + @Before + public void setUp() { + when(mMockContext.getMainLooper()).thenReturn(Looper.myLooper()); + Resources mockResources = mock(Resources.class); + when(mMockContext.getResources()).thenReturn(mockResources); + when(mockResources.getInteger(R.integer.config_longAnimTime)) + .thenReturn(1000); + mMockSettingsBridge = mock(MagnificationController.SettingsBridge.class); + mMagnificationController = new MagnificationController(mMockContext, mMockAms, new Object(), + mMessageCapturingHandler, mMockWindowManager, mMockValueAnimator, + mMockSettingsBridge); + + doAnswer(new Answer<Void>() { + @Override + public Void answer(InvocationOnMock invocationOnMock) throws Throwable { + Object[] args = invocationOnMock.getArguments(); + Region regionArg = (Region) args[0]; + regionArg.set(INITIAL_MAGNIFICATION_REGION); + return null; + } + }).when(mMockWindowManager).getMagnificationRegion((Region) anyObject()); + + ArgumentCaptor<ValueAnimator.AnimatorUpdateListener> listenerArgumentCaptor = + ArgumentCaptor.forClass(ValueAnimator.AnimatorUpdateListener.class); + verify(mMockValueAnimator).addUpdateListener(listenerArgumentCaptor.capture()); + mTargetAnimationListener = listenerArgumentCaptor.getValue(); + Mockito.reset(mMockValueAnimator); // Ignore other initialization + } + + @Test + public void testRegister_WindowManagerAndContextRegisterListeners() { + mMagnificationController.register(); + verify(mMockContext).registerReceiver( + (BroadcastReceiver) anyObject(), (IntentFilter) anyObject()); + verify(mMockWindowManager).setMagnificationCallbacks((MagnificationCallbacks) anyObject()); + assertTrue(mMagnificationController.isRegisteredLocked()); + } + + @Test + public void testRegister_WindowManagerAndContextUnregisterListeners() { + mMagnificationController.register(); + mMagnificationController.unregister(); + + verify(mMockContext).unregisterReceiver((BroadcastReceiver) anyObject()); + verify(mMockWindowManager).setMagnificationCallbacks(null); + assertFalse(mMagnificationController.isRegisteredLocked()); + } + + @Test + public void testInitialState_noMagnificationAndMagnificationRegionReadFromWindowManager() { + mMagnificationController.register(); + MagnificationSpec expectedInitialSpec = getMagnificationSpec(1.0f, 0.0f, 0.0f); + Region initialMagRegion = new Region(); + Rect initialBounds = new Rect(); + + assertEquals(expectedInitialSpec, getCurrentMagnificationSpec()); + mMagnificationController.getMagnificationRegion(initialMagRegion); + mMagnificationController.getMagnificationBounds(initialBounds); + assertEquals(INITIAL_MAGNIFICATION_REGION, initialMagRegion); + assertEquals(INITIAL_MAGNIFICATION_BOUNDS, initialBounds); + assertEquals(INITIAL_MAGNIFICATION_BOUNDS.centerX(), + mMagnificationController.getCenterX(), 0.0f); + assertEquals(INITIAL_MAGNIFICATION_BOUNDS.centerY(), + mMagnificationController.getCenterY(), 0.0f); + } + + @Test + public void testNotRegistered_publicMethodsShouldBeBenign() { + assertFalse(mMagnificationController.isMagnifying()); + assertFalse(mMagnificationController.magnificationRegionContains(100, 100)); + assertFalse(mMagnificationController.reset(true)); + assertFalse(mMagnificationController.setScale(2, 100, 100, true, 0)); + assertFalse(mMagnificationController.setCenter(100, 100, false, 1)); + assertFalse(mMagnificationController.setScaleAndCenter(1.5f, 100, 100, false, 2)); + assertTrue(mMagnificationController.getIdOfLastServiceToMagnify() < 0); + + mMagnificationController.getMagnificationRegion(new Region()); + mMagnificationController.getMagnificationBounds(new Rect()); + mMagnificationController.getScale(); + mMagnificationController.getOffsetX(); + mMagnificationController.getOffsetY(); + mMagnificationController.getCenterX(); + mMagnificationController.getCenterY(); + mMagnificationController.offsetMagnifiedRegion(50, 50, 1); + mMagnificationController.unregister(); + } + + @Test + public void testSetScale_noAnimation_shouldGoStraightToWindowManagerAndUpdateState() { + mMagnificationController.register(); + final float scale = 2.0f; + final PointF center = INITIAL_MAGNIFICATION_BOUNDS_CENTER; + final PointF offsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, center, scale); + assertTrue(mMagnificationController + .setScale(scale, center.x, center.y, false, SERVICE_ID_1)); + + final MagnificationSpec expectedSpec = getMagnificationSpec(scale, offsets); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(expectedSpec))); + assertThat(getCurrentMagnificationSpec(), closeTo(expectedSpec)); + assertEquals(center.x, mMagnificationController.getCenterX(), 0.0); + assertEquals(center.y, mMagnificationController.getCenterY(), 0.0); + verify(mMockValueAnimator, times(0)).start(); + } + + @Test + public void testSetScale_withPivotAndAnimation_stateChangesAndAnimationHappens() { + mMagnificationController.register(); + MagnificationSpec startSpec = getCurrentMagnificationSpec(); + float scale = 2.0f; + PointF pivotPoint = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER; + assertTrue(mMagnificationController + .setScale(scale, pivotPoint.x, pivotPoint.y, true, SERVICE_ID_1)); + + // New center should be halfway between original center and pivot + PointF newCenter = new PointF((pivotPoint.x + INITIAL_MAGNIFICATION_BOUNDS.centerX()) / 2, + (pivotPoint.y + INITIAL_MAGNIFICATION_BOUNDS.centerY()) / 2); + PointF offsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale); + MagnificationSpec endSpec = getMagnificationSpec(scale, offsets); + + assertEquals(newCenter.x, mMagnificationController.getCenterX(), 0.5); + assertEquals(newCenter.y, mMagnificationController.getCenterY(), 0.5); + assertThat(getCurrentMagnificationSpec(), closeTo(endSpec)); + verify(mMockValueAnimator).start(); + + // Initial value + when(mMockValueAnimator.getAnimatedFraction()).thenReturn(0.0f); + mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); + verify(mMockWindowManager).setMagnificationSpec(startSpec); + + // Intermediate point + Mockito.reset(mMockWindowManager); + float fraction = 0.5f; + when(mMockValueAnimator.getAnimatedFraction()).thenReturn(fraction); + mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); + verify(mMockWindowManager).setMagnificationSpec( + argThat(closeTo(getInterpolatedMagSpec(startSpec, endSpec, fraction)))); + + // Final value + Mockito.reset(mMockWindowManager); + when(mMockValueAnimator.getAnimatedFraction()).thenReturn(1.0f); + mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(endSpec))); + } + + @Test + public void testSetCenter_whileMagnifying_noAnimation_centerMoves() { + mMagnificationController.register(); + // First zoom in + float scale = 2.0f; + assertTrue(mMagnificationController.setScale(scale, + INITIAL_MAGNIFICATION_BOUNDS.centerX(), INITIAL_MAGNIFICATION_BOUNDS.centerY(), + false, SERVICE_ID_1)); + Mockito.reset(mMockWindowManager); + + PointF newCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER; + assertTrue(mMagnificationController + .setCenter(newCenter.x, newCenter.y, false, SERVICE_ID_1)); + PointF expectedOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale); + MagnificationSpec expectedSpec = getMagnificationSpec(scale, expectedOffsets); + + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(expectedSpec))); + assertEquals(newCenter.x, mMagnificationController.getCenterX(), 0.0); + assertEquals(newCenter.y, mMagnificationController.getCenterY(), 0.0); + verify(mMockValueAnimator, times(0)).start(); + } + + @Test + public void testSetScaleAndCenter_animated_stateChangesAndAnimationHappens() { + mMagnificationController.register(); + MagnificationSpec startSpec = getCurrentMagnificationSpec(); + float scale = 2.5f; + PointF newCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER; + PointF offsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale); + MagnificationSpec endSpec = getMagnificationSpec(scale, offsets); + + assertTrue(mMagnificationController.setScaleAndCenter(scale, newCenter.x, newCenter.y, + true, SERVICE_ID_1)); + + assertEquals(newCenter.x, mMagnificationController.getCenterX(), 0.5); + assertEquals(newCenter.y, mMagnificationController.getCenterY(), 0.5); + assertThat(getCurrentMagnificationSpec(), closeTo(endSpec)); + verify(mMockAms).notifyMagnificationChanged( + INITIAL_MAGNIFICATION_REGION, scale, newCenter.x, newCenter.y); + verify(mMockValueAnimator).start(); + + // Initial value + when(mMockValueAnimator.getAnimatedFraction()).thenReturn(0.0f); + mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); + verify(mMockWindowManager).setMagnificationSpec(startSpec); + + // Intermediate point + Mockito.reset(mMockWindowManager); + float fraction = 0.33f; + when(mMockValueAnimator.getAnimatedFraction()).thenReturn(fraction); + mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); + verify(mMockWindowManager).setMagnificationSpec( + argThat(closeTo(getInterpolatedMagSpec(startSpec, endSpec, fraction)))); + + // Final value + Mockito.reset(mMockWindowManager); + when(mMockValueAnimator.getAnimatedFraction()).thenReturn(1.0f); + mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(endSpec))); + } + + @Test + public void testSetScaleAndCenter_scaleOutOfBounds_cappedAtLimits() { + mMagnificationController.register(); + MagnificationSpec startSpec = getCurrentMagnificationSpec(); + PointF newCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER; + PointF offsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, + MagnificationController.MAX_SCALE); + MagnificationSpec endSpec = getMagnificationSpec( + MagnificationController.MAX_SCALE, offsets); + + assertTrue(mMagnificationController.setScaleAndCenter( + MagnificationController.MAX_SCALE + 1.0f, + newCenter.x, newCenter.y, false, SERVICE_ID_1)); + + assertEquals(newCenter.x, mMagnificationController.getCenterX(), 0.5); + assertEquals(newCenter.y, mMagnificationController.getCenterY(), 0.5); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(endSpec))); + Mockito.reset(mMockWindowManager); + + // Verify that we can't zoom below 1x + assertTrue(mMagnificationController.setScaleAndCenter(0.5f, + INITIAL_MAGNIFICATION_BOUNDS_CENTER.x, INITIAL_MAGNIFICATION_BOUNDS_CENTER.y, + false, SERVICE_ID_1)); + + assertEquals(INITIAL_MAGNIFICATION_BOUNDS_CENTER.x, + mMagnificationController.getCenterX(), 0.5); + assertEquals(INITIAL_MAGNIFICATION_BOUNDS_CENTER.y, + mMagnificationController.getCenterY(), 0.5); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(startSpec))); + } + + @Test + public void testSetScaleAndCenter_centerOutOfBounds_cappedAtLimits() { + mMagnificationController.register(); + float scale = 2.0f; + + // Off the edge to the top and left + assertTrue(mMagnificationController.setScaleAndCenter( + scale, -100f, -200f, false, SERVICE_ID_1)); + + PointF newCenter = INITIAL_BOUNDS_UPPER_LEFT_2X_CENTER; + PointF newOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale); + assertEquals(newCenter.x, mMagnificationController.getCenterX(), 0.5); + assertEquals(newCenter.y, mMagnificationController.getCenterY(), 0.5); + verify(mMockWindowManager).setMagnificationSpec( + argThat(closeTo(getMagnificationSpec(scale, newOffsets)))); + Mockito.reset(mMockWindowManager); + + // Off the edge to the bottom and right + assertTrue(mMagnificationController.setScaleAndCenter(scale, + INITIAL_MAGNIFICATION_BOUNDS.right + 1, INITIAL_MAGNIFICATION_BOUNDS.bottom + 1, + false, SERVICE_ID_1)); + newCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER; + newOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale); + assertEquals(newCenter.x, mMagnificationController.getCenterX(), 0.5); + assertEquals(newCenter.y, mMagnificationController.getCenterY(), 0.5); + verify(mMockWindowManager).setMagnificationSpec( + argThat(closeTo(getMagnificationSpec(scale, newOffsets)))); + } + + @Test + public void testMagnificationRegionChanged_serviceNotified() { + mMagnificationController.register(); + MagnificationCallbacks callbacks = getMagnificationCallbacks(); + callbacks.onMagnificationRegionChanged(OTHER_REGION); + mMessageCapturingHandler.sendAllMessages(); + verify(mMockAms).notifyMagnificationChanged(OTHER_REGION, 1.0f, + OTHER_MAGNIFICATION_BOUNDS.centerX(), OTHER_MAGNIFICATION_BOUNDS.centerY()); + } + + @Test + public void testOffsetMagnifiedRegion_whileMagnifying_offsetsMove() { + mMagnificationController.register(); + PointF startCenter = INITIAL_MAGNIFICATION_BOUNDS_CENTER; + float scale = 2.0f; + PointF startOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, startCenter, scale); + // First zoom in + assertTrue(mMagnificationController + .setScaleAndCenter(scale, startCenter.x, startCenter.y, false, SERVICE_ID_1)); + Mockito.reset(mMockWindowManager); + + PointF newCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER; + PointF newOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale); + mMagnificationController.offsetMagnifiedRegion( + startOffsets.x - newOffsets.x, startOffsets.y - newOffsets.y, SERVICE_ID_1); + + MagnificationSpec expectedSpec = getMagnificationSpec(scale, newOffsets); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(expectedSpec))); + assertEquals(newCenter.x, mMagnificationController.getCenterX(), 0.0); + assertEquals(newCenter.y, mMagnificationController.getCenterY(), 0.0); + verify(mMockValueAnimator, times(0)).start(); + } + + @Test + public void testOffsetMagnifiedRegion_whileNotMagnifying_hasNoEffect() { + mMagnificationController.register(); + Mockito.reset(mMockWindowManager); + MagnificationSpec startSpec = getCurrentMagnificationSpec(); + mMagnificationController.offsetMagnifiedRegion(10, 10, SERVICE_ID_1); + assertThat(getCurrentMagnificationSpec(), closeTo(startSpec)); + mMagnificationController.offsetMagnifiedRegion(-10, -10, SERVICE_ID_1); + assertThat(getCurrentMagnificationSpec(), closeTo(startSpec)); + verifyNoMoreInteractions(mMockWindowManager); + } + + @Test + public void testOffsetMagnifiedRegion_whileMagnifyingButAtEdge_hasNoEffect() { + mMagnificationController.register(); + float scale = 2.0f; + + // Upper left edges + PointF ulCenter = INITIAL_BOUNDS_UPPER_LEFT_2X_CENTER; + assertTrue(mMagnificationController + .setScaleAndCenter(scale, ulCenter.x, ulCenter.y, false, SERVICE_ID_1)); + Mockito.reset(mMockWindowManager); + MagnificationSpec ulSpec = getCurrentMagnificationSpec(); + mMagnificationController.offsetMagnifiedRegion(-10, -10, SERVICE_ID_1); + assertThat(getCurrentMagnificationSpec(), closeTo(ulSpec)); + verifyNoMoreInteractions(mMockWindowManager); + + // Lower right edges + PointF lrCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER; + assertTrue(mMagnificationController + .setScaleAndCenter(scale, lrCenter.x, lrCenter.y, false, SERVICE_ID_1)); + Mockito.reset(mMockWindowManager); + MagnificationSpec lrSpec = getCurrentMagnificationSpec(); + mMagnificationController.offsetMagnifiedRegion(10, 10, SERVICE_ID_1); + assertThat(getCurrentMagnificationSpec(), closeTo(lrSpec)); + verifyNoMoreInteractions(mMockWindowManager); + } + + @Test + public void testGetIdOfLastServiceToChange_returnsCorrectValue() { + mMagnificationController.register(); + PointF startCenter = INITIAL_MAGNIFICATION_BOUNDS_CENTER; + assertTrue(mMagnificationController + .setScale(2.0f, startCenter.x, startCenter.y, false, SERVICE_ID_1)); + assertEquals(SERVICE_ID_1, mMagnificationController.getIdOfLastServiceToMagnify()); + assertTrue(mMagnificationController + .setScale(1.5f, startCenter.x, startCenter.y, false, SERVICE_ID_2)); + assertEquals(SERVICE_ID_2, mMagnificationController.getIdOfLastServiceToMagnify()); + } + + @Test + public void testSetUserId_resetsOnlyIfIdChanges() { + final int userId1 = 1; + final int userId2 = 2; + + mMagnificationController.register(); + mMagnificationController.setUserId(userId1); + PointF startCenter = INITIAL_MAGNIFICATION_BOUNDS_CENTER; + float scale = 2.0f; + mMagnificationController.setScale(scale, startCenter.x, startCenter.y, false, SERVICE_ID_1); + + mMagnificationController.setUserId(userId1); + assertTrue(mMagnificationController.isMagnifying()); + mMagnificationController.setUserId(userId2); + assertFalse(mMagnificationController.isMagnifying()); + } + + @Test + public void testResetIfNeeded_doesWhatItSays() { + mMagnificationController.register(); + zoomIn2xToMiddle(); + assertTrue(mMagnificationController.resetIfNeeded(false)); + verify(mMockAms).notifyMagnificationChanged( + eq(INITIAL_MAGNIFICATION_REGION), eq(1.0f), anyInt(), anyInt()); + assertFalse(mMagnificationController.isMagnifying()); + assertFalse(mMagnificationController.resetIfNeeded(false)); + } + + @Test + public void testTurnScreenOff_resetsMagnification() { + mMagnificationController.register(); + ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + verify(mMockContext).registerReceiver( + broadcastReceiverCaptor.capture(), (IntentFilter) anyObject()); + BroadcastReceiver br = broadcastReceiverCaptor.getValue(); + zoomIn2xToMiddle(); + br.onReceive(mMockContext, null); + mMessageCapturingHandler.sendAllMessages(); + assertFalse(mMagnificationController.isMagnifying()); + } + + @Test + public void testUserContextChange_resetsMagnification() { + mMagnificationController.register(); + MagnificationCallbacks callbacks = getMagnificationCallbacks(); + zoomIn2xToMiddle(); + callbacks.onUserContextChanged(); + mMessageCapturingHandler.sendAllMessages(); + assertFalse(mMagnificationController.isMagnifying()); + } + + @Test + public void testRotation_resetsMagnification() { + mMagnificationController.register(); + MagnificationCallbacks callbacks = getMagnificationCallbacks(); + zoomIn2xToMiddle(); + mMessageCapturingHandler.sendAllMessages(); + assertTrue(mMagnificationController.isMagnifying()); + callbacks.onRotationChanged(0); + mMessageCapturingHandler.sendAllMessages(); + assertFalse(mMagnificationController.isMagnifying()); + } + + @Test + public void testBoundsChange_whileMagnifyingWithCompatibleSpec_noSpecChange() { + // Going from a small region to a large one leads to no issues + mMagnificationController.register(); + zoomIn2xToMiddle(); + MagnificationSpec startSpec = getCurrentMagnificationSpec(); + MagnificationCallbacks callbacks = getMagnificationCallbacks(); + Mockito.reset(mMockWindowManager); + callbacks.onMagnificationRegionChanged(OTHER_REGION); + mMessageCapturingHandler.sendAllMessages(); + assertThat(getCurrentMagnificationSpec(), closeTo(startSpec)); + verifyNoMoreInteractions(mMockWindowManager); + } + + @Test + public void testBoundsChange_whileZoomingWithCompatibleSpec_noSpecChange() { + mMagnificationController.register(); + PointF startCenter = INITIAL_MAGNIFICATION_BOUNDS_CENTER; + float scale = 2.0f; + mMagnificationController.setScale(scale, startCenter.x, startCenter.y, true, SERVICE_ID_1); + MagnificationSpec startSpec = getCurrentMagnificationSpec(); + MagnificationCallbacks callbacks = getMagnificationCallbacks(); + Mockito.reset(mMockWindowManager); + callbacks.onMagnificationRegionChanged(OTHER_REGION); + mMessageCapturingHandler.sendAllMessages(); + assertThat(getCurrentMagnificationSpec(), closeTo(startSpec)); + verifyNoMoreInteractions(mMockWindowManager); + } + + @Test + public void testBoundsChange_whileMagnifyingWithIncompatibleSpec_offsetsConstrained() { + // In a large region, pan to the farthest point possible + mMagnificationController.register(); + MagnificationCallbacks callbacks = getMagnificationCallbacks(); + callbacks.onMagnificationRegionChanged(OTHER_REGION); + mMessageCapturingHandler.sendAllMessages(); + PointF startCenter = OTHER_BOUNDS_LOWER_RIGHT_2X_CENTER; + float scale = 2.0f; + mMagnificationController.setScale(scale, startCenter.x, startCenter.y, false, SERVICE_ID_1); + MagnificationSpec startSpec = getCurrentMagnificationSpec(); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(startSpec))); + Mockito.reset(mMockWindowManager); + + callbacks.onMagnificationRegionChanged(INITIAL_MAGNIFICATION_REGION); + mMessageCapturingHandler.sendAllMessages(); + + MagnificationSpec endSpec = getCurrentMagnificationSpec(); + assertThat(endSpec, CoreMatchers.not(closeTo(startSpec))); + PointF expectedOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, + INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER, scale); + assertThat(endSpec, closeTo(getMagnificationSpec(scale, expectedOffsets))); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(endSpec))); + } + + @Test + public void testBoundsChange_whileZoomingWithIncompatibleSpec_jumpsToCompatibleSpec() { + mMagnificationController.register(); + MagnificationCallbacks callbacks = getMagnificationCallbacks(); + callbacks.onMagnificationRegionChanged(OTHER_REGION); + mMessageCapturingHandler.sendAllMessages(); + PointF startCenter = OTHER_BOUNDS_LOWER_RIGHT_2X_CENTER; + float scale = 2.0f; + mMagnificationController.setScale(scale, startCenter.x, startCenter.y, true, SERVICE_ID_1); + MagnificationSpec startSpec = getCurrentMagnificationSpec(); + when (mMockValueAnimator.isRunning()).thenReturn(true); + + callbacks.onMagnificationRegionChanged(INITIAL_MAGNIFICATION_REGION); + mMessageCapturingHandler.sendAllMessages(); + verify(mMockValueAnimator).cancel(); + + MagnificationSpec endSpec = getCurrentMagnificationSpec(); + assertThat(endSpec, CoreMatchers.not(closeTo(startSpec))); + PointF expectedOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, + INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER, scale); + assertThat(endSpec, closeTo(getMagnificationSpec(scale, expectedOffsets))); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(endSpec))); + } + + @Test + public void testRequestRectOnScreen_rectAlreadyOnScreen_doesNothing() { + mMagnificationController.register(); + zoomIn2xToMiddle(); + MagnificationSpec startSpec = getCurrentMagnificationSpec(); + MagnificationCallbacks callbacks = getMagnificationCallbacks(); + Mockito.reset(mMockWindowManager); + int centerX = (int) INITIAL_MAGNIFICATION_BOUNDS_CENTER.x; + int centerY = (int) INITIAL_MAGNIFICATION_BOUNDS_CENTER.y; + callbacks.onRectangleOnScreenRequested(centerX - 1, centerY - 1, centerX + 1, centerY - 1); + mMessageCapturingHandler.sendAllMessages(); + assertThat(getCurrentMagnificationSpec(), closeTo(startSpec)); + verifyNoMoreInteractions(mMockWindowManager); + } + + @Test + public void testRequestRectOnScreen_rectCanFitOnScreen_pansToGetRectOnScreen() { + mMagnificationController.register(); + zoomIn2xToMiddle(); + MagnificationCallbacks callbacks = getMagnificationCallbacks(); + Mockito.reset(mMockWindowManager); + callbacks.onRectangleOnScreenRequested(0, 0, 1, 1); + mMessageCapturingHandler.sendAllMessages(); + MagnificationSpec expectedEndSpec = getMagnificationSpec(2.0f, 0, 0); + assertThat(getCurrentMagnificationSpec(), closeTo(expectedEndSpec)); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(expectedEndSpec))); + } + + @Test + public void testRequestRectOnScreen_garbageInput_doesNothing() { + mMagnificationController.register(); + zoomIn2xToMiddle(); + MagnificationSpec startSpec = getCurrentMagnificationSpec(); + MagnificationCallbacks callbacks = getMagnificationCallbacks(); + Mockito.reset(mMockWindowManager); + callbacks.onRectangleOnScreenRequested(0, 0, -50, -50); + mMessageCapturingHandler.sendAllMessages(); + assertThat(getCurrentMagnificationSpec(), closeTo(startSpec)); + verifyNoMoreInteractions(mMockWindowManager); + } + + + @Test + public void testRequestRectOnScreen_rectTooWide_pansToGetStartOnScreenBasedOnLocale() { + Locale.setDefault(new Locale("en", "us")); + mMagnificationController.register(); + zoomIn2xToMiddle(); + MagnificationCallbacks callbacks = getMagnificationCallbacks(); + MagnificationSpec startSpec = getCurrentMagnificationSpec(); + Mockito.reset(mMockWindowManager); + Rect wideRect = new Rect(0, 50, 100, 51); + callbacks.onRectangleOnScreenRequested( + wideRect.left, wideRect.top, wideRect.right, wideRect.bottom); + mMessageCapturingHandler.sendAllMessages(); + MagnificationSpec expectedEndSpec = getMagnificationSpec(2.0f, 0, startSpec.offsetY); + assertThat(getCurrentMagnificationSpec(), closeTo(expectedEndSpec)); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(expectedEndSpec))); + Mockito.reset(mMockWindowManager); + + // Repeat with RTL + Locale.setDefault(new Locale("he", "il")); + callbacks.onRectangleOnScreenRequested( + wideRect.left, wideRect.top, wideRect.right, wideRect.bottom); + mMessageCapturingHandler.sendAllMessages(); + expectedEndSpec = getMagnificationSpec(2.0f, -100, startSpec.offsetY); + assertThat(getCurrentMagnificationSpec(), closeTo(expectedEndSpec)); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(expectedEndSpec))); + } + + @Test + public void testRequestRectOnScreen_rectTooTall_pansMinimumToGetTopOnScreen() { + mMagnificationController.register(); + zoomIn2xToMiddle(); + MagnificationCallbacks callbacks = getMagnificationCallbacks(); + MagnificationSpec startSpec = getCurrentMagnificationSpec(); + Mockito.reset(mMockWindowManager); + Rect tallRect = new Rect(50, 0, 51, 100); + callbacks.onRectangleOnScreenRequested( + tallRect.left, tallRect.top, tallRect.right, tallRect.bottom); + mMessageCapturingHandler.sendAllMessages(); + MagnificationSpec expectedEndSpec = getMagnificationSpec(2.0f, startSpec.offsetX, 0); + assertThat(getCurrentMagnificationSpec(), closeTo(expectedEndSpec)); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(expectedEndSpec))); + } + + @Test + public void testChangeMagnification_duringAnimation_animatesToNewValue() { + mMagnificationController.register(); + MagnificationSpec startSpec = getCurrentMagnificationSpec(); + float scale = 2.5f; + PointF firstCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER; + MagnificationSpec firstEndSpec = getMagnificationSpec( + scale, computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, firstCenter, scale)); + + assertTrue(mMagnificationController.setScaleAndCenter(scale, firstCenter.x, firstCenter.y, + true, SERVICE_ID_1)); + + assertEquals(firstCenter.x, mMagnificationController.getCenterX(), 0.5); + assertEquals(firstCenter.y, mMagnificationController.getCenterY(), 0.5); + assertThat(getCurrentMagnificationSpec(), closeTo(firstEndSpec)); + verify(mMockValueAnimator, times(1)).start(); + + // Initial value + when(mMockValueAnimator.getAnimatedFraction()).thenReturn(0.0f); + mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); + verify(mMockWindowManager).setMagnificationSpec(startSpec); + verify(mMockAms).notifyMagnificationChanged( + INITIAL_MAGNIFICATION_REGION, scale, firstCenter.x, firstCenter.y); + Mockito.reset(mMockWindowManager); + + // Intermediate point + float fraction = 0.33f; + when(mMockValueAnimator.getAnimatedFraction()).thenReturn(fraction); + mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); + MagnificationSpec intermediateSpec1 = + getInterpolatedMagSpec(startSpec, firstEndSpec, fraction); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(intermediateSpec1))); + Mockito.reset(mMockWindowManager); + + PointF newCenter = INITIAL_BOUNDS_UPPER_LEFT_2X_CENTER; + MagnificationSpec newEndSpec = getMagnificationSpec( + scale, computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, newCenter, scale)); + assertTrue(mMagnificationController.setCenter( + newCenter.x, newCenter.y, true, SERVICE_ID_1)); + + // Animation should have been restarted + verify(mMockValueAnimator, times(2)).start(); + verify(mMockAms).notifyMagnificationChanged( + INITIAL_MAGNIFICATION_REGION, scale, newCenter.x, newCenter.y); + + // New starting point should be where we left off + when(mMockValueAnimator.getAnimatedFraction()).thenReturn(0.0f); + mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(intermediateSpec1))); + Mockito.reset(mMockWindowManager); + + // Second intermediate point + fraction = 0.5f; + when(mMockValueAnimator.getAnimatedFraction()).thenReturn(fraction); + mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); + verify(mMockWindowManager).setMagnificationSpec( + argThat(closeTo(getInterpolatedMagSpec(intermediateSpec1, newEndSpec, fraction)))); + Mockito.reset(mMockWindowManager); + + // Final value should be the new center + Mockito.reset(mMockWindowManager); + when(mMockValueAnimator.getAnimatedFraction()).thenReturn(1.0f); + mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); + verify(mMockWindowManager).setMagnificationSpec(argThat(closeTo(newEndSpec))); + } + + private void zoomIn2xToMiddle() { + PointF startCenter = INITIAL_MAGNIFICATION_BOUNDS_CENTER; + float scale = 2.0f; + mMagnificationController.setScale(scale, startCenter.x, startCenter.y, false, SERVICE_ID_1); + assertTrue(mMagnificationController.isMagnifying()); + } + + private MagnificationCallbacks getMagnificationCallbacks() { + ArgumentCaptor<MagnificationCallbacks> magnificationCallbacksCaptor = + ArgumentCaptor.forClass(MagnificationCallbacks.class); + verify(mMockWindowManager) + .setMagnificationCallbacks(magnificationCallbacksCaptor.capture()); + return magnificationCallbacksCaptor.getValue(); + } + + private PointF computeOffsets(Rect magnifiedBounds, PointF center, float scale) { + return new PointF( + magnifiedBounds.centerX() - scale * center.x, + magnifiedBounds.centerY() - scale * center.y); + } + + private MagnificationSpec getInterpolatedMagSpec(MagnificationSpec start, MagnificationSpec end, + float fraction) { + MagnificationSpec interpolatedSpec = MagnificationSpec.obtain(); + interpolatedSpec.scale = start.scale + fraction * (end.scale - start.scale); + interpolatedSpec.offsetX = start.offsetX + fraction * (end.offsetX - start.offsetX); + interpolatedSpec.offsetY = start.offsetY + fraction * (end.offsetY - start.offsetY); + return interpolatedSpec; + } + + private MagnificationSpec getMagnificationSpec(float scale, PointF offsets) { + return getMagnificationSpec(scale, offsets.x, offsets.y); + } + + private MagnificationSpec getMagnificationSpec(float scale, float offsetX, float offsetY) { + MagnificationSpec spec = MagnificationSpec.obtain(); + spec.scale = scale; + spec.offsetX = offsetX; + spec.offsetY = offsetY; + return spec; + } + + private MagnificationSpec getCurrentMagnificationSpec() { + return getMagnificationSpec(mMagnificationController.getScale(), + mMagnificationController.getOffsetX(), mMagnificationController.getOffsetY()); + } + + private MagSpecMatcher closeTo(MagnificationSpec spec) { + return new MagSpecMatcher(spec, 0.01f, 0.5f); + } + + private class MagSpecMatcher extends TypeSafeMatcher<MagnificationSpec> { + final MagnificationSpec mMagSpec; + final float mScaleTolerance; + final float mOffsetTolerance; + + MagSpecMatcher(MagnificationSpec spec, float scaleTolerance, float offsetTolerance) { + mMagSpec = spec; + mScaleTolerance = scaleTolerance; + mOffsetTolerance = offsetTolerance; + } + + @Override + protected boolean matchesSafely(MagnificationSpec magnificationSpec) { + if (Math.abs(mMagSpec.scale - magnificationSpec.scale) > mScaleTolerance) { + return false; + } + if (Math.abs(mMagSpec.offsetX - magnificationSpec.offsetX) > mOffsetTolerance) { + return false; + } + if (Math.abs(mMagSpec.offsetY - magnificationSpec.offsetY) > mOffsetTolerance) { + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("Match spec: " + mMagSpec); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MessageCapturingHandler.java b/services/tests/servicestests/src/com/android/server/accessibility/MessageCapturingHandler.java new file mode 100644 index 000000000000..003f7abf6b00 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/MessageCapturingHandler.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility; + +import android.os.Handler; +import android.os.Message; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class to capture messages dispatched through a handler and control when they arrive + * at their target. + */ +public class MessageCapturingHandler extends Handler { + List<Pair<Message, Long>> timedMessages = new ArrayList<>(); + + Handler.Callback mCallback; + + public MessageCapturingHandler(Handler.Callback callback) { + mCallback = callback; + } + + @Override + public boolean sendMessageAtTime(Message message, long uptimeMillis) { + timedMessages.add(new Pair<>(Message.obtain(message), uptimeMillis)); + return super.sendMessageAtTime(message, uptimeMillis); + } + + public void sendOneMessage() { + Message message = timedMessages.remove(0).first; + removeMessages(message.what, message.obj); + mCallback.handleMessage(message); + removeStaleMessages(); + } + + public void sendAllMessages() { + while (!timedMessages.isEmpty()) { + sendOneMessage(); + } + } + + public void sendLastMessage() { + Message message = timedMessages.remove(timedMessages.size() - 1).first; + removeMessages(message.what, message.obj); + mCallback.handleMessage(message); + removeStaleMessages(); + } + + public boolean hasMessages() { + removeStaleMessages(); + return !timedMessages.isEmpty(); + } + + private void removeStaleMessages() { + for (int i = 0; i < timedMessages.size(); i++) { + Message message = timedMessages.get(i).first; + if (!hasMessages(message.what, message.obj)) { + timedMessages.remove(i--); + } + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java b/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java index 5920fef1dff5..d5305d96725d 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java @@ -92,7 +92,12 @@ public class MotionEventInjectorTest { @Before public void setUp() { - mMessageCapturingHandler = new MessageCapturingHandler(); + mMessageCapturingHandler = new MessageCapturingHandler(new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + return mMotionEventInjector.handleMessage(msg); + } + }); mMotionEventInjector = new MotionEventInjector(mMessageCapturingHandler); mClickList.add( MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, CLICK_X, CLICK_Y_START, 0)); @@ -501,48 +506,4 @@ public class MotionEventInjectorTest { return false; } } - - private class MessageCapturingHandler extends Handler { - List<Pair<Message, Long>> timedMessages = new ArrayList<>(); - - @Override - public boolean sendMessageAtTime(Message message, long uptimeMillis) { - timedMessages.add(new Pair<>(Message.obtain(message), uptimeMillis)); - return super.sendMessageAtTime(message, uptimeMillis); - } - - void sendOneMessage() { - Message message = timedMessages.remove(0).first; - removeMessages(message.what, message.obj); - mMotionEventInjector.handleMessage(message); - removeStaleMessages(); - } - - void sendAllMessages() { - while (!timedMessages.isEmpty()) { - sendOneMessage(); - } - } - - void sendLastMessage() { - Message message = timedMessages.remove(timedMessages.size() - 1).first; - removeMessages(message.what, message.obj); - mMotionEventInjector.handleMessage(message); - removeStaleMessages(); - } - - boolean hasMessages() { - removeStaleMessages(); - return !timedMessages.isEmpty(); - } - - private void removeStaleMessages() { - for (int i = 0; i < timedMessages.size(); i++) { - Message message = timedMessages.get(i).first; - if (!hasMessages(message.what, message.obj)) { - timedMessages.remove(i--); - } - } - } - } } |