1 /*
2  * Copyright (C) 2022 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 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_WINDOW;
22 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
23 
24 import static com.android.internal.accessibility.common.MagnificationConstants.SCALE_MAX_VALUE;
25 import static com.android.internal.accessibility.common.MagnificationConstants.SCALE_MIN_VALUE;
26 
27 import android.annotation.IntDef;
28 import android.content.BroadcastReceiver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.content.pm.ActivityInfo;
33 import android.database.ContentObserver;
34 import android.graphics.Insets;
35 import android.graphics.PixelFormat;
36 import android.graphics.Rect;
37 import android.os.Bundle;
38 import android.os.UserHandle;
39 import android.provider.Settings;
40 import android.util.MathUtils;
41 import android.view.Gravity;
42 import android.view.MotionEvent;
43 import android.view.View;
44 import android.view.View.AccessibilityDelegate;
45 import android.view.ViewGroup;
46 import android.view.WindowInsets;
47 import android.view.WindowManager;
48 import android.view.WindowManager.LayoutParams;
49 import android.view.WindowMetrics;
50 import android.view.accessibility.AccessibilityManager;
51 import android.view.accessibility.AccessibilityNodeInfo;
52 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
53 import android.widget.Button;
54 import android.widget.ImageButton;
55 import android.widget.LinearLayout;
56 import android.widget.SeekBar;
57 import android.widget.Switch;
58 import android.widget.TextView;
59 
60 import com.android.internal.annotations.VisibleForTesting;
61 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
62 import com.android.systemui.R;
63 import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView;
64 import com.android.systemui.util.settings.SecureSettings;
65 
66 import java.lang.annotation.Retention;
67 import java.lang.annotation.RetentionPolicy;
68 import java.util.Collections;
69 
70 /**
71  * Class to set value about WindowManificationSettings.
72  */
73 class WindowMagnificationSettings implements MagnificationGestureDetector.OnGestureListener {
74     private static final String TAG = "WindowMagnificationSettings";
75     private final Context mContext;
76     private final AccessibilityManager mAccessibilityManager;
77     private final WindowManager mWindowManager;
78     private final SecureSettings mSecureSettings;
79 
80     private final Runnable mWindowInsetChangeRunnable;
81     private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider;
82 
83     @VisibleForTesting
84     final LayoutParams mParams;
85     @VisibleForTesting
86     final Rect mDraggableWindowBounds = new Rect();
87     private boolean mIsVisible = false;
88     private final MagnificationGestureDetector mGestureDetector;
89     private boolean mSingleTapDetected = false;
90 
91     private SeekBarWithIconButtonsView mZoomSeekbar;
92     private LinearLayout mAllowDiagonalScrollingView;
93     private TextView mAllowDiagonalScrollingTitle;
94     private Switch mAllowDiagonalScrollingSwitch;
95     private LinearLayout mPanelView;
96     private LinearLayout mSettingView;
97     private ImageButton mSmallButton;
98     private ImageButton mMediumButton;
99     private ImageButton mLargeButton;
100     private Button mDoneButton;
101     private Button mEditButton;
102     private ImageButton mFullScreenButton;
103     private int mLastSelectedButtonIndex = MagnificationSize.NONE;
104     private boolean mAllowDiagonalScrolling = false;
105     /**
106      * Amount by which magnification scale changes compared to seekbar in settings.
107      * magnitude = 10 means, for every 1 scale increase, 10 progress increase in seekbar.
108      */
109     private int mSeekBarMagnitude;
110     private float mScale = SCALE_MIN_VALUE;
111 
112     private WindowMagnificationSettingsCallback mCallback;
113 
114     private ContentObserver mMagnificationCapabilityObserver;
115 
116     @Retention(RetentionPolicy.SOURCE)
117     @IntDef({
118             MagnificationSize.NONE,
119             MagnificationSize.SMALL,
120             MagnificationSize.MEDIUM,
121             MagnificationSize.LARGE,
122             MagnificationSize.FULLSCREEN
123     })
124     /** Denotes the Magnification size type. */
125     public @interface MagnificationSize {
126         int NONE = 0;
127         int SMALL = 1;
128         int MEDIUM = 2;
129         int LARGE = 3;
130         int FULLSCREEN = 4;
131     }
132 
133     @VisibleForTesting
WindowMagnificationSettings(Context context, WindowMagnificationSettingsCallback callback, SfVsyncFrameCallbackProvider sfVsyncFrameProvider, SecureSettings secureSettings)134     WindowMagnificationSettings(Context context, WindowMagnificationSettingsCallback callback,
135             SfVsyncFrameCallbackProvider sfVsyncFrameProvider, SecureSettings secureSettings) {
136         mContext = context;
137         mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
138         mWindowManager = mContext.getSystemService(WindowManager.class);
139         mSfVsyncFrameProvider = sfVsyncFrameProvider;
140         mCallback = callback;
141         mSecureSettings = secureSettings;
142 
143         mAllowDiagonalScrolling = mSecureSettings.getIntForUser(
144                 Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, 0,
145                 UserHandle.USER_CURRENT) == 1;
146 
147         mParams = createLayoutParams(context);
148         mWindowInsetChangeRunnable = this::onWindowInsetChanged;
149 
150         inflateView();
151 
152         mGestureDetector = new MagnificationGestureDetector(context,
153                 context.getMainThreadHandler(), this);
154 
155         mMagnificationCapabilityObserver = new ContentObserver(
156                 mContext.getMainThreadHandler()) {
157             @Override
158             public void onChange(boolean selfChange) {
159                 mSettingView.post(() -> {
160                     updateUIControlsIfNeeded();
161                 });
162             }
163         };
164     }
165 
166     private class ZoomSeekbarChangeListener implements
167             SeekBarWithIconButtonsView.OnSeekBarWithIconButtonsChangeListener {
168         @Override
onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)169         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
170             // Notify the service to update the magnifier scale only when the progress changed is
171             // triggered by user interaction on seekbar
172             if (fromUser) {
173                 final float scale = transformProgressToScale(progress);
174                 // We don't need to update the persisted scale when the seekbar progress is
175                 // changing. The update should be triggered when the changing is ended.
176                 mCallback.onMagnifierScale(scale, /* updatePersistence= */ false);
177             }
178         }
179 
180         @Override
onStartTrackingTouch(SeekBar seekBar)181         public void onStartTrackingTouch(SeekBar seekBar) {
182             // Do nothing
183         }
184 
185         @Override
onStopTrackingTouch(SeekBar seekBar)186         public void onStopTrackingTouch(SeekBar seekBar) {
187             // Do nothing
188         }
189 
190         @Override
onUserInteractionFinalized(SeekBar seekBar, @ControlUnitType int control)191         public void onUserInteractionFinalized(SeekBar seekBar, @ControlUnitType int control) {
192             // Update the Settings persisted scale only when user interaction with seekbar ends
193             final int progress = seekBar.getProgress();
194             final float scale = transformProgressToScale(progress);
195             mCallback.onMagnifierScale(scale, /* updatePersistence= */ true);
196         }
197 
transformProgressToScale(float progress)198         private float transformProgressToScale(float progress) {
199             return (progress / (float) mSeekBarMagnitude) + SCALE_MIN_VALUE;
200         }
201     }
202 
203     private final AccessibilityDelegate mPanelDelegate = new AccessibilityDelegate() {
204         @Override
205         public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
206             super.onInitializeAccessibilityNodeInfo(host, info);
207 
208             info.addAction(new AccessibilityAction(R.id.accessibility_action_move_up,
209                     mContext.getString(R.string.accessibility_control_move_up)));
210             info.addAction(new AccessibilityAction(R.id.accessibility_action_move_down,
211                     mContext.getString(R.string.accessibility_control_move_down)));
212             info.addAction(new AccessibilityAction(R.id.accessibility_action_move_left,
213                     mContext.getString(R.string.accessibility_control_move_left)));
214             info.addAction(new AccessibilityAction(R.id.accessibility_action_move_right,
215                     mContext.getString(R.string.accessibility_control_move_right)));
216         }
217 
218         @Override
219         public boolean performAccessibilityAction(View host, int action, Bundle args) {
220             if (performA11yAction(host, action)) {
221                 return true;
222             }
223             return super.performAccessibilityAction(host, action, args);
224         }
225 
226         private boolean performA11yAction(View view, int action) {
227             final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
228             if (action == R.id.accessibility_action_move_up) {
229                 moveButton(0, -windowBounds.height());
230             } else if (action == R.id.accessibility_action_move_down) {
231                 moveButton(0, windowBounds.height());
232             } else if (action == R.id.accessibility_action_move_left) {
233                 moveButton(-windowBounds.width(), 0);
234             } else if (action == R.id.accessibility_action_move_right) {
235                 moveButton(windowBounds.width(), 0);
236             } else {
237                 return false;
238             }
239             return true;
240         }
241     };
242 
onTouch(View v, MotionEvent event)243     private boolean onTouch(View v, MotionEvent event) {
244         if (!mIsVisible) {
245             return false;
246         }
247         return mGestureDetector.onTouch(v, event);
248     }
249 
250     private View.OnClickListener mButtonClickListener = new View.OnClickListener() {
251         @Override
252         public void onClick(View view) {
253             int id = view.getId();
254             if (id == R.id.magnifier_small_button) {
255                 setMagnifierSize(MagnificationSize.SMALL);
256             } else if (id == R.id.magnifier_medium_button) {
257                 setMagnifierSize(MagnificationSize.MEDIUM);
258             } else if (id == R.id.magnifier_large_button) {
259                 setMagnifierSize(MagnificationSize.LARGE);
260             } else if (id == R.id.magnifier_full_button) {
261                 setMagnifierSize(MagnificationSize.FULLSCREEN);
262             } else if (id == R.id.magnifier_edit_button) {
263                 editMagnifierSizeMode(true);
264             } else if (id == R.id.magnifier_done_button) {
265                 hideSettingPanel();
266             }
267         }
268     };
269 
270     @Override
onSingleTap(View view)271     public boolean onSingleTap(View view) {
272         mSingleTapDetected = true;
273         return true;
274     }
275 
276     @Override
onDrag(View v, float offsetX, float offsetY)277     public boolean onDrag(View v, float offsetX, float offsetY) {
278         moveButton(offsetX, offsetY);
279         return true;
280     }
281 
282     @Override
onStart(float x, float y)283     public boolean onStart(float x, float y) {
284         return true;
285     }
286 
287     @Override
onFinish(float xOffset, float yOffset)288     public boolean onFinish(float xOffset, float yOffset) {
289         if (!mSingleTapDetected) {
290             showSettingPanel();
291         }
292         mSingleTapDetected = false;
293         return true;
294     }
295 
296     @VisibleForTesting
getSettingView()297     public ViewGroup getSettingView() {
298         return mSettingView;
299     }
300 
moveButton(float offsetX, float offsetY)301     private void moveButton(float offsetX, float offsetY) {
302         mSfVsyncFrameProvider.postFrameCallback(l -> {
303             mParams.x += offsetX;
304             mParams.y += offsetY;
305             updateButtonViewLayoutIfNeeded();
306         });
307     }
308 
hideSettingPanel()309     public void hideSettingPanel() {
310         hideSettingPanel(true);
311     }
312 
hideSettingPanel(boolean resetPosition)313     public void hideSettingPanel(boolean resetPosition) {
314         if (!mIsVisible) {
315             return;
316         }
317 
318         // Unregister observer before removing view
319         mSecureSettings.unregisterContentObserver(mMagnificationCapabilityObserver);
320         mWindowManager.removeView(mSettingView);
321         mIsVisible = false;
322         if (resetPosition) {
323             mParams.x = 0;
324             mParams.y = 0;
325         }
326 
327         mContext.unregisterReceiver(mScreenOffReceiver);
328         mCallback.onSettingsPanelVisibilityChanged(/* shown= */ false);
329     }
330 
toggleSettingsPanelVisibility()331     public void toggleSettingsPanelVisibility() {
332         if (!mIsVisible) {
333             showSettingPanel();
334         } else {
335             hideSettingPanel();
336         }
337     }
338 
showSettingPanel()339     public void showSettingPanel() {
340         showSettingPanel(true);
341     }
342 
isSettingPanelShowing()343     public boolean isSettingPanelShowing() {
344         return mIsVisible;
345     }
346 
setScaleSeekbar(float scale)347     public void setScaleSeekbar(float scale) {
348         int index = (int) ((scale - SCALE_MIN_VALUE) * mSeekBarMagnitude);
349         if (index < 0) {
350             index = 0;
351         } else if (index > mZoomSeekbar.getMax()) {
352             index = mZoomSeekbar.getMax();
353         }
354         mZoomSeekbar.setProgress(index);
355     }
356 
transitToMagnificationMode(int mode)357     private void transitToMagnificationMode(int mode) {
358         mCallback.onModeSwitch(mode);
359     }
360 
361     /**
362      * Shows the panel for magnification settings.
363      * When the panel is going to be visible by calling this method, the layout position can be
364      * reset depending on the flag.
365      *
366      * @param resetPosition if the panel position needs to be reset
367      */
showSettingPanel(boolean resetPosition)368     private void showSettingPanel(boolean resetPosition) {
369         if (!mIsVisible) {
370             updateUIControlsIfNeeded();
371             setScaleSeekbar(mScale);
372             if (resetPosition) {
373                 mDraggableWindowBounds.set(getDraggableWindowBounds());
374                 mParams.x = mDraggableWindowBounds.right;
375                 mParams.y = mDraggableWindowBounds.bottom;
376             }
377 
378             mWindowManager.addView(mSettingView, mParams);
379 
380             mSecureSettings.registerContentObserverForUser(
381                     Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
382                     mMagnificationCapabilityObserver,
383                     UserHandle.USER_CURRENT);
384 
385             // Exclude magnification switch button from system gesture area.
386             setSystemGestureExclusion();
387             mIsVisible = true;
388             mCallback.onSettingsPanelVisibilityChanged(/* shown= */ true);
389 
390             if (resetPosition) {
391                 // We could not put focus on the settings panel automatically
392                 // since it is an inactive window. Therefore, we announce the existence of
393                 // magnification settings for accessibility when it is opened.
394                 mSettingView.announceForAccessibility(
395                         mContext.getResources().getString(
396                                 R.string.accessibility_magnification_settings_panel_description));
397             }
398         }
399         mContext.registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
400     }
401 
getMagnificationMode()402     private int getMagnificationMode() {
403         // If current capability is window mode, we would like the default value of the mode to
404         // be WINDOW, otherwise, the default value would be FULLSCREEN.
405         int defaultValue =
406                 (getMagnificationCapability() == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW)
407                         ? ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW
408                         : ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
409 
410         return mSecureSettings.getIntForUser(
411                 Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
412                 defaultValue,
413                 UserHandle.USER_CURRENT);
414     }
415 
getMagnificationCapability()416     private int getMagnificationCapability() {
417         return mSecureSettings.getIntForUser(
418                 Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
419                 ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN,
420                 UserHandle.USER_CURRENT);
421     }
422 
423     /**
424      * Only called from outside to notify the controlling magnifier scale changed
425      *
426      * @param scale The new controlling magnifier scale
427      */
setMagnificationScale(float scale)428     public void setMagnificationScale(float scale) {
429         mScale = scale;
430 
431         if (isSettingPanelShowing()) {
432             setScaleSeekbar(scale);
433         }
434     }
435 
updateUIControlsIfNeeded()436     private void updateUIControlsIfNeeded() {
437         int capability = getMagnificationCapability();
438         int selectedButtonIndex = mLastSelectedButtonIndex;
439         switch (capability) {
440             case ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW:
441                 mEditButton.setVisibility(View.VISIBLE);
442                 mAllowDiagonalScrollingView.setVisibility(View.VISIBLE);
443                 mFullScreenButton.setVisibility(View.GONE);
444                 if (selectedButtonIndex == MagnificationSize.FULLSCREEN) {
445                     selectedButtonIndex = MagnificationSize.NONE;
446                 }
447                 break;
448 
449             case ACCESSIBILITY_MAGNIFICATION_MODE_ALL:
450                 int mode = getMagnificationMode();
451                 mFullScreenButton.setVisibility(View.VISIBLE);
452                 if (mode == ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN) {
453                     // set the edit button visibility to View.INVISIBLE to keep the height, to
454                     // prevent the size title from too close to the size buttons
455                     mEditButton.setVisibility(View.INVISIBLE);
456                     mAllowDiagonalScrollingView.setVisibility(View.GONE);
457                     // force the fullscreen button showing
458                     selectedButtonIndex = MagnificationSize.FULLSCREEN;
459                 } else { // mode = ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW
460                     mEditButton.setVisibility(View.VISIBLE);
461                     mAllowDiagonalScrollingView.setVisibility(View.VISIBLE);
462                 }
463                 break;
464 
465             case ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN:
466                 // We will never fall into this case since we never show settings panel when
467                 // capability equals to ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN.
468                 // Currently, the case follows the UI controls when capability equals to
469                 // ACCESSIBILITY_MAGNIFICATION_MODE_ALL and mode equals to
470                 // ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN, but we could also consider to
471                 // remove the whole icon button selections int the future since they are no use
472                 // for fullscreen only capability.
473 
474                 mFullScreenButton.setVisibility(View.VISIBLE);
475                 // set the edit button visibility to View.INVISIBLE to keep the height, to
476                 // prevent the size title from too close to the size buttons
477                 mEditButton.setVisibility(View.INVISIBLE);
478                 mAllowDiagonalScrollingView.setVisibility(View.GONE);
479                 // force the fullscreen button showing
480                 selectedButtonIndex = MagnificationSize.FULLSCREEN;
481                 break;
482 
483             default:
484                 break;
485         }
486 
487         updateSelectedButton(selectedButtonIndex);
488         mSettingView.requestLayout();
489     }
490 
491     private final BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() {
492         @Override
493         public void onReceive(Context context, Intent intent) {
494             hideSettingPanel();
495         }
496     };
497 
inflateView()498     void inflateView() {
499         mSettingView = (LinearLayout) View.inflate(mContext,
500                 R.layout.window_magnification_settings_view, null);
501 
502         mSettingView.setFocusable(true);
503         mSettingView.setFocusableInTouchMode(true);
504         mSettingView.setOnTouchListener(this::onTouch);
505 
506         mSettingView.setAccessibilityDelegate(mPanelDelegate);
507 
508         mPanelView = mSettingView.findViewById(R.id.magnifier_panel_view);
509         mSmallButton = mSettingView.findViewById(R.id.magnifier_small_button);
510         mMediumButton = mSettingView.findViewById(R.id.magnifier_medium_button);
511         mLargeButton = mSettingView.findViewById(R.id.magnifier_large_button);
512         mDoneButton = mSettingView.findViewById(R.id.magnifier_done_button);
513         mEditButton = mSettingView.findViewById(R.id.magnifier_edit_button);
514         mFullScreenButton = mSettingView.findViewById(R.id.magnifier_full_button);
515         mAllowDiagonalScrollingTitle =
516                 mSettingView.findViewById(R.id.magnifier_horizontal_lock_title);
517 
518         mZoomSeekbar = mSettingView.findViewById(R.id.magnifier_zoom_slider);
519         mZoomSeekbar.setMax((int) (mZoomSeekbar.getChangeMagnitude()
520                 * (SCALE_MAX_VALUE - SCALE_MIN_VALUE)));
521         mSeekBarMagnitude = mZoomSeekbar.getChangeMagnitude();
522         setScaleSeekbar(mScale);
523         mZoomSeekbar.setOnSeekBarWithIconButtonsChangeListener(new ZoomSeekbarChangeListener());
524 
525         mAllowDiagonalScrollingView =
526                 (LinearLayout) mSettingView.findViewById(R.id.magnifier_horizontal_lock_view);
527         mAllowDiagonalScrollingSwitch =
528                 (Switch) mSettingView.findViewById(R.id.magnifier_horizontal_lock_switch);
529         mAllowDiagonalScrollingSwitch.setChecked(mAllowDiagonalScrolling);
530         mAllowDiagonalScrollingSwitch.setOnCheckedChangeListener((view, checked) -> {
531             toggleDiagonalScrolling();
532         });
533 
534         mSmallButton.setOnClickListener(mButtonClickListener);
535         mMediumButton.setOnClickListener(mButtonClickListener);
536         mLargeButton.setOnClickListener(mButtonClickListener);
537         mDoneButton.setOnClickListener(mButtonClickListener);
538         mFullScreenButton.setOnClickListener(mButtonClickListener);
539         mEditButton.setOnClickListener(mButtonClickListener);
540         mAllowDiagonalScrollingTitle.setSelected(true);
541 
542         mSettingView.setOnApplyWindowInsetsListener((v, insets) -> {
543             // Adds a pending post check to avoiding redundant calculation because this callback
544             // is sent frequently when the switch icon window dragged by the users.
545             if (mSettingView.isAttachedToWindow()
546                     && !mSettingView.getHandler().hasCallbacks(mWindowInsetChangeRunnable)) {
547                 mSettingView.getHandler().post(mWindowInsetChangeRunnable);
548             }
549             return v.onApplyWindowInsets(insets);
550         });
551 
552         updateSelectedButton(mLastSelectedButtonIndex);
553     }
554 
onConfigurationChanged(int configDiff)555     void onConfigurationChanged(int configDiff) {
556         if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0
557                 || (configDiff & ActivityInfo.CONFIG_ASSETS_PATHS) != 0
558                 || (configDiff & ActivityInfo.CONFIG_FONT_SCALE) != 0
559                 || (configDiff & ActivityInfo.CONFIG_LOCALE) != 0
560                 || (configDiff & ActivityInfo.CONFIG_DENSITY) != 0) {
561             // We listen to following config changes to trigger layout inflation:
562             // CONFIG_UI_MODE: theme change
563             // CONFIG_ASSETS_PATHS: wallpaper change
564             // CONFIG_FONT_SCALE: font size change
565             // CONFIG_LOCALE: language change
566             // CONFIG_DENSITY: display size change
567             mParams.accessibilityTitle = getAccessibilityWindowTitle(mContext);
568 
569             boolean showSettingPanelAfterConfigChange = mIsVisible;
570             hideSettingPanel(/* resetPosition= */ false);
571             inflateView();
572             if (showSettingPanelAfterConfigChange) {
573                 showSettingPanel(/* resetPosition= */ false);
574             }
575             return;
576         }
577 
578         if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0
579                 || (configDiff & ActivityInfo.CONFIG_SCREEN_SIZE) != 0) {
580             mDraggableWindowBounds.set(getDraggableWindowBounds());
581             // reset the panel position to the right-bottom corner
582             mParams.x = mDraggableWindowBounds.right;
583             mParams.y = mDraggableWindowBounds.bottom;
584             updateButtonViewLayoutIfNeeded();
585         }
586     }
587 
onWindowInsetChanged()588     private void onWindowInsetChanged() {
589         final Rect newBounds = getDraggableWindowBounds();
590         if (mDraggableWindowBounds.equals(newBounds)) {
591             return;
592         }
593         mDraggableWindowBounds.set(newBounds);
594     }
595 
596     @VisibleForTesting
updateButtonViewLayoutIfNeeded()597     void updateButtonViewLayoutIfNeeded() {
598         if (mIsVisible) {
599             mParams.x = MathUtils.constrain(mParams.x, mDraggableWindowBounds.left,
600                     mDraggableWindowBounds.right);
601             mParams.y = MathUtils.constrain(mParams.y, mDraggableWindowBounds.top,
602                     mDraggableWindowBounds.bottom);
603             mWindowManager.updateViewLayout(mSettingView, mParams);
604         }
605     }
606 
editMagnifierSizeMode(boolean enable)607     public void editMagnifierSizeMode(boolean enable) {
608         setEditMagnifierSizeMode(enable);
609         updateSelectedButton(MagnificationSize.NONE);
610         hideSettingPanel();
611     }
612 
setMagnifierSize(@agnificationSize int index)613     private void setMagnifierSize(@MagnificationSize int index) {
614         if (index == MagnificationSize.FULLSCREEN) {
615             // transit to fullscreen magnifier if needed
616             transitToMagnificationMode(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
617         } else if (index != MagnificationSize.NONE) {
618             // update the window magnifier size
619             mCallback.onSetMagnifierSize(index);
620             // transit to window magnifier if needed
621             transitToMagnificationMode(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
622         } else {
623             return;
624         }
625 
626         updateSelectedButton(index);
627     }
628 
toggleDiagonalScrolling()629     private void toggleDiagonalScrolling() {
630         boolean enabled = mSecureSettings.getIntForUser(
631                 Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, 0,
632                 UserHandle.USER_CURRENT) == 1;
633         setDiagonalScrolling(!enabled);
634     }
635 
636     @VisibleForTesting
setDiagonalScrolling(boolean enabled)637     void setDiagonalScrolling(boolean enabled) {
638         mSecureSettings.putIntForUser(
639                 Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, enabled ? 1 : 0,
640                 UserHandle.USER_CURRENT);
641 
642         mCallback.onSetDiagonalScrolling(enabled);
643     }
644 
setEditMagnifierSizeMode(boolean enable)645     private void setEditMagnifierSizeMode(boolean enable) {
646         mCallback.onEditMagnifierSizeMode(enable);
647     }
648 
createLayoutParams(Context context)649     private static LayoutParams createLayoutParams(Context context) {
650         final LayoutParams params = new LayoutParams(
651                 LayoutParams.WRAP_CONTENT,
652                 LayoutParams.WRAP_CONTENT,
653                 LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
654                 LayoutParams.FLAG_NOT_FOCUSABLE,
655                 PixelFormat.TRANSPARENT);
656         params.gravity = Gravity.TOP | Gravity.START;
657         params.accessibilityTitle = getAccessibilityWindowTitle(context);
658         params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
659         return params;
660     }
661 
getDraggableWindowBounds()662     private Rect getDraggableWindowBounds() {
663         final int layoutMargin = mContext.getResources().getDimensionPixelSize(
664                 R.dimen.magnification_switch_button_margin);
665         final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
666         final Insets windowInsets = windowMetrics.getWindowInsets().getInsetsIgnoringVisibility(
667                 WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
668         final Rect boundRect = new Rect(windowMetrics.getBounds());
669         boundRect.offsetTo(0, 0);
670         boundRect.inset(0, 0, mParams.width, mParams.height);
671         boundRect.inset(windowInsets);
672         boundRect.inset(layoutMargin, layoutMargin);
673 
674         return boundRect;
675     }
676 
getAccessibilityWindowTitle(Context context)677     private static String getAccessibilityWindowTitle(Context context) {
678         return context.getString(com.android.internal.R.string.android_system_label);
679     }
680 
setSystemGestureExclusion()681     private void setSystemGestureExclusion() {
682         mSettingView.post(() -> {
683             mSettingView.setSystemGestureExclusionRects(
684                     Collections.singletonList(
685                             new Rect(0, 0, mSettingView.getWidth(), mSettingView.getHeight())));
686         });
687     }
688 
updateSelectedButton(@agnificationSize int index)689     private void updateSelectedButton(@MagnificationSize int index) {
690         // Clear the state of last selected button
691         if (mLastSelectedButtonIndex == MagnificationSize.SMALL) {
692             mSmallButton.setSelected(false);
693         } else if (mLastSelectedButtonIndex == MagnificationSize.MEDIUM) {
694             mMediumButton.setSelected(false);
695         } else if (mLastSelectedButtonIndex == MagnificationSize.LARGE) {
696             mLargeButton.setSelected(false);
697         } else if (mLastSelectedButtonIndex == MagnificationSize.FULLSCREEN) {
698             mFullScreenButton.setSelected(false);
699         }
700 
701         // Set the state for selected button
702         if (index == MagnificationSize.SMALL) {
703             mSmallButton.setSelected(true);
704         } else if (index == MagnificationSize.MEDIUM) {
705             mMediumButton.setSelected(true);
706         } else if (index == MagnificationSize.LARGE) {
707             mLargeButton.setSelected(true);
708         } else if (index == MagnificationSize.FULLSCREEN) {
709             mFullScreenButton.setSelected(true);
710         }
711 
712         mLastSelectedButtonIndex = index;
713     }
714 }
715