/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.accessibility.floatingmenu; import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED; import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_ICON_TYPE; import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT; import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY; import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE; import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON; import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets; import static com.android.systemui.Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP; import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.ShapeType; import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.SizeType; import android.annotation.FloatRange; import android.content.Context; import android.database.ContentObserver; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.Prefs; import com.android.systemui.shared.system.SysUiStatsLog; /** * Contains logic for an accessibility floating menu view. */ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { private static final int DEFAULT_FADE_EFFECT_IS_ENABLED = 1; private static final int DEFAULT_MIGRATION_TOOLTIP_PROMPT_IS_DISABLED = 0; @FloatRange(from = 0.0, to = 1.0) private static final float DEFAULT_OPACITY_VALUE = 0.55f; @FloatRange(from = 0.0, to = 1.0) private static final float DEFAULT_POSITION_X_PERCENT = 1.0f; @FloatRange(from = 0.0, to = 1.0) private static final float DEFAULT_POSITION_Y_PERCENT = 0.9f; private final Context mContext; private final AccessibilityFloatingMenuView mMenuView; private final MigrationTooltipView mMigrationTooltipView; private final DockTooltipView mDockTooltipView; private final Handler mHandler = new Handler(Looper.getMainLooper()); private final ContentObserver mContentObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { mMenuView.onTargetsChanged(getTargets(mContext, ACCESSIBILITY_BUTTON)); } }; private final ContentObserver mSizeContentObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { mMenuView.setSizeType(getSizeType(mContext)); } }; private final ContentObserver mFadeOutContentObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { mMenuView.updateOpacityWith(isFadeEffectEnabled(mContext), getOpacityValue(mContext)); } }; private final ContentObserver mEnabledA11yServicesContentObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { mMenuView.onEnabledFeaturesChanged(); } }; public AccessibilityFloatingMenu(Context context) { mContext = context; mMenuView = new AccessibilityFloatingMenuView(context, getPosition(context)); mMigrationTooltipView = new MigrationTooltipView(mContext, mMenuView); mDockTooltipView = new DockTooltipView(mContext, mMenuView); } @VisibleForTesting AccessibilityFloatingMenu(Context context, AccessibilityFloatingMenuView menuView) { mContext = context; mMenuView = menuView; mMigrationTooltipView = new MigrationTooltipView(mContext, mMenuView); mDockTooltipView = new DockTooltipView(mContext, mMenuView); } @Override public boolean isShowing() { return mMenuView.isShowing(); } @Override public void show() { if (isShowing()) { return; } mMenuView.show(); mMenuView.onTargetsChanged(getTargets(mContext, ACCESSIBILITY_BUTTON)); mMenuView.updateOpacityWith(isFadeEffectEnabled(mContext), getOpacityValue(mContext)); mMenuView.setSizeType(getSizeType(mContext)); mMenuView.setShapeType(getShapeType(mContext)); mMenuView.setOnDragEndListener(this::onDragEnd); showMigrationTooltipIfNecessary(); registerContentObservers(); } @Override public void hide() { if (!isShowing()) { return; } mMenuView.hide(); mMenuView.setOnDragEndListener(null); mMigrationTooltipView.hide(); mDockTooltipView.hide(); unregisterContentObservers(); } @NonNull private Position getPosition(Context context) { final String absolutePositionString = Prefs.getString(context, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null); if (TextUtils.isEmpty(absolutePositionString)) { return new Position(DEFAULT_POSITION_X_PERCENT, DEFAULT_POSITION_Y_PERCENT); } else { return Position.fromString(absolutePositionString); } } // Migration tooltip was the android S feature. It's just used on the Android version from R // to S. In addition, it only shows once. private void showMigrationTooltipIfNecessary() { if (isMigrationTooltipPromptEnabled(mContext)) { mMigrationTooltipView.show(); Settings.Secure.putInt(mContext.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT, /* disabled */ 0); } } private static boolean isMigrationTooltipPromptEnabled(Context context) { return Settings.Secure.getInt( context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT, DEFAULT_MIGRATION_TOOLTIP_PROMPT_IS_DISABLED) == /* enabled */ 1; } private void onDragEnd(Position position) { SysUiStatsLog.write(SysUiStatsLog.ACCESSIBILITY_FLOATING_MENU_UI_CHANGED, position.getPercentageX(), position.getPercentageY(), mContext.getResources().getConfiguration().orientation); savePosition(mContext, position); showDockTooltipIfNecessary(mContext); } private void savePosition(Context context, Position position) { Prefs.putString(context, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, position.toString()); } /** * Shows tooltip when user drags accessibility floating menu for the first time. */ private void showDockTooltipIfNecessary(Context context) { if (!Prefs.get(context).getBoolean( HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, false)) { // if the menu is an oval, the user has already dragged it out, so show the tooltip. if (mMenuView.isOvalShape()) { mDockTooltipView.show(); } Prefs.putBoolean(context, HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, true); } } private static boolean isFadeEffectEnabled(Context context) { return Settings.Secure.getInt( context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED, DEFAULT_FADE_EFFECT_IS_ENABLED) == /* enabled */ 1; } private static float getOpacityValue(Context context) { return Settings.Secure.getFloat( context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_OPACITY, DEFAULT_OPACITY_VALUE); } private static int getSizeType(Context context) { return Settings.Secure.getInt( context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_SIZE, SizeType.SMALL); } private static int getShapeType(Context context) { return Settings.Secure.getInt( context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_ICON_TYPE, ShapeType.OVAL); } private void registerContentObservers() { mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS), /* notifyForDescendants */ false, mContentObserver, UserHandle.USER_CURRENT); mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE), /* notifyForDescendants */ false, mSizeContentObserver, UserHandle.USER_CURRENT); mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED), /* notifyForDescendants */ false, mFadeOutContentObserver, UserHandle.USER_CURRENT); mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY), /* notifyForDescendants */ false, mFadeOutContentObserver, UserHandle.USER_CURRENT); mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES), /* notifyForDescendants */ false, mEnabledA11yServicesContentObserver, UserHandle.USER_CURRENT); } private void unregisterContentObservers() { mContext.getContentResolver().unregisterContentObserver(mContentObserver); mContext.getContentResolver().unregisterContentObserver(mSizeContentObserver); mContext.getContentResolver().unregisterContentObserver(mFadeOutContentObserver); mContext.getContentResolver().unregisterContentObserver( mEnabledA11yServicesContentObserver); } }