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.Nullable;
23 import android.annotation.UiContext;
24 import android.content.Context;
25 import android.content.res.Resources;
26 import android.os.RemoteException;
27 import android.util.Log;
28 import android.view.accessibility.IRemoteMagnificationAnimationCallback;
29 import android.view.animation.AccelerateInterpolator;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.systemui.R;
33 
34 import java.io.PrintWriter;
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     private static final int STATE_DISABLED = 0;
54     // The window magnification is enabled.
55     private 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 final WindowMagnificationController mController;
62     private final ValueAnimator mValueAnimator;
63     private final AnimationSpec mStartSpec = new AnimationSpec();
64     private final AnimationSpec mEndSpec = new AnimationSpec();
65     private final Context mContext;
66     // Called when the animation is ended successfully without cancelling or mStartSpec and
67     // mEndSpec are equal.
68     private IRemoteMagnificationAnimationCallback mAnimationCallback;
69     // The flag to ignore the animation end callback.
70     private boolean mEndAnimationCanceled = false;
71     @MagnificationState
72     private int mState = STATE_DISABLED;
73 
WindowMagnificationAnimationController(@iContext Context context, WindowMagnificationController controller)74     WindowMagnificationAnimationController(@UiContext Context context,
75             WindowMagnificationController controller) {
76         this(context, controller, newValueAnimator(context.getResources()));
77     }
78 
79     @VisibleForTesting
WindowMagnificationAnimationController(Context context, WindowMagnificationController controller, ValueAnimator valueAnimator)80     WindowMagnificationAnimationController(Context context,
81             WindowMagnificationController controller, ValueAnimator valueAnimator) {
82         mContext = context;
83         mController = controller;
84         mValueAnimator = valueAnimator;
85         mValueAnimator.addUpdateListener(this);
86         mValueAnimator.addListener(this);
87     }
88 
89     /**
90      * Wraps {@link WindowMagnificationController#enableWindowMagnification(float, float, float)}
91      * with transition animation. If the window magnification is not enabled, the scale will start
92      * from 1.0 and the center won't be changed during the animation. If {@link #mState} is
93      * {@code STATE_DISABLING}, the animation runs in reverse.
94      *
95      * @param scale   The target scale, or {@link Float#NaN} to leave unchanged.
96      * @param centerX The screen-relative X coordinate around which to center,
97      *                or {@link Float#NaN} to leave unchanged.
98      * @param centerY The screen-relative Y coordinate around which to center,
99      *                or {@link Float#NaN} to leave unchanged.
100      * @param animationCallback Called when the transition is complete, the given arguments
101      *                          are as same as current values, or the transition is interrupted
102      *                          due to the new transition request.
103      *
104      * @see #onAnimationUpdate(ValueAnimator)
105      */
enableWindowMagnification(float scale, float centerX, float centerY, @Nullable IRemoteMagnificationAnimationCallback animationCallback)106     void enableWindowMagnification(float scale, float centerX, float centerY,
107             @Nullable IRemoteMagnificationAnimationCallback animationCallback) {
108         sendAnimationCallback(false);
109         mAnimationCallback = animationCallback;
110         setupEnableAnimationSpecs(scale, centerX, centerY);
111         if (mEndSpec.equals(mStartSpec)) {
112             if (mState == STATE_DISABLED) {
113                 mController.enableWindowMagnification(scale, centerX, centerY);
114             } else if (mState == STATE_ENABLING || mState == STATE_DISABLING) {
115                 mValueAnimator.cancel();
116             }
117             sendAnimationCallback(true);
118             setState(STATE_ENABLED);
119         } else {
120             if (mState == STATE_DISABLING) {
121                 mValueAnimator.reverse();
122             } else {
123                 if (mState == STATE_ENABLING) {
124                     mValueAnimator.cancel();
125                 }
126                 mValueAnimator.start();
127             }
128             setState(STATE_ENABLING);
129         }
130     }
131 
setupEnableAnimationSpecs(float scale, float centerX, float centerY)132     private void setupEnableAnimationSpecs(float scale, float centerX, float centerY) {
133         final float currentScale = mController.getScale();
134         final float currentCenterX = mController.getCenterX();
135         final float currentCenterY = mController.getCenterY();
136 
137         if (mState == STATE_DISABLED) {
138             // We don't need to offset the center during the animation.
139             mStartSpec.set(/* scale*/ 1.0f, centerX, centerY);
140             mEndSpec.set(Float.isNaN(scale) ? mContext.getResources().getInteger(
141                     R.integer.magnification_default_scale) : scale, centerX, centerY);
142         } else {
143             mStartSpec.set(currentScale, currentCenterX, currentCenterY);
144             mEndSpec.set(Float.isNaN(scale) ? currentScale : scale,
145                     Float.isNaN(centerX) ? currentCenterX : centerX,
146                     Float.isNaN(centerY) ? currentCenterY : centerY);
147         }
148         if (DEBUG) {
149             Log.d(TAG, "SetupEnableAnimationSpecs : mStartSpec = " + mStartSpec + ", endSpec = "
150                     + mEndSpec);
151         }
152     }
153 
154     /**
155      * Wraps {@link WindowMagnificationController#setScale(float)}. If the animation is
156      * running, it has no effect.
157      */
setScale(float scale)158     void setScale(float scale) {
159         if (mValueAnimator.isRunning()) {
160             return;
161         }
162         mController.setScale(scale);
163     }
164 
165     /**
166      * Wraps {@link WindowMagnificationController#deleteWindowMagnification()}} with transition
167      * animation. If the window magnification is enabling, it runs the animation in reverse.
168      *
169      * @param animationCallback Called when the transition is complete, the given arguments
170      *                          are as same as current values, or the transition is interrupted
171      *                          due to the new transition request.
172      */
deleteWindowMagnification( @ullable IRemoteMagnificationAnimationCallback animationCallback)173     void deleteWindowMagnification(
174             @Nullable IRemoteMagnificationAnimationCallback animationCallback) {
175         sendAnimationCallback(false);
176         mAnimationCallback = animationCallback;
177         if (mState == STATE_DISABLED || mState == STATE_DISABLING) {
178             if (mState == STATE_DISABLED) {
179                 sendAnimationCallback(true);
180             }
181             return;
182         }
183         mStartSpec.set(/* scale*/ 1.0f, Float.NaN, Float.NaN);
184         mEndSpec.set(/* scale*/ mController.getScale(), Float.NaN, Float.NaN);
185 
186         mValueAnimator.reverse();
187         setState(STATE_DISABLING);
188     }
189 
190     /**
191      * Wraps {@link WindowMagnificationController#moveWindowMagnifier(float, float)}. If the
192      * animation is running, it has no effect.
193      * @param offsetX The amount in pixels to offset the window magnifier in the X direction, in
194      *                current screen pixels.
195      * @param offsetY The amount in pixels to offset the window magnifier in the Y direction, in
196      *                current screen pixels.
197      */
moveWindowMagnifier(float offsetX, float offsetY)198     void moveWindowMagnifier(float offsetX, float offsetY) {
199         if (mValueAnimator.isRunning()) {
200             return;
201         }
202         mController.moveWindowMagnifier(offsetX, offsetY);
203     }
204 
onConfigurationChanged(int configDiff)205     void onConfigurationChanged(int configDiff) {
206         mController.onConfigurationChanged(configDiff);
207     }
208 
setState(@agnificationState int state)209     private void setState(@MagnificationState int state) {
210         if (DEBUG) {
211             Log.d(TAG, "setState from " + mState + " to " + state);
212         }
213         mState = state;
214     }
215 
216     @Override
onAnimationStart(Animator animation)217     public void onAnimationStart(Animator animation) {
218         mEndAnimationCanceled = false;
219     }
220 
221     @Override
onAnimationEnd(Animator animation, boolean isReverse)222     public void onAnimationEnd(Animator animation, boolean isReverse) {
223         if (mEndAnimationCanceled) {
224             return;
225         }
226         if (isReverse) {
227             mController.deleteWindowMagnification();
228             setState(STATE_DISABLED);
229         } else {
230             setState(STATE_ENABLED);
231         }
232         sendAnimationCallback(true);
233     }
234 
235     @Override
onAnimationEnd(Animator animation)236     public void onAnimationEnd(Animator animation) {
237     }
238 
239     @Override
onAnimationCancel(Animator animation)240     public void onAnimationCancel(Animator animation) {
241         mEndAnimationCanceled = true;
242     }
243 
244     @Override
onAnimationRepeat(Animator animation)245     public void onAnimationRepeat(Animator animation) {
246     }
247 
sendAnimationCallback(boolean success)248     private void sendAnimationCallback(boolean success) {
249         if (mAnimationCallback != null) {
250             try {
251                 mAnimationCallback.onResult(success);
252                 if (DEBUG) {
253                     Log.d(TAG, "sendAnimationCallback success = " + success);
254                 }
255             } catch (RemoteException e) {
256                 Log.w(TAG, "sendAnimationCallback failed : " + e);
257             }
258             mAnimationCallback = null;
259         }
260     }
261 
262     @Override
onAnimationUpdate(ValueAnimator animation)263     public void onAnimationUpdate(ValueAnimator animation) {
264         final float fract = animation.getAnimatedFraction();
265         final float sentScale = mStartSpec.mScale + (mEndSpec.mScale - mStartSpec.mScale) * fract;
266         final float centerX =
267                 mStartSpec.mCenterX + (mEndSpec.mCenterX - mStartSpec.mCenterX) * fract;
268         final float centerY =
269                 mStartSpec.mCenterY + (mEndSpec.mCenterY - mStartSpec.mCenterY) * fract;
270         mController.enableWindowMagnification(sentScale, centerX, centerY);
271     }
272 
updateSysUiStateFlag()273     public void updateSysUiStateFlag() {
274         mController.updateSysUIStateFlag();
275     }
276 
dump(PrintWriter pw)277     void dump(PrintWriter pw) {
278         mController.dump(pw);
279     }
280 
newValueAnimator(Resources resources)281     private static ValueAnimator newValueAnimator(Resources resources) {
282         final ValueAnimator valueAnimator = new ValueAnimator();
283         valueAnimator.setDuration(
284                 resources.getInteger(com.android.internal.R.integer.config_longAnimTime));
285         valueAnimator.setInterpolator(new AccelerateInterpolator(2.5f));
286         valueAnimator.setFloatValues(0.0f, 1.0f);
287         return valueAnimator;
288     }
289 
290     private static class AnimationSpec {
291         private float mScale = Float.NaN;
292         private float mCenterX = Float.NaN;
293         private float mCenterY = Float.NaN;
294 
295         @Override
equals(Object other)296         public boolean equals(Object other) {
297             if (this == other) {
298                 return true;
299             }
300 
301             if (other == null || getClass() != other.getClass()) {
302                 return false;
303             }
304 
305             final AnimationSpec s = (AnimationSpec) other;
306             return mScale == s.mScale && mCenterX == s.mCenterX && mCenterY == s.mCenterY;
307         }
308 
309         @Override
hashCode()310         public int hashCode() {
311             int result = (mScale != +0.0f ? Float.floatToIntBits(mScale) : 0);
312             result = 31 * result + (mCenterX != +0.0f ? Float.floatToIntBits(mCenterX) : 0);
313             result = 31 * result + (mCenterY != +0.0f ? Float.floatToIntBits(mCenterY) : 0);
314             return result;
315         }
316 
set(float scale, float centerX, float centerY)317         void set(float scale, float centerX, float centerY) {
318             mScale = scale;
319             mCenterX = centerX;
320             mCenterY = centerY;
321         }
322 
323         @Override
toString()324         public String toString() {
325             return "AnimationSpec{"
326                     + "mScale=" + mScale
327                     + ", mCenterX=" + mCenterX
328                     + ", mCenterY=" + mCenterY
329                     + '}';
330         }
331     }
332 }
333