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