1 /*
2  * Copyright (C) 2023 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 package com.android.server.accessibility.magnification;
17 
18 import android.animation.Animator;
19 import android.animation.ObjectAnimator;
20 import android.annotation.AnyThread;
21 import android.annotation.MainThread;
22 import android.content.Context;
23 import android.graphics.PixelFormat;
24 import android.graphics.Point;
25 import android.graphics.Rect;
26 import android.os.Handler;
27 import android.util.Log;
28 import android.view.Gravity;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.WindowInsets;
32 import android.view.WindowManager;
33 import android.widget.FrameLayout;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.VisibleForTesting;
37 
38 import com.android.internal.R;
39 /**
40  *  This class is used to show of magnification thumbnail
41  *  from FullScreenMagnification. It is responsible for
42  *  show of magnification and fade in/out animation, and
43  *  it just only uses in FullScreenMagnification
44  */
45 public class MagnificationThumbnail {
46     private static final boolean DEBUG = false;
47     private static final String LOG_TAG = "MagnificationThumbnail";
48 
49     private static final int FADE_IN_ANIMATION_DURATION_MS = 200;
50     private static final int FADE_OUT_ANIMATION_DURATION_MS = 1000;
51     private static final int LINGER_DURATION_MS = 500;
52 
53     private Rect mWindowBounds;
54     private final Context mContext;
55     private final WindowManager mWindowManager;
56     private final Handler mHandler;
57 
58     @VisibleForTesting
59     public final FrameLayout mThumbnailLayout;
60 
61     private final View mThumbnailView;
62     private int mThumbnailWidth;
63     private int mThumbnailHeight;
64 
65     private final WindowManager.LayoutParams mBackgroundParams;
66     private boolean mVisible = false;
67 
68     private static final float ASPECT_RATIO = 14f;
69     private static final float BG_ASPECT_RATIO = ASPECT_RATIO / 2f;
70 
71     private ObjectAnimator mThumbnailAnimator;
72     private boolean mIsFadingIn;
73 
74     /**
75      * FullScreenMagnificationThumbnail Constructor
76      */
MagnificationThumbnail(Context context, WindowManager windowManager, Handler handler)77     public MagnificationThumbnail(Context context, WindowManager windowManager, Handler handler) {
78         mContext = context;
79         mWindowManager = windowManager;
80         mHandler = handler;
81         mWindowBounds =  mWindowManager.getCurrentWindowMetrics().getBounds();
82         mThumbnailLayout = (FrameLayout) LayoutInflater.from(mContext)
83                 .inflate(R.layout.thumbnail_background_view, /* root: */ null);
84         mThumbnailView =
85                 mThumbnailLayout.findViewById(R.id.accessibility_magnification_thumbnail_view);
86         mBackgroundParams = createLayoutParams();
87         mThumbnailWidth = 0;
88         mThumbnailHeight = 0;
89     }
90 
91     /**
92      * Sets the magnificationBounds for Thumbnail and resets the position on the screen.
93      *
94      * @param currentBounds the current magnification bounds
95      */
96     @AnyThread
setThumbnailBounds(Rect currentBounds, float scale, float centerX, float centerY)97     public void setThumbnailBounds(Rect currentBounds, float scale, float centerX, float centerY) {
98         if (DEBUG) {
99             Log.d(LOG_TAG, "setThumbnailBounds " + currentBounds);
100         }
101         mHandler.post(() -> {
102             refreshBackgroundBounds(currentBounds);
103             if (mVisible) {
104                 updateThumbnailMainThread(scale, centerX, centerY);
105             }
106         });
107     }
108 
109     @MainThread
refreshBackgroundBounds(Rect currentBounds)110     private void refreshBackgroundBounds(Rect currentBounds) {
111         mWindowBounds = currentBounds;
112 
113         Point magnificationBoundary = getMagnificationThumbnailPadding(mContext);
114         mThumbnailWidth = (int) (mWindowBounds.width() / BG_ASPECT_RATIO);
115         mThumbnailHeight = (int) (mWindowBounds.height() / BG_ASPECT_RATIO);
116         int initX = magnificationBoundary.x;
117         int initY = magnificationBoundary.y;
118         mBackgroundParams.width = mThumbnailWidth;
119         mBackgroundParams.height = mThumbnailHeight;
120         mBackgroundParams.x = initX;
121         mBackgroundParams.y = initY;
122 
123         if (mVisible) {
124             mWindowManager.updateViewLayout(mThumbnailLayout, mBackgroundParams);
125         }
126     }
127 
128     @MainThread
showThumbnail()129     private void showThumbnail() {
130         if (DEBUG) {
131             Log.d(LOG_TAG, "showThumbnail " + mVisible);
132         }
133         animateThumbnail(true);
134     }
135 
136     /**
137      * Hides thumbnail and removes the view from the window when finished animating.
138      */
139     @AnyThread
hideThumbnail()140     public void hideThumbnail() {
141         mHandler.post(this::hideThumbnailMainThread);
142     }
143 
144     @MainThread
hideThumbnailMainThread()145     private void hideThumbnailMainThread() {
146         if (DEBUG) {
147             Log.d(LOG_TAG, "hideThumbnail " + mVisible);
148         }
149         if (mVisible) {
150             animateThumbnail(false);
151         }
152     }
153 
154     /**
155      * Animates the thumbnail in or out and resets the timeout to auto-hiding.
156      *
157      * @param fadeIn true: fade in, false fade out
158      */
159     @MainThread
animateThumbnail(boolean fadeIn)160     private void animateThumbnail(boolean fadeIn) {
161         if (DEBUG) {
162             Log.d(
163                     LOG_TAG,
164                     "setThumbnailAnimation "
165                         + " fadeIn: " + fadeIn
166                         + " mVisible: " + mVisible
167                         + " isFadingIn: " + mIsFadingIn
168                         + " isRunning: " + mThumbnailAnimator
169             );
170         }
171 
172         // Reset countdown to hide automatically
173         mHandler.removeCallbacks(this::hideThumbnailMainThread);
174         if (fadeIn) {
175             mHandler.postDelayed(this::hideThumbnailMainThread, LINGER_DURATION_MS);
176         }
177 
178         if (fadeIn == mIsFadingIn) {
179             return;
180         }
181         mIsFadingIn = fadeIn;
182 
183         if (fadeIn && !mVisible) {
184             mWindowManager.addView(mThumbnailLayout, mBackgroundParams);
185             mVisible = true;
186         }
187 
188         if (mThumbnailAnimator != null) {
189             mThumbnailAnimator.cancel();
190         }
191         mThumbnailAnimator = ObjectAnimator.ofFloat(
192                 mThumbnailLayout,
193                 "alpha",
194                 fadeIn ? 1f : 0f
195         );
196         mThumbnailAnimator.setDuration(
197                 fadeIn ? FADE_IN_ANIMATION_DURATION_MS : FADE_OUT_ANIMATION_DURATION_MS
198         );
199         mThumbnailAnimator.addListener(new Animator.AnimatorListener() {
200             private boolean mIsCancelled;
201 
202             @Override
203             public void onAnimationStart(@NonNull Animator animation) {
204 
205             }
206 
207             @Override
208             public void onAnimationEnd(@NonNull Animator animation) {
209                 if (DEBUG) {
210                     Log.d(
211                             LOG_TAG,
212                             "onAnimationEnd "
213                                 + " fadeIn: " + fadeIn
214                                 + " mVisible: " + mVisible
215                                 + " mIsCancelled: " + mIsCancelled
216                                 + " animation: " + animation);
217                 }
218                 if (mIsCancelled) {
219                     return;
220                 }
221                 if (!fadeIn && mVisible) {
222                     mWindowManager.removeView(mThumbnailLayout);
223                     mVisible = false;
224                 }
225             }
226 
227             @Override
228             public void onAnimationCancel(@NonNull Animator animation) {
229                 if (DEBUG) {
230                     Log.d(LOG_TAG, "onAnimationCancel "
231                             + " fadeIn: " + fadeIn
232                             + " mVisible: " + mVisible
233                             + " animation: " + animation);
234                 }
235                 mIsCancelled = true;
236             }
237 
238             @Override
239             public void onAnimationRepeat(@NonNull Animator animation) {
240 
241             }
242         });
243 
244         mThumbnailAnimator.start();
245     }
246 
247     /**
248      * Scale up/down the current magnification thumbnail spec.
249      *
250      * <p>Will show/hide the thumbnail with animations when appropriate.
251      *
252      * @param scale the magnification scale
253      * @param centerX the unscaled, screen-relative X coordinate of the center
254      *                of the viewport, or {@link Float#NaN} to leave unchanged
255      * @param centerY the unscaled, screen-relative Y coordinate of the center
256      *                of the viewport, or {@link Float#NaN} to leave unchanged
257      */
258     @AnyThread
updateThumbnail(float scale, float centerX, float centerY)259     public void updateThumbnail(float scale, float centerX, float centerY) {
260         mHandler.post(() -> updateThumbnailMainThread(scale, centerX, centerY));
261     }
262 
263     @MainThread
updateThumbnailMainThread(float scale, float centerX, float centerY)264     private void updateThumbnailMainThread(float scale, float centerX, float centerY) {
265         // Restart the fadeout countdown (or show if it's hidden)
266         showThumbnail();
267 
268         var scaleDown = Float.isNaN(scale) ? mThumbnailView.getScaleX() : 1f / scale;
269         if (!Float.isNaN(scale)) {
270             mThumbnailView.setScaleX(scaleDown);
271             mThumbnailView.setScaleY(scaleDown);
272         }
273 
274         if (!Float.isNaN(centerX)
275                 && !Float.isNaN(centerY)
276                 && mThumbnailWidth > 0
277                 && mThumbnailHeight > 0
278         ) {
279             var padding = mThumbnailView.getPaddingTop();
280             var ratio = 1f / BG_ASPECT_RATIO;
281             var centerXScaled = centerX * ratio - (mThumbnailWidth / 2f + padding);
282             var centerYScaled = centerY * ratio - (mThumbnailHeight / 2f + padding);
283 
284             if (DEBUG) {
285                 Log.d(
286                         LOG_TAG,
287                         "updateThumbnail centerXScaled : " + centerXScaled
288                                 + " centerYScaled : " + centerYScaled
289                                 + " getTranslationX : " + mThumbnailView.getTranslationX()
290                                 + " ratio : " + ratio
291                 );
292             }
293 
294             mThumbnailView.setTranslationX(centerXScaled);
295             mThumbnailView.setTranslationY(centerYScaled);
296         }
297     }
298 
createLayoutParams()299     private WindowManager.LayoutParams createLayoutParams() {
300         WindowManager.LayoutParams params = new WindowManager.LayoutParams(
301                 WindowManager.LayoutParams.WRAP_CONTENT,
302                 WindowManager.LayoutParams.WRAP_CONTENT,
303                 WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY,
304                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
305                     | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
306                 PixelFormat.TRANSPARENT);
307         params.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
308         params.gravity = Gravity.BOTTOM | Gravity.LEFT;
309         params.setFitInsetsTypes(WindowInsets.Type.ime() | WindowInsets.Type.navigationBars());
310         return params;
311     }
312 
getMagnificationThumbnailPadding(Context context)313     private Point getMagnificationThumbnailPadding(Context context) {
314         Point thumbnailPaddings = new Point(0, 0);
315         final int defaultPadding = mContext.getResources()
316                 .getDimensionPixelSize(R.dimen.accessibility_magnification_thumbnail_padding);
317         thumbnailPaddings.x = defaultPadding;
318         thumbnailPaddings.y = defaultPadding;
319         return thumbnailPaddings;
320     }
321 }
322