1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.accessibility;
18 
19 import android.animation.Animator;
20 import android.animation.ValueAnimator;
21 import android.annotation.IntDef;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.UiContext;
25 import android.content.Context;
26 import android.content.res.Resources;
27 import android.os.RemoteException;
28 import android.util.Log;
29 import android.view.accessibility.IRemoteMagnificationAnimationCallback;
30 import android.view.animation.AccelerateInterpolator;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.systemui.R;
34 
35 import java.lang.annotation.Retention;
36 import java.lang.annotation.RetentionPolicy;
37 
38 /**
39  * Provides same functionality of {@link WindowMagnificationController}. Some methods run with
40  * the animation.
41  */
42 class WindowMagnificationAnimationController implements ValueAnimator.AnimatorUpdateListener,
43         Animator.AnimatorListener {
44 
45     private static final String TAG = "WindowMagnificationAnimationController";
46     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
47 
48     @Retention(RetentionPolicy.SOURCE)
49     @IntDef({STATE_DISABLED, STATE_ENABLED, STATE_DISABLING, STATE_ENABLING})
50     @interface MagnificationState {}
51 
52     // The window magnification is disabled.
53     @VisibleForTesting static final int STATE_DISABLED = 0;
54     // The window magnification is enabled.
55     @VisibleForTesting static final int STATE_ENABLED = 1;
56     // The window magnification is going to be disabled when the animation is end.
57     private static final int STATE_DISABLING = 2;
58     // The animation is running for enabling the window magnification.
59     private static final int STATE_ENABLING = 3;
60 
61     private WindowMagnificationController mController;
62     private final ValueAnimator mValueAnimator;
63     private final AnimationSpec mStartSpec = new AnimationSpec();
64     private final AnimationSpec mEndSpec = new AnimationSpec();
65     private float mMagnificationFrameOffsetRatioX = 0f;
66     private float mMagnificationFrameOffsetRatioY = 0f;
67     private final Context mContext;
68     // Called when the animation is ended successfully without cancelling or mStartSpec and
69     // mEndSpec are equal.
70     private IRemoteMagnificationAnimationCallback mAnimationCallback;
71     // The flag to ignore the animation end callback.
72     private boolean mEndAnimationCanceled = false;
73     @MagnificationState
74     private int mState = STATE_DISABLED;
75 
WindowMagnificationAnimationController(@iContext Context context)76     WindowMagnificationAnimationController(@UiContext Context context) {
77         this(context, newValueAnimator(context.getResources()));
78     }
79 
80     @VisibleForTesting
WindowMagnificationAnimationController(Context context, ValueAnimator valueAnimator)81     WindowMagnificationAnimationController(Context context, ValueAnimator valueAnimator) {
82         mContext = context;
83         mValueAnimator = valueAnimator;
84         mValueAnimator.addUpdateListener(this);
85         mValueAnimator.addListener(this);
86     }
87 
setWindowMagnificationController(@onNull WindowMagnificationController controller)88     void setWindowMagnificationController(@NonNull WindowMagnificationController controller) {
89         mController = controller;
90     }
91 
92     /**
93      * Wraps {@link WindowMagnificationController#enableWindowMagnification(float, float, float,
94      * float, float, IRemoteMagnificationAnimationCallback)}
95      * with transition animation. If the window magnification is not enabled, the scale will start
96      * from 1.0 and the center won't be changed during the animation. If {@link #mState} is
97      * {@code STATE_DISABLING}, the animation runs in reverse.
98      *
99      * @param scale   The target scale, or {@link Float#NaN} to leave unchanged.
100      * @param centerX The screen-relative X coordinate around which to center,
101      *                or {@link Float#NaN} to leave unchanged.
102      * @param centerY The screen-relative Y coordinate around which to center,
103      *                or {@link Float#NaN} to leave unchanged.
104      * @param animationCallback Called when the transition is complete, the given arguments
105      *                          are as same as current values, or the transition is interrupted
106      *                          due to the new transition request.
107      *
108      * @see #onAnimationUpdate(ValueAnimator)
109      */
enableWindowMagnification(float scale, float centerX, float centerY, @Nullable IRemoteMagnificationAnimationCallback animationCallback)110     void enableWindowMagnification(float scale, float centerX, float centerY,
111             @Nullable IRemoteMagnificationAnimationCallback animationCallback) {
112         enableWindowMagnification(scale, centerX, centerY, 0f, 0f, animationCallback);
113     }
114 
115     /**
116      * Wraps {@link WindowMagnificationController#enableWindowMagnification(float, float, float,
117      * float, float, IRemoteMagnificationAnimationCallback)}
118      * with transition animation. If the window magnification is not enabled, the scale will start
119      * from 1.0 and the center won't be changed during the animation. If {@link #mState} is
120      * {@code STATE_DISABLING}, the animation runs in reverse.
121      *
122      * @param scale   The target scale, or {@link Float#NaN} to leave unchanged.
123      * @param centerX The screen-relative X coordinate around which to center for magnification,
124      *                or {@link Float#NaN} to leave unchanged.
125      * @param centerY The screen-relative Y coordinate around which to center for magnification,
126      *                or {@link Float#NaN} to leave unchanged.
127      * @param magnificationFrameOffsetRatioX Indicate the X coordinate offset between
128      *                                       frame position X and centerX
129      * @param magnificationFrameOffsetRatioY Indicate the Y coordinate offset between
130      *                                       frame position Y and centerY
131      * @param animationCallback Called when the transition is complete, the given arguments
132      *                          are as same as current values, or the transition is interrupted
133      *                          due to the new transition request.
134      *
135      * @see #onAnimationUpdate(ValueAnimator)
136      */
enableWindowMagnification(float scale, float centerX, float centerY, float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY, @Nullable IRemoteMagnificationAnimationCallback animationCallback)137     void enableWindowMagnification(float scale, float centerX, float centerY,
138             float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY,
139             @Nullable IRemoteMagnificationAnimationCallback animationCallback) {
140         if (mController == null) {
141             return;
142         }
143         sendAnimationCallback(false);
144         mMagnificationFrameOffsetRatioX = magnificationFrameOffsetRatioX;
145         mMagnificationFrameOffsetRatioY = magnificationFrameOffsetRatioY;
146 
147         // Enable window magnification without animation immediately.
148         if (animationCallback == null) {
149             if (mState == STATE_ENABLING || mState == STATE_DISABLING) {
150                 mValueAnimator.cancel();
151             }
152             mController.enableWindowMagnificationInternal(scale, centerX, centerY,
153                     mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY);
154             updateState();
155             return;
156         }
157         mAnimationCallback = animationCallback;
158         setupEnableAnimationSpecs(scale, centerX, centerY);
159 
160         if (mEndSpec.equals(mStartSpec)) {
161             if (mState == STATE_DISABLED) {
162                 mController.enableWindowMagnificationInternal(scale, centerX, centerY,
163                         mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY);
164             } else if (mState == STATE_ENABLING || mState == STATE_DISABLING) {
165                 mValueAnimator.cancel();
166             }
167             sendAnimationCallback(true);
168             updateState();
169         } else {
170             if (mState == STATE_DISABLING) {
171                 mValueAnimator.reverse();
172             } else {
173                 if (mState == STATE_ENABLING) {
174                     mValueAnimator.cancel();
175                 }
176                 mValueAnimator.start();
177             }
178             setState(STATE_ENABLING);
179         }
180     }
181 
moveWindowMagnifierToPosition(float centerX, float centerY, IRemoteMagnificationAnimationCallback callback)182     void moveWindowMagnifierToPosition(float centerX, float centerY,
183             IRemoteMagnificationAnimationCallback callback) {
184         if (mState == STATE_ENABLED) {
185             // We set the animation duration to shortAnimTime which would be reset at the end.
186             mValueAnimator.setDuration(mContext.getResources()
187                     .getInteger(com.android.internal.R.integer.config_shortAnimTime));
188             enableWindowMagnification(Float.NaN, centerX, centerY,
189                     /* magnificationFrameOffsetRatioX */ Float.NaN,
190                     /* magnificationFrameOffsetRatioY */ Float.NaN, callback);
191         } else if (mState == STATE_ENABLING) {
192             sendAnimationCallback(false);
193             mAnimationCallback = callback;
194             mValueAnimator.setDuration(mContext.getResources()
195                     .getInteger(com.android.internal.R.integer.config_shortAnimTime));
196             setupEnableAnimationSpecs(Float.NaN, centerX, centerY);
197         }
198     }
199 
setupEnableAnimationSpecs(float scale, float centerX, float centerY)200     private void setupEnableAnimationSpecs(float scale, float centerX, float centerY) {
201         if (mController == null) {
202             return;
203         }
204         final float currentScale = mController.getScale();
205         final float currentCenterX = mController.getCenterX();
206         final float currentCenterY = mController.getCenterY();
207 
208         if (mState == STATE_DISABLED) {
209             // We don't need to offset the center during the animation.
210             mStartSpec.set(/* scale*/ 1.0f, centerX, centerY);
211             mEndSpec.set(Float.isNaN(scale) ? mContext.getResources().getInteger(
212                     R.integer.magnification_default_scale) : scale, centerX, centerY);
213         } else {
214             mStartSpec.set(currentScale, currentCenterX, currentCenterY);
215 
216             final float endScale = (mState == STATE_ENABLING ? mEndSpec.mScale : currentScale);
217             final float endCenterX =
218                     (mState == STATE_ENABLING ? mEndSpec.mCenterX : currentCenterX);
219             final float endCenterY =
220                     (mState == STATE_ENABLING ? mEndSpec.mCenterY : currentCenterY);
221 
222             mEndSpec.set(Float.isNaN(scale) ? endScale : scale,
223                     Float.isNaN(centerX) ? endCenterX : centerX,
224                     Float.isNaN(centerY) ? endCenterY : centerY);
225         }
226         if (DEBUG) {
227             Log.d(TAG, "SetupEnableAnimationSpecs : mStartSpec = " + mStartSpec + ", endSpec = "
228                     + mEndSpec);
229         }
230     }
231 
232     /** Returns {@code true} if the animator is running. */
isAnimating()233     boolean isAnimating() {
234         return mValueAnimator.isRunning();
235     }
236 
237     /**
238      * Wraps {@link WindowMagnificationController#deleteWindowMagnification()}} with transition
239      * animation. If the window magnification is enabling, it runs the animation in reverse.
240      *
241      * @param animationCallback Called when the transition is complete, the given arguments
242      *                          are as same as current values, or the transition is interrupted
243      *                          due to the new transition request.
244      */
deleteWindowMagnification( @ullable IRemoteMagnificationAnimationCallback animationCallback)245     void deleteWindowMagnification(
246             @Nullable IRemoteMagnificationAnimationCallback animationCallback) {
247         if (mController == null) {
248             return;
249         }
250         sendAnimationCallback(false);
251         // Delete window magnification without animation.
252         if (animationCallback == null) {
253             if (mState == STATE_ENABLING || mState == STATE_DISABLING) {
254                 mValueAnimator.cancel();
255             }
256             mController.deleteWindowMagnification();
257             updateState();
258             return;
259         }
260 
261         mAnimationCallback = animationCallback;
262         if (mState == STATE_DISABLED || mState == STATE_DISABLING) {
263             if (mState == STATE_DISABLED) {
264                 sendAnimationCallback(true);
265             }
266             return;
267         }
268         mStartSpec.set(/* scale*/ 1.0f, Float.NaN, Float.NaN);
269         mEndSpec.set(/* scale*/ mController.getScale(), Float.NaN, Float.NaN);
270 
271         mValueAnimator.reverse();
272         setState(STATE_DISABLING);
273     }
274 
updateState()275     private void updateState() {
276         if (Float.isNaN(mController.getScale())) {
277             setState(STATE_DISABLED);
278         } else {
279             setState(STATE_ENABLED);
280         }
281     }
282 
setState(@agnificationState int state)283     private void setState(@MagnificationState int state) {
284         if (DEBUG) {
285             Log.d(TAG, "setState from " + mState + " to " + state);
286         }
287         mState = state;
288     }
289 
290     @VisibleForTesting
getState()291     @MagnificationState int getState() {
292         return mState;
293     }
294 
295     @Override
onAnimationStart(Animator animation)296     public void onAnimationStart(Animator animation) {
297         mEndAnimationCanceled = false;
298     }
299 
300     @Override
onAnimationEnd(Animator animation, boolean isReverse)301     public void onAnimationEnd(Animator animation, boolean isReverse) {
302         if (mEndAnimationCanceled || mController == null) {
303             return;
304         }
305         if (mState == STATE_DISABLING) {
306             mController.deleteWindowMagnification();
307         }
308         updateState();
309         sendAnimationCallback(true);
310         // We reset the duration to config_longAnimTime
311         mValueAnimator.setDuration(mContext.getResources()
312                 .getInteger(com.android.internal.R.integer.config_longAnimTime));
313     }
314 
315     @Override
onAnimationEnd(Animator animation)316     public void onAnimationEnd(Animator animation) {
317     }
318 
319     @Override
onAnimationCancel(Animator animation)320     public void onAnimationCancel(Animator animation) {
321         mEndAnimationCanceled = true;
322     }
323 
324     @Override
onAnimationRepeat(Animator animation)325     public void onAnimationRepeat(Animator animation) {
326     }
327 
sendAnimationCallback(boolean success)328     private void sendAnimationCallback(boolean success) {
329         if (mAnimationCallback != null) {
330             try {
331                 mAnimationCallback.onResult(success);
332                 if (DEBUG) {
333                     Log.d(TAG, "sendAnimationCallback success = " + success);
334                 }
335             } catch (RemoteException e) {
336                 Log.w(TAG, "sendAnimationCallback failed : " + e);
337             }
338             mAnimationCallback = null;
339         }
340     }
341 
342     @Override
onAnimationUpdate(ValueAnimator animation)343     public void onAnimationUpdate(ValueAnimator animation) {
344         if (mController == null) {
345             return;
346         }
347         final float fract = animation.getAnimatedFraction();
348         final float sentScale = mStartSpec.mScale + (mEndSpec.mScale - mStartSpec.mScale) * fract;
349         final float centerX =
350                 mStartSpec.mCenterX + (mEndSpec.mCenterX - mStartSpec.mCenterX) * fract;
351         final float centerY =
352                 mStartSpec.mCenterY + (mEndSpec.mCenterY - mStartSpec.mCenterY) * fract;
353         mController.enableWindowMagnificationInternal(sentScale, centerX, centerY,
354                 mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY);
355     }
356 
newValueAnimator(Resources resource)357     private static ValueAnimator newValueAnimator(Resources resource) {
358         final ValueAnimator valueAnimator = new ValueAnimator();
359         valueAnimator.setDuration(
360                 resource.getInteger(com.android.internal.R.integer.config_longAnimTime));
361         valueAnimator.setInterpolator(new AccelerateInterpolator(2.5f));
362         valueAnimator.setFloatValues(0.0f, 1.0f);
363         return valueAnimator;
364     }
365 
366     private static class AnimationSpec {
367         private float mScale = Float.NaN;
368         private float mCenterX = Float.NaN;
369         private float mCenterY = Float.NaN;
370 
371         @Override
equals(Object other)372         public boolean equals(Object other) {
373             if (this == other) {
374                 return true;
375             }
376 
377             if (other == null || getClass() != other.getClass()) {
378                 return false;
379             }
380 
381             final AnimationSpec s = (AnimationSpec) other;
382             return mScale == s.mScale && mCenterX == s.mCenterX && mCenterY == s.mCenterY;
383         }
384 
385         @Override
hashCode()386         public int hashCode() {
387             int result = (mScale != +0.0f ? Float.floatToIntBits(mScale) : 0);
388             result = 31 * result + (mCenterX != +0.0f ? Float.floatToIntBits(mCenterX) : 0);
389             result = 31 * result + (mCenterY != +0.0f ? Float.floatToIntBits(mCenterY) : 0);
390             return result;
391         }
392 
set(float scale, float centerX, float centerY)393         void set(float scale, float centerX, float centerY) {
394             mScale = scale;
395             mCenterX = centerX;
396             mCenterY = centerY;
397         }
398 
399         @Override
toString()400         public String toString() {
401             return "AnimationSpec{"
402                     + "mScale=" + mScale
403                     + ", mCenterX=" + mCenterX
404                     + ", mCenterY=" + mCenterY
405                     + '}';
406         }
407     }
408 }
409