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.res.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     private Runnable mOnAnimationEndRunnable;
76 
WindowMagnificationAnimationController(@iContext Context context)77     WindowMagnificationAnimationController(@UiContext Context context) {
78         this(context, newValueAnimator(context.getResources()));
79     }
80 
81     @VisibleForTesting
WindowMagnificationAnimationController(Context context, ValueAnimator valueAnimator)82     WindowMagnificationAnimationController(Context context, ValueAnimator valueAnimator) {
83         mContext = context;
84         mValueAnimator = valueAnimator;
85         mValueAnimator.addUpdateListener(this);
86         mValueAnimator.addListener(this);
87     }
88 
setWindowMagnificationController(@onNull WindowMagnificationController controller)89     void setWindowMagnificationController(@NonNull WindowMagnificationController controller) {
90         mController = controller;
91     }
92 
93     /**
94      * Wraps {@link WindowMagnificationController#enableWindowMagnification(float, float, float,
95      * float, float, IRemoteMagnificationAnimationCallback)}
96      * with transition animation. If the window magnification is not enabled, the scale will start
97      * from 1.0 and the center won't be changed during the animation. If {@link #mState} is
98      * {@code STATE_DISABLING}, the animation runs in reverse.
99      *
100      * @param scale   The target scale, or {@link Float#NaN} to leave unchanged.
101      * @param centerX The screen-relative X coordinate around which to center,
102      *                or {@link Float#NaN} to leave unchanged.
103      * @param centerY The screen-relative Y coordinate around which to center,
104      *                or {@link Float#NaN} to leave unchanged.
105      * @param animationCallback Called when the transition is complete, the given arguments
106      *                          are as same as current values, or the transition is interrupted
107      *                          due to the new transition request.
108      *
109      * @see #onAnimationUpdate(ValueAnimator)
110      */
enableWindowMagnification(float scale, float centerX, float centerY, @Nullable IRemoteMagnificationAnimationCallback animationCallback)111     void enableWindowMagnification(float scale, float centerX, float centerY,
112             @Nullable IRemoteMagnificationAnimationCallback animationCallback) {
113         enableWindowMagnification(scale, centerX, centerY, 0f, 0f, animationCallback);
114     }
115 
116     /**
117      * Wraps {@link WindowMagnificationController#enableWindowMagnification(float, float, float,
118      * float, float, IRemoteMagnificationAnimationCallback)}
119      * with transition animation. If the window magnification is not enabled, the scale will start
120      * from 1.0 and the center won't be changed during the animation. If {@link #mState} is
121      * {@code STATE_DISABLING}, the animation runs in reverse.
122      *
123      * @param scale   The target scale, or {@link Float#NaN} to leave unchanged.
124      * @param centerX The screen-relative X coordinate around which to center for magnification,
125      *                or {@link Float#NaN} to leave unchanged.
126      * @param centerY The screen-relative Y coordinate around which to center for magnification,
127      *                or {@link Float#NaN} to leave unchanged.
128      * @param magnificationFrameOffsetRatioX Indicate the X coordinate offset between
129      *                                       frame position X and centerX
130      * @param magnificationFrameOffsetRatioY Indicate the Y coordinate offset between
131      *                                       frame position Y and centerY
132      * @param animationCallback Called when the transition is complete, the given arguments
133      *                          are as same as current values, or the transition is interrupted
134      *                          due to the new transition request.
135      *
136      * @see #onAnimationUpdate(ValueAnimator)
137      */
enableWindowMagnification(float scale, float centerX, float centerY, float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY, @Nullable IRemoteMagnificationAnimationCallback animationCallback)138     void enableWindowMagnification(float scale, float centerX, float centerY,
139             float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY,
140             @Nullable IRemoteMagnificationAnimationCallback animationCallback) {
141         if (mController == null) {
142             return;
143         }
144         sendAnimationCallback(false);
145         mMagnificationFrameOffsetRatioX = magnificationFrameOffsetRatioX;
146         mMagnificationFrameOffsetRatioY = magnificationFrameOffsetRatioY;
147 
148         // Enable window magnification without animation immediately.
149         if (animationCallback == null) {
150             if (mState == STATE_ENABLING || mState == STATE_DISABLING) {
151                 mValueAnimator.cancel();
152             }
153             mController.updateWindowMagnificationInternal(scale, centerX, centerY,
154                     mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY);
155             updateState();
156             return;
157         }
158         mAnimationCallback = animationCallback;
159         setupEnableAnimationSpecs(scale, centerX, centerY);
160 
161         if (mEndSpec.equals(mStartSpec)) {
162             if (mState == STATE_DISABLED) {
163                 mController.updateWindowMagnificationInternal(scale, centerX, centerY,
164                         mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY);
165             } else if (mState == STATE_ENABLING || mState == STATE_DISABLING) {
166                 mValueAnimator.cancel();
167             }
168             sendAnimationCallback(true);
169             updateState();
170         } else {
171             if (mState == STATE_DISABLING) {
172                 mValueAnimator.reverse();
173             } else {
174                 if (mState == STATE_ENABLING) {
175                     mValueAnimator.cancel();
176                 }
177                 mValueAnimator.start();
178             }
179             setState(STATE_ENABLING);
180         }
181     }
182 
moveWindowMagnifierToPosition(float centerX, float centerY, IRemoteMagnificationAnimationCallback callback)183     void moveWindowMagnifierToPosition(float centerX, float centerY,
184             IRemoteMagnificationAnimationCallback callback) {
185         if (mState == STATE_ENABLED) {
186             // We set the animation duration to shortAnimTime which would be reset at the end.
187             mValueAnimator.setDuration(mContext.getResources()
188                     .getInteger(com.android.internal.R.integer.config_shortAnimTime));
189             enableWindowMagnification(Float.NaN, centerX, centerY,
190                     /* magnificationFrameOffsetRatioX */ Float.NaN,
191                     /* magnificationFrameOffsetRatioY */ Float.NaN, callback);
192         } else if (mState == STATE_ENABLING) {
193             sendAnimationCallback(false);
194             mAnimationCallback = callback;
195             mValueAnimator.setDuration(mContext.getResources()
196                     .getInteger(com.android.internal.R.integer.config_shortAnimTime));
197             setupEnableAnimationSpecs(Float.NaN, centerX, centerY);
198         }
199     }
200 
setupEnableAnimationSpecs(float scale, float centerX, float centerY)201     private void setupEnableAnimationSpecs(float scale, float centerX, float centerY) {
202         if (mController == null) {
203             return;
204         }
205         final float currentScale = mController.getScale();
206         final float currentCenterX = mController.getCenterX();
207         final float currentCenterY = mController.getCenterY();
208 
209         if (mState == STATE_DISABLED) {
210             // We don't need to offset the center during the animation.
211             mStartSpec.set(/* scale*/ 1.0f, centerX, centerY);
212             mEndSpec.set(Float.isNaN(scale) ? mContext.getResources().getInteger(
213                     R.integer.magnification_default_scale) : scale, centerX, centerY);
214         } else {
215             mStartSpec.set(currentScale, currentCenterX, currentCenterY);
216 
217             final float endScale = (mState == STATE_ENABLING ? mEndSpec.mScale : currentScale);
218             final float endCenterX =
219                     (mState == STATE_ENABLING ? mEndSpec.mCenterX : currentCenterX);
220             final float endCenterY =
221                     (mState == STATE_ENABLING ? mEndSpec.mCenterY : currentCenterY);
222 
223             mEndSpec.set(Float.isNaN(scale) ? endScale : scale,
224                     Float.isNaN(centerX) ? endCenterX : centerX,
225                     Float.isNaN(centerY) ? endCenterY : centerY);
226         }
227         if (DEBUG) {
228             Log.d(TAG, "SetupEnableAnimationSpecs : mStartSpec = " + mStartSpec + ", endSpec = "
229                     + mEndSpec);
230         }
231     }
232 
233     /** Returns {@code true} if the animator is running. */
isAnimating()234     boolean isAnimating() {
235         return mValueAnimator.isRunning();
236     }
237 
238     /**
239      * Wraps {@link WindowMagnificationController#deleteWindowMagnification()}} with transition
240      * animation. If the window magnification is enabling, it runs the animation in reverse.
241      *
242      * @param animationCallback Called when the transition is complete, the given arguments
243      *                          are as same as current values, or the transition is interrupted
244      *                          due to the new transition request.
245      */
deleteWindowMagnification( @ullable IRemoteMagnificationAnimationCallback animationCallback)246     void deleteWindowMagnification(
247             @Nullable IRemoteMagnificationAnimationCallback animationCallback) {
248         if (mController == null) {
249             return;
250         }
251         sendAnimationCallback(false);
252         // Delete window magnification without animation.
253         if (animationCallback == null) {
254             if (mState == STATE_ENABLING || mState == STATE_DISABLING) {
255                 mValueAnimator.cancel();
256             }
257             mController.deleteWindowMagnification();
258             updateState();
259             return;
260         }
261 
262         mAnimationCallback = animationCallback;
263         if (mState == STATE_DISABLED || mState == STATE_DISABLING) {
264             if (mState == STATE_DISABLED) {
265                 sendAnimationCallback(true);
266             }
267             return;
268         }
269         mStartSpec.set(/* scale*/ 1.0f, Float.NaN, Float.NaN);
270         mEndSpec.set(/* scale*/ mController.getScale(), Float.NaN, Float.NaN);
271 
272         mValueAnimator.reverse();
273         setState(STATE_DISABLING);
274     }
275 
updateState()276     private void updateState() {
277         if (Float.isNaN(mController.getScale())) {
278             setState(STATE_DISABLED);
279         } else {
280             setState(STATE_ENABLED);
281         }
282     }
283 
setState(@agnificationState int state)284     private void setState(@MagnificationState int state) {
285         if (DEBUG) {
286             Log.d(TAG, "setState from " + mState + " to " + state);
287         }
288         mState = state;
289     }
290 
291     @VisibleForTesting
getState()292     @MagnificationState int getState() {
293         return mState;
294     }
295 
296     @Override
onAnimationStart(Animator animation)297     public void onAnimationStart(Animator animation) {
298         mEndAnimationCanceled = false;
299     }
300 
301     @Override
onAnimationEnd(Animator animation, boolean isReverse)302     public void onAnimationEnd(Animator animation, boolean isReverse) {
303         if (mEndAnimationCanceled || mController == null) {
304             return;
305         }
306 
307         mOnAnimationEndRunnable.run();
308 
309         if (mState == STATE_DISABLING) {
310             mController.deleteWindowMagnification();
311         }
312         updateState();
313         sendAnimationCallback(true);
314         // We reset the duration to config_longAnimTime
315         mValueAnimator.setDuration(mContext.getResources()
316                 .getInteger(com.android.internal.R.integer.config_longAnimTime));
317     }
318 
319     @Override
onAnimationEnd(Animator animation)320     public void onAnimationEnd(Animator animation) {
321     }
322 
323     @Override
onAnimationCancel(Animator animation)324     public void onAnimationCancel(Animator animation) {
325         mEndAnimationCanceled = true;
326     }
327 
328     @Override
onAnimationRepeat(Animator animation)329     public void onAnimationRepeat(Animator animation) {
330     }
331 
setOnAnimationEndRunnable(Runnable runnable)332     void setOnAnimationEndRunnable(Runnable runnable) {
333         mOnAnimationEndRunnable = runnable;
334     }
335 
sendAnimationCallback(boolean success)336     private void sendAnimationCallback(boolean success) {
337         if (mAnimationCallback != null) {
338             try {
339                 mAnimationCallback.onResult(success);
340                 if (DEBUG) {
341                     Log.d(TAG, "sendAnimationCallback success = " + success);
342                 }
343             } catch (RemoteException e) {
344                 Log.w(TAG, "sendAnimationCallback failed : " + e);
345             }
346             mAnimationCallback = null;
347         }
348     }
349 
350     @Override
onAnimationUpdate(ValueAnimator animation)351     public void onAnimationUpdate(ValueAnimator animation) {
352         if (mController == null) {
353             return;
354         }
355         final float fract = animation.getAnimatedFraction();
356         final float sentScale = mStartSpec.mScale + (mEndSpec.mScale - mStartSpec.mScale) * fract;
357         final float centerX =
358                 mStartSpec.mCenterX + (mEndSpec.mCenterX - mStartSpec.mCenterX) * fract;
359         final float centerY =
360                 mStartSpec.mCenterY + (mEndSpec.mCenterY - mStartSpec.mCenterY) * fract;
361         mController.updateWindowMagnificationInternal(sentScale, centerX, centerY,
362                 mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY);
363     }
364 
newValueAnimator(Resources resource)365     private static ValueAnimator newValueAnimator(Resources resource) {
366         final ValueAnimator valueAnimator = new ValueAnimator();
367         valueAnimator.setDuration(
368                 resource.getInteger(com.android.internal.R.integer.config_longAnimTime));
369         valueAnimator.setInterpolator(new AccelerateInterpolator(2.5f));
370         valueAnimator.setFloatValues(0.0f, 1.0f);
371         return valueAnimator;
372     }
373 
374     private static class AnimationSpec {
375         private float mScale = Float.NaN;
376         private float mCenterX = Float.NaN;
377         private float mCenterY = Float.NaN;
378 
379         @Override
equals(Object other)380         public boolean equals(Object other) {
381             if (this == other) {
382                 return true;
383             }
384 
385             if (other == null || getClass() != other.getClass()) {
386                 return false;
387             }
388 
389             final AnimationSpec s = (AnimationSpec) other;
390             return mScale == s.mScale && mCenterX == s.mCenterX && mCenterY == s.mCenterY;
391         }
392 
393         @Override
hashCode()394         public int hashCode() {
395             int result = (mScale != +0.0f ? Float.floatToIntBits(mScale) : 0);
396             result = 31 * result + (mCenterX != +0.0f ? Float.floatToIntBits(mCenterX) : 0);
397             result = 31 * result + (mCenterY != +0.0f ? Float.floatToIntBits(mCenterY) : 0);
398             return result;
399         }
400 
set(float scale, float centerX, float centerY)401         void set(float scale, float centerX, float centerY) {
402             mScale = scale;
403             mCenterX = centerX;
404             mCenterY = centerY;
405         }
406 
407         @Override
toString()408         public String toString() {
409             return "AnimationSpec{"
410                     + "mScale=" + mScale
411                     + ", mCenterX=" + mCenterX
412                     + ", mCenterY=" + mCenterY
413                     + '}';
414         }
415     }
416 }
417