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