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.server.accessibility.magnification;
18 
19 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL;
20 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
21 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE;
22 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.content.Context;
27 import android.graphics.PointF;
28 import android.graphics.Rect;
29 import android.graphics.Region;
30 import android.os.SystemClock;
31 import android.provider.Settings;
32 import android.util.Slog;
33 import android.util.SparseArray;
34 import android.view.accessibility.MagnificationAnimationCallback;
35 
36 import com.android.internal.accessibility.util.AccessibilityStatsLogUtils;
37 import com.android.internal.annotations.GuardedBy;
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.server.accessibility.AccessibilityManagerService;
40 
41 /**
42  * Handles all magnification controllers initialization, generic interactions,
43  * magnification mode transition and magnification switch UI show/hide logic
44  * in the following callbacks:
45  *
46  * <ol>
47  *   <li> 1. {@link #onTouchInteractionStart} shows magnification switch UI when
48  *   the user touch interaction starts if magnification capabilities is all. </li>
49  *   <li> 2. {@link #onTouchInteractionEnd} shows magnification switch UI when
50  *   the user touch interaction ends if magnification capabilities is all. </li>
51  *   <li> 3. {@link #onShortcutTriggered} updates magnification switch UI depending on
52  *   magnification capabilities and magnification active state when magnification shortcut
53  *   is triggered.</li>
54  *   <li> 4. {@link #onTripleTapped} updates magnification switch UI depending on magnification
55  *   capabilities and magnification active state when triple-tap gesture is detected. </li>
56  *   <li> 4. {@link #onRequestMagnificationSpec} updates magnification switch UI depending on
57  *   magnification capabilities and magnification active state when new magnification spec is
58  *   changed by external request from calling public APIs. </li>
59  * </ol>
60  *
61  *  <b>Note</b> Updates magnification switch UI when magnification mode transition
62  *  is done and before invoking {@link TransitionCallBack#onResult}.
63  */
64 public class MagnificationController implements WindowMagnificationManager.Callback,
65         MagnificationGestureHandler.Callback,
66         FullScreenMagnificationController.MagnificationInfoChangedCallback {
67 
68     private static final boolean DEBUG = false;
69     private static final String TAG = "MagnificationController";
70     private final AccessibilityManagerService mAms;
71     private final PointF mTempPoint = new PointF();
72     private final Object mLock;
73     private final Context mContext;
74     private final SparseArray<DisableMagnificationCallback>
75             mMagnificationEndRunnableSparseArray = new SparseArray();
76 
77     private FullScreenMagnificationController mFullScreenMagnificationController;
78     private WindowMagnificationManager mWindowMagnificationMgr;
79     private int mMagnificationCapabilities = ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
80 
81     @GuardedBy("mLock")
82     private int mActivatedMode = ACCESSIBILITY_MAGNIFICATION_MODE_NONE;
83     @GuardedBy("mLock")
84     private boolean mImeWindowVisible = false;
85     private long mWindowModeEnabledTime = 0;
86     private long mFullScreenModeEnabledTime = 0;
87 
88     /**
89      * A callback to inform the magnification transition result.
90      */
91     public interface TransitionCallBack {
92         /**
93          * Invoked when the transition ends.
94          * @param success {@code true} if the transition success.
95          */
onResult(boolean success)96         void onResult(boolean success);
97     }
98 
MagnificationController(AccessibilityManagerService ams, Object lock, Context context)99     public MagnificationController(AccessibilityManagerService ams, Object lock,
100             Context context) {
101         mAms = ams;
102         mLock = lock;
103         mContext = context;
104     }
105 
106     @VisibleForTesting
MagnificationController(AccessibilityManagerService ams, Object lock, Context context, FullScreenMagnificationController fullScreenMagnificationController, WindowMagnificationManager windowMagnificationManager)107     public MagnificationController(AccessibilityManagerService ams, Object lock,
108             Context context, FullScreenMagnificationController fullScreenMagnificationController,
109             WindowMagnificationManager windowMagnificationManager) {
110         this(ams, lock, context);
111         mFullScreenMagnificationController = fullScreenMagnificationController;
112         mWindowMagnificationMgr = windowMagnificationManager;
113     }
114 
115     @Override
onPerformScaleAction(int displayId, float scale)116     public void onPerformScaleAction(int displayId, float scale) {
117         getWindowMagnificationMgr().setScale(displayId, scale);
118         getWindowMagnificationMgr().persistScale(displayId);
119     }
120 
121     @Override
onAccessibilityActionPerformed(int displayId)122     public void onAccessibilityActionPerformed(int displayId) {
123         updateMagnificationButton(displayId, ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
124     }
125 
126     @Override
onTouchInteractionStart(int displayId, int mode)127     public void onTouchInteractionStart(int displayId, int mode) {
128         handleUserInteractionChanged(displayId, mode);
129     }
130 
131     @Override
onTouchInteractionEnd(int displayId, int mode)132     public void onTouchInteractionEnd(int displayId, int mode) {
133         handleUserInteractionChanged(displayId, mode);
134     }
135 
handleUserInteractionChanged(int displayId, int mode)136     private void handleUserInteractionChanged(int displayId, int mode) {
137         if (mMagnificationCapabilities != Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL) {
138             return;
139         }
140         if (isActivated(displayId, mode)) {
141             getWindowMagnificationMgr().showMagnificationButton(displayId, mode);
142         }
143     }
144 
145     @Override
onShortcutTriggered(int displayId, int mode)146     public void onShortcutTriggered(int displayId, int mode) {
147         updateMagnificationButton(displayId, mode);
148     }
149 
150     @Override
onTripleTapped(int displayId, int mode)151     public void onTripleTapped(int displayId, int mode) {
152         updateMagnificationButton(displayId, mode);
153     }
154 
updateMagnificationButton(int displayId, int mode)155     private void updateMagnificationButton(int displayId, int mode) {
156         final boolean isActivated = isActivated(displayId, mode);
157         final boolean showButton;
158         synchronized (mLock) {
159             showButton = isActivated && mMagnificationCapabilities
160                     == Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL;
161         }
162         if (showButton) {
163             getWindowMagnificationMgr().showMagnificationButton(displayId, mode);
164         } else {
165             getWindowMagnificationMgr().removeMagnificationButton(displayId);
166         }
167     }
168 
169     /**
170      * Transitions to the target Magnification mode with current center of the magnification mode
171      * if it is available.
172      *
173      * @param displayId The logical display
174      * @param targetMode The target magnification mode
175      * @param transitionCallBack The callback invoked when the transition is finished.
176      */
transitionMagnificationModeLocked(int displayId, int targetMode, @NonNull TransitionCallBack transitionCallBack)177     public void transitionMagnificationModeLocked(int displayId, int targetMode,
178             @NonNull TransitionCallBack transitionCallBack) {
179         final PointF magnificationCenter = getCurrentMagnificationBoundsCenterLocked(displayId,
180                 targetMode);
181         final DisableMagnificationCallback animationCallback =
182                 getDisableMagnificationEndRunnableLocked(displayId);
183         if (magnificationCenter == null && animationCallback == null) {
184             transitionCallBack.onResult(true);
185             return;
186         }
187 
188         if (animationCallback != null) {
189             if (animationCallback.mCurrentMode == targetMode) {
190                 animationCallback.restoreToCurrentMagnificationMode();
191                 return;
192             }
193             Slog.w(TAG, "request during transition, abandon current:"
194                     + animationCallback.mTargetMode);
195             animationCallback.setExpiredAndRemoveFromListLocked();
196         }
197 
198         if (magnificationCenter == null) {
199             Slog.w(TAG, "Invalid center, ignore it");
200             transitionCallBack.onResult(true);
201             return;
202         }
203         final FullScreenMagnificationController screenMagnificationController =
204                 getFullScreenMagnificationController();
205         final WindowMagnificationManager windowMagnificationMgr = getWindowMagnificationMgr();
206         final float scale = windowMagnificationMgr.getPersistedScale();
207         final DisableMagnificationCallback animationEndCallback =
208                 new DisableMagnificationCallback(transitionCallBack, displayId, targetMode,
209                         scale, magnificationCenter);
210         if (targetMode == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW) {
211             screenMagnificationController.reset(displayId, animationEndCallback);
212         } else {
213             windowMagnificationMgr.disableWindowMagnification(displayId, false,
214                     animationEndCallback);
215         }
216         setDisableMagnificationCallbackLocked(displayId, animationEndCallback);
217     }
218 
219     @Override
onRequestMagnificationSpec(int displayId, int serviceId)220     public void onRequestMagnificationSpec(int displayId, int serviceId) {
221         final WindowMagnificationManager windowMagnificationManager;
222         synchronized (mLock) {
223             if (serviceId == AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID) {
224                 return;
225             }
226             updateMagnificationButton(displayId, ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
227             windowMagnificationManager = mWindowMagnificationMgr;
228         }
229         if (windowMagnificationManager != null) {
230             mWindowMagnificationMgr.disableWindowMagnification(displayId, false);
231         }
232     }
233 
234     // TODO : supporting multi-display (b/182227245).
235     @Override
onWindowMagnificationActivationState(int displayId, boolean activated)236     public void onWindowMagnificationActivationState(int displayId, boolean activated) {
237         if (activated) {
238             mWindowModeEnabledTime = SystemClock.uptimeMillis();
239 
240             synchronized (mLock) {
241                 mActivatedMode = ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
242             }
243             logMagnificationModeWithImeOnIfNeeded();
244             disableFullScreenMagnificationIfNeeded(displayId);
245         } else {
246             logMagnificationUsageState(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW,
247                     SystemClock.uptimeMillis() - mWindowModeEnabledTime);
248 
249             synchronized (mLock) {
250                 mActivatedMode = ACCESSIBILITY_MAGNIFICATION_MODE_NONE;
251             }
252         }
253     }
254 
disableFullScreenMagnificationIfNeeded(int displayId)255     private void disableFullScreenMagnificationIfNeeded(int displayId) {
256         final FullScreenMagnificationController fullScreenMagnificationController =
257                 getFullScreenMagnificationController();
258         // Internal request may be for transition, so we just need to check external request.
259         final boolean isMagnifyByExternalRequest =
260                 fullScreenMagnificationController.getIdOfLastServiceToMagnify(displayId) > 0;
261         if (isMagnifyByExternalRequest) {
262             fullScreenMagnificationController.reset(displayId, false);
263         }
264     }
265 
266     @Override
onFullScreenMagnificationActivationState(boolean activated)267     public void onFullScreenMagnificationActivationState(boolean activated) {
268         if (activated) {
269             mFullScreenModeEnabledTime = SystemClock.uptimeMillis();
270 
271             synchronized (mLock) {
272                 mActivatedMode = ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
273             }
274             logMagnificationModeWithImeOnIfNeeded();
275         } else {
276             logMagnificationUsageState(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN,
277                     SystemClock.uptimeMillis() - mFullScreenModeEnabledTime);
278 
279             synchronized (mLock) {
280                 mActivatedMode = ACCESSIBILITY_MAGNIFICATION_MODE_NONE;
281             }
282         }
283     }
284 
285     @Override
onImeWindowVisibilityChanged(boolean shown)286     public void onImeWindowVisibilityChanged(boolean shown) {
287         synchronized (mLock) {
288             mImeWindowVisible = shown;
289         }
290         logMagnificationModeWithImeOnIfNeeded();
291     }
292 
293     /**
294      * Wrapper method of logging the magnification activated mode and its duration of the usage
295      * when the magnification is disabled.
296      *
297      * @param mode The activated magnification mode.
298      * @param duration The duration in milliseconds during the magnification is activated.
299      */
300     @VisibleForTesting
logMagnificationUsageState(int mode, long duration)301     public void logMagnificationUsageState(int mode, long duration) {
302         AccessibilityStatsLogUtils.logMagnificationUsageState(mode, duration);
303     }
304 
305     /**
306      * Wrapper method of logging the activated mode of the magnification when the IME window
307      * is shown on the screen.
308      *
309      * @param mode The activated magnification mode.
310      */
311     @VisibleForTesting
logMagnificationModeWithIme(int mode)312     public void logMagnificationModeWithIme(int mode) {
313         AccessibilityStatsLogUtils.logMagnificationModeWithImeOn(mode);
314     }
315 
316     /**
317      * Updates the active user ID of {@link FullScreenMagnificationController} and {@link
318      * WindowMagnificationManager}.
319      *
320      * @param userId the currently active user ID
321      */
updateUserIdIfNeeded(int userId)322     public void updateUserIdIfNeeded(int userId) {
323         synchronized (mLock) {
324             if (mFullScreenMagnificationController != null) {
325                 mFullScreenMagnificationController.setUserId(userId);
326             }
327             if (mWindowMagnificationMgr != null) {
328                 mWindowMagnificationMgr.setUserId(userId);
329             }
330         }
331     }
332 
333     /**
334      * Removes the magnification instance with given id.
335      *
336      * @param displayId The logical display id.
337      */
onDisplayRemoved(int displayId)338     public void onDisplayRemoved(int displayId) {
339         synchronized (mLock) {
340             if (mFullScreenMagnificationController != null) {
341                 mFullScreenMagnificationController.onDisplayRemoved(displayId);
342             }
343             if (mWindowMagnificationMgr != null) {
344                 mWindowMagnificationMgr.onDisplayRemoved(displayId);
345             }
346         }
347     }
348 
setMagnificationCapabilities(int capabilities)349     public void setMagnificationCapabilities(int capabilities) {
350         mMagnificationCapabilities = capabilities;
351     }
352 
getDisableMagnificationEndRunnableLocked( int displayId)353     private DisableMagnificationCallback getDisableMagnificationEndRunnableLocked(
354             int displayId) {
355         return mMagnificationEndRunnableSparseArray.get(displayId);
356     }
357 
setDisableMagnificationCallbackLocked(int displayId, @Nullable DisableMagnificationCallback callback)358     private void setDisableMagnificationCallbackLocked(int displayId,
359             @Nullable DisableMagnificationCallback callback) {
360         mMagnificationEndRunnableSparseArray.put(displayId, callback);
361         if (DEBUG) {
362             Slog.d(TAG, "setDisableMagnificationCallbackLocked displayId = " + displayId
363                     + ", callback = " + callback);
364         }
365     }
366 
logMagnificationModeWithImeOnIfNeeded()367     private void logMagnificationModeWithImeOnIfNeeded() {
368         final int mode;
369 
370         synchronized (mLock) {
371             if (!mImeWindowVisible || mActivatedMode == ACCESSIBILITY_MAGNIFICATION_MODE_NONE) {
372                 return;
373             }
374             mode = mActivatedMode;
375         }
376         logMagnificationModeWithIme(mode);
377     }
378 
379     /**
380      * Getter of {@link FullScreenMagnificationController}.
381      *
382      * @return {@link FullScreenMagnificationController}.
383      */
getFullScreenMagnificationController()384     public FullScreenMagnificationController getFullScreenMagnificationController() {
385         synchronized (mLock) {
386             if (mFullScreenMagnificationController == null) {
387                 mFullScreenMagnificationController = new FullScreenMagnificationController(mContext,
388                         mAms, mLock, this);
389                 mFullScreenMagnificationController.setUserId(mAms.getCurrentUserIdLocked());
390             }
391         }
392         return mFullScreenMagnificationController;
393     }
394 
395     /**
396      * Is {@link #mFullScreenMagnificationController} is initialized.
397      * @return {code true} if {@link #mFullScreenMagnificationController} is initialized.
398      */
isFullScreenMagnificationControllerInitialized()399     public boolean isFullScreenMagnificationControllerInitialized() {
400         synchronized (mLock) {
401             return mFullScreenMagnificationController != null;
402         }
403     }
404 
405     /**
406      * Getter of {@link WindowMagnificationManager}.
407      *
408      * @return {@link WindowMagnificationManager}.
409      */
getWindowMagnificationMgr()410     public WindowMagnificationManager getWindowMagnificationMgr() {
411         synchronized (mLock) {
412             if (mWindowMagnificationMgr == null) {
413                 mWindowMagnificationMgr = new WindowMagnificationManager(mContext,
414                         mAms.getCurrentUserIdLocked(), this, mAms.getTraceManager());
415             }
416             return mWindowMagnificationMgr;
417         }
418     }
419 
420     private @Nullable
getCurrentMagnificationBoundsCenterLocked(int displayId, int targetMode)421             PointF getCurrentMagnificationBoundsCenterLocked(int displayId, int targetMode) {
422         if (targetMode == ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN) {
423             if (mWindowMagnificationMgr == null
424                     || !mWindowMagnificationMgr.isWindowMagnifierEnabled(displayId)) {
425                 return null;
426             }
427             mTempPoint.set(mWindowMagnificationMgr.getCenterX(displayId),
428                     mWindowMagnificationMgr.getCenterY(displayId));
429         } else {
430             if (mFullScreenMagnificationController == null
431                     || !mFullScreenMagnificationController.isMagnifying(displayId)) {
432                 return null;
433             }
434             mTempPoint.set(mFullScreenMagnificationController.getCenterX(displayId),
435                     mFullScreenMagnificationController.getCenterY(displayId));
436         }
437         return mTempPoint;
438     }
439 
isActivated(int displayId, int mode)440     private boolean isActivated(int displayId, int mode) {
441         boolean isActivated = false;
442         if (mode == ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN) {
443             synchronized (mLock) {
444                 if (mFullScreenMagnificationController == null) {
445                     return false;
446                 }
447                 isActivated = mFullScreenMagnificationController.isMagnifying(displayId)
448                         || mFullScreenMagnificationController.isForceShowMagnifiableBounds(
449                         displayId);
450             }
451         } else if (mode == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW) {
452             synchronized (mLock) {
453                 if (mWindowMagnificationMgr == null) {
454                     return false;
455                 }
456                 isActivated = mWindowMagnificationMgr.isWindowMagnifierEnabled(displayId);
457             }
458         }
459         return isActivated;
460     }
461 
462     private final class DisableMagnificationCallback implements
463             MagnificationAnimationCallback {
464         private final TransitionCallBack mTransitionCallBack;
465         private boolean mExpired = false;
466         private final int mDisplayId;
467         private final int mTargetMode;
468         private final int mCurrentMode;
469         private final float mCurrentScale;
470         private final PointF mCurrentCenter = new PointF();
471 
DisableMagnificationCallback(TransitionCallBack transitionCallBack, int displayId, int targetMode, float scale, PointF currentCenter)472         DisableMagnificationCallback(TransitionCallBack transitionCallBack,
473                 int displayId, int targetMode, float scale, PointF currentCenter) {
474             mTransitionCallBack = transitionCallBack;
475             mDisplayId = displayId;
476             mTargetMode = targetMode;
477             mCurrentMode = mTargetMode ^ ACCESSIBILITY_MAGNIFICATION_MODE_ALL;
478             mCurrentScale = scale;
479             mCurrentCenter.set(currentCenter);
480         }
481 
482         @Override
onResult(boolean success)483         public void onResult(boolean success) {
484             synchronized (mLock) {
485                 if (DEBUG) {
486                     Slog.d(TAG, "onResult success = " + success);
487                 }
488                 if (mExpired) {
489                     return;
490                 }
491                 setExpiredAndRemoveFromListLocked();
492                 if (success) {
493                     adjustCurrentCenterIfNeededLocked();
494                     applyMagnificationModeLocked(mTargetMode);
495                 }
496                 updateMagnificationButton(mDisplayId, mTargetMode);
497                 mTransitionCallBack.onResult(success);
498             }
499         }
500 
adjustCurrentCenterIfNeededLocked()501         private void adjustCurrentCenterIfNeededLocked() {
502             if (mTargetMode == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW) {
503                 return;
504             }
505             final Region outRegion = new Region();
506             getFullScreenMagnificationController().getMagnificationRegion(mDisplayId, outRegion);
507             if (outRegion.contains((int) mCurrentCenter.x, (int) mCurrentCenter.y)) {
508                 return;
509             }
510             final Rect bounds = outRegion.getBounds();
511             mCurrentCenter.set(bounds.exactCenterX(), bounds.exactCenterY());
512         }
513 
restoreToCurrentMagnificationMode()514         void restoreToCurrentMagnificationMode() {
515             synchronized (mLock) {
516                 if (mExpired) {
517                     return;
518                 }
519                 setExpiredAndRemoveFromListLocked();
520                 applyMagnificationModeLocked(mCurrentMode);
521                 updateMagnificationButton(mDisplayId, mCurrentMode);
522                 mTransitionCallBack.onResult(true);
523             }
524         }
525 
setExpiredAndRemoveFromListLocked()526         void setExpiredAndRemoveFromListLocked() {
527             mExpired = true;
528             setDisableMagnificationCallbackLocked(mDisplayId, null);
529         }
530 
applyMagnificationModeLocked(int mode)531         private void applyMagnificationModeLocked(int mode) {
532             if (mode == ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN) {
533                 getFullScreenMagnificationController().setScaleAndCenter(mDisplayId,
534                         mCurrentScale, mCurrentCenter.x,
535                         mCurrentCenter.y, true,
536                         AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
537             } else {
538                 getWindowMagnificationMgr().enableWindowMagnification(mDisplayId,
539                         mCurrentScale, mCurrentCenter.x,
540                         mCurrentCenter.y);
541             }
542         }
543     }
544 }
545