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.floatingmenu;
18 
19 import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED;
20 import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT;
21 import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY;
22 import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE;
23 import static android.provider.Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES;
24 
25 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
26 import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
27 import static com.android.systemui.accessibility.floatingmenu.MenuFadeEffectInfoKt.DEFAULT_FADE_EFFECT_IS_ENABLED;
28 import static com.android.systemui.accessibility.floatingmenu.MenuFadeEffectInfoKt.DEFAULT_OPACITY_VALUE;
29 import static com.android.systemui.accessibility.floatingmenu.MenuViewAppearance.MenuSizeType.SMALL;
30 
31 import android.annotation.FloatRange;
32 import android.annotation.IntDef;
33 import android.content.ComponentCallbacks;
34 import android.content.Context;
35 import android.content.pm.ActivityInfo;
36 import android.content.res.Configuration;
37 import android.database.ContentObserver;
38 import android.os.Build;
39 import android.os.Handler;
40 import android.os.Looper;
41 import android.os.UserHandle;
42 import android.provider.Settings;
43 import android.text.TextUtils;
44 import android.util.Log;
45 import android.view.View;
46 import android.view.accessibility.AccessibilityManager;
47 
48 import androidx.annotation.NonNull;
49 
50 import com.android.internal.accessibility.dialog.AccessibilityTarget;
51 import com.android.internal.annotations.VisibleForTesting;
52 import com.android.systemui.Prefs;
53 import com.android.systemui.util.settings.SecureSettings;
54 
55 import java.lang.annotation.Retention;
56 import java.lang.annotation.RetentionPolicy;
57 import java.util.List;
58 
59 /**
60  * Stores and observe the settings contents for the menu view.
61  */
62 class MenuInfoRepository {
63     private static final String TAG = "MenuInfoRepository";
64     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG) || Build.IS_DEBUGGABLE;
65 
66     @FloatRange(from = 0.0, to = 1.0)
67     private static final float DEFAULT_MENU_POSITION_X_PERCENT = 1.0f;
68 
69     @FloatRange(from = 0.0, to = 1.0)
70     private static final float DEFAULT_MENU_POSITION_X_PERCENT_RTL = 0.0f;
71 
72     @FloatRange(from = 0.0, to = 1.0)
73     private static final float DEFAULT_MENU_POSITION_Y_PERCENT = 0.77f;
74     private static final boolean DEFAULT_MOVE_TO_TUCKED_VALUE = false;
75     private static final boolean DEFAULT_HAS_SEEN_DOCK_TOOLTIP_VALUE = false;
76     private static final int DEFAULT_MIGRATION_TOOLTIP_VALUE_PROMPT = MigrationPrompt.DISABLED;
77 
78     private final Context mContext;
79     private final Configuration mConfiguration;
80     private final AccessibilityManager mAccessibilityManager;
81     private final AccessibilityManager.AccessibilityServicesStateChangeListener
82             mA11yServicesStateChangeListener = manager -> onTargetFeaturesChanged();
83     private final Handler mHandler = new Handler(Looper.getMainLooper());
84     private final OnSettingsContentsChanged mSettingsContentsCallback;
85     private final SecureSettings mSecureSettings;
86     private Position mPercentagePosition;
87 
88     @IntDef({
89             MigrationPrompt.DISABLED,
90             MigrationPrompt.ENABLED,
91     })
92     @Retention(RetentionPolicy.SOURCE)
93     @interface MigrationPrompt {
94         int DISABLED = 0;
95         int ENABLED = 1;
96     }
97 
98     @VisibleForTesting
99     final ContentObserver mMenuTargetFeaturesContentObserver =
100             new ContentObserver(mHandler) {
101                 @Override
102                 public void onChange(boolean selfChange) {
103                     onTargetFeaturesChanged();
104                 }
105             };
106 
107     @VisibleForTesting
108     final ContentObserver mMenuSizeContentObserver =
109             new ContentObserver(mHandler) {
110                 @Override
111                 public void onChange(boolean selfChange) {
112                     mSettingsContentsCallback.onSizeTypeChanged(
113                             getMenuSizeTypeFromSettings());
114                 }
115             };
116 
117     @VisibleForTesting
118     final ContentObserver mMenuFadeOutContentObserver =
119             new ContentObserver(mHandler) {
120                 @Override
121                 public void onChange(boolean selfChange) {
122                     mSettingsContentsCallback.onFadeEffectInfoChanged(getMenuFadeEffectInfo());
123                 }
124             };
125 
126     @VisibleForTesting
127     final ComponentCallbacks mComponentCallbacks = new ComponentCallbacks() {
128         @Override
129         public void onConfigurationChanged(@NonNull Configuration newConfig) {
130             final int diff = newConfig.diff(mConfiguration);
131 
132             if (DEBUG) {
133                 Log.d(TAG, "onConfigurationChanged = " + Configuration.configurationDiffToString(
134                         diff));
135             }
136 
137             if ((diff & ActivityInfo.CONFIG_LOCALE) != 0) {
138                 onTargetFeaturesChanged();
139             }
140 
141             mConfiguration.setTo(newConfig);
142         }
143 
144         @Override
145         public void onLowMemory() {
146             // Do nothing.
147         }
148     };
149 
MenuInfoRepository(Context context, AccessibilityManager accessibilityManager, OnSettingsContentsChanged settingsContentsChanged, SecureSettings secureSettings)150     MenuInfoRepository(Context context, AccessibilityManager accessibilityManager,
151             OnSettingsContentsChanged settingsContentsChanged, SecureSettings secureSettings) {
152         mContext = context;
153         mAccessibilityManager = accessibilityManager;
154         mConfiguration = new Configuration(context.getResources().getConfiguration());
155         mSettingsContentsCallback = settingsContentsChanged;
156         mSecureSettings = secureSettings;
157 
158         mPercentagePosition = getStartPosition();
159     }
160 
loadMenuMoveToTucked(OnInfoReady<Boolean> callback)161     void loadMenuMoveToTucked(OnInfoReady<Boolean> callback) {
162         callback.onReady(
163                 Prefs.getBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED,
164                         DEFAULT_MOVE_TO_TUCKED_VALUE));
165     }
166 
loadDockTooltipVisibility(OnInfoReady<Boolean> callback)167     void loadDockTooltipVisibility(OnInfoReady<Boolean> callback) {
168         callback.onReady(Prefs.getBoolean(mContext,
169                 Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP,
170                 DEFAULT_HAS_SEEN_DOCK_TOOLTIP_VALUE));
171     }
172 
loadMigrationTooltipVisibility(OnInfoReady<Boolean> callback)173     void loadMigrationTooltipVisibility(OnInfoReady<Boolean> callback) {
174         callback.onReady(mSecureSettings.getIntForUser(
175                 ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
176                 DEFAULT_MIGRATION_TOOLTIP_VALUE_PROMPT, UserHandle.USER_CURRENT)
177                 == MigrationPrompt.ENABLED);
178     }
179 
loadMenuPosition(OnInfoReady<Position> callback)180     void loadMenuPosition(OnInfoReady<Position> callback) {
181         callback.onReady(mPercentagePosition);
182     }
183 
loadMenuTargetFeatures(OnInfoReady<List<AccessibilityTarget>> callback)184     void loadMenuTargetFeatures(OnInfoReady<List<AccessibilityTarget>> callback) {
185         callback.onReady(getTargets(mContext, SOFTWARE));
186     }
187 
loadMenuSizeType(OnInfoReady<Integer> callback)188     void loadMenuSizeType(OnInfoReady<Integer> callback) {
189         callback.onReady(getMenuSizeTypeFromSettings());
190     }
191 
loadMenuFadeEffectInfo(OnInfoReady<MenuFadeEffectInfo> callback)192     void loadMenuFadeEffectInfo(OnInfoReady<MenuFadeEffectInfo> callback) {
193         callback.onReady(getMenuFadeEffectInfo());
194     }
195 
getMenuFadeEffectInfo()196     private MenuFadeEffectInfo getMenuFadeEffectInfo() {
197         return new MenuFadeEffectInfo(isMenuFadeEffectEnabledFromSettings(),
198                 getMenuOpacityFromSettings());
199     }
200 
updateMoveToTucked(boolean isMoveToTucked)201     void updateMoveToTucked(boolean isMoveToTucked) {
202         Prefs.putBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED,
203                 isMoveToTucked);
204     }
205 
updateMenuSavingPosition(Position percentagePosition)206     void updateMenuSavingPosition(Position percentagePosition) {
207         mPercentagePosition = percentagePosition;
208         Prefs.putString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION,
209                 percentagePosition.toString());
210     }
211 
updateDockTooltipVisibility(boolean hasSeen)212     void updateDockTooltipVisibility(boolean hasSeen) {
213         Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP,
214                 hasSeen);
215     }
216 
updateMigrationTooltipVisibility(boolean visible)217     void updateMigrationTooltipVisibility(boolean visible) {
218         mSecureSettings.putIntForUser(
219                 ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
220                 visible ? MigrationPrompt.ENABLED : MigrationPrompt.DISABLED,
221                 UserHandle.USER_CURRENT);
222     }
223 
onTargetFeaturesChanged()224     private void onTargetFeaturesChanged() {
225         mSettingsContentsCallback.onTargetFeaturesChanged(
226                 getTargets(mContext, SOFTWARE));
227     }
228 
getStartPosition()229     private Position getStartPosition() {
230         final String absolutePositionString = Prefs.getString(mContext,
231                 Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
232 
233         final float defaultPositionXPercent =
234                 mConfiguration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
235                         ? DEFAULT_MENU_POSITION_X_PERCENT_RTL
236                         : DEFAULT_MENU_POSITION_X_PERCENT;
237         return TextUtils.isEmpty(absolutePositionString)
238                 ? new Position(defaultPositionXPercent, DEFAULT_MENU_POSITION_Y_PERCENT)
239                 : Position.fromString(absolutePositionString);
240     }
241 
registerObserversAndCallbacks()242     void registerObserversAndCallbacks() {
243         mSecureSettings.registerContentObserverForUserSync(
244                 mSecureSettings.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS),
245                 /* notifyForDescendants */ false, mMenuTargetFeaturesContentObserver,
246                 UserHandle.USER_CURRENT);
247         if (!com.android.systemui.Flags.floatingMenuNarrowTargetContentObserver()) {
248             mSecureSettings.registerContentObserverForUserSync(
249                     mSecureSettings.getUriFor(ENABLED_ACCESSIBILITY_SERVICES),
250                     /* notifyForDescendants */ false,
251                     mMenuTargetFeaturesContentObserver,
252                     UserHandle.USER_CURRENT);
253         }
254         mSecureSettings.registerContentObserverForUserSync(
255                 mSecureSettings.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE),
256                 /* notifyForDescendants */ false, mMenuSizeContentObserver,
257                 UserHandle.USER_CURRENT);
258         mSecureSettings.registerContentObserverForUserSync(
259                 mSecureSettings.getUriFor(ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED),
260                 /* notifyForDescendants */ false, mMenuFadeOutContentObserver,
261                 UserHandle.USER_CURRENT);
262         mSecureSettings.registerContentObserverForUserSync(
263                 mSecureSettings.getUriFor(ACCESSIBILITY_FLOATING_MENU_OPACITY),
264                 /* notifyForDescendants */ false, mMenuFadeOutContentObserver,
265                 UserHandle.USER_CURRENT);
266         mContext.registerComponentCallbacks(mComponentCallbacks);
267 
268         if (!com.android.systemui.Flags.floatingMenuNarrowTargetContentObserver()) {
269             mAccessibilityManager.addAccessibilityServicesStateChangeListener(
270                     mA11yServicesStateChangeListener);
271         }
272     }
273 
unregisterObserversAndCallbacks()274     void unregisterObserversAndCallbacks() {
275         mContext.getContentResolver().unregisterContentObserver(mMenuTargetFeaturesContentObserver);
276         mContext.getContentResolver().unregisterContentObserver(mMenuSizeContentObserver);
277         mContext.getContentResolver().unregisterContentObserver(mMenuFadeOutContentObserver);
278         mContext.unregisterComponentCallbacks(mComponentCallbacks);
279 
280         if (!com.android.systemui.Flags.floatingMenuNarrowTargetContentObserver()) {
281             mAccessibilityManager.removeAccessibilityServicesStateChangeListener(
282                     mA11yServicesStateChangeListener);
283         }
284     }
285 
286     interface OnSettingsContentsChanged {
onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures)287         void onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures);
288 
onSizeTypeChanged(int newSizeType)289         void onSizeTypeChanged(int newSizeType);
290 
onFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo)291         void onFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo);
292     }
293 
294     interface OnInfoReady<T> {
onReady(T info)295         void onReady(T info);
296     }
297 
getMenuSizeTypeFromSettings()298     private int getMenuSizeTypeFromSettings() {
299         return mSecureSettings.getIntForUser(
300                 ACCESSIBILITY_FLOATING_MENU_SIZE, SMALL, UserHandle.USER_CURRENT);
301     }
302 
isMenuFadeEffectEnabledFromSettings()303     private boolean isMenuFadeEffectEnabledFromSettings() {
304         return mSecureSettings.getIntForUser(
305                 ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
306                 DEFAULT_FADE_EFFECT_IS_ENABLED, UserHandle.USER_CURRENT) == /* enabled */ 1;
307     }
308 
getMenuOpacityFromSettings()309     private float getMenuOpacityFromSettings() {
310         return mSecureSettings.getFloatForUser(
311                 ACCESSIBILITY_FLOATING_MENU_OPACITY, DEFAULT_OPACITY_VALUE,
312                 UserHandle.USER_CURRENT);
313     }
314 }
315