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