diff --git a/aconfig/accessibility/accessibility_flags.aconfig b/aconfig/accessibility/accessibility_flags.aconfig index 7f1f909812d..1c03a270ada 100644 --- a/aconfig/accessibility/accessibility_flags.aconfig +++ b/aconfig/accessibility/accessibility_flags.aconfig @@ -2,9 +2,17 @@ package: "com.android.settings.accessibility" # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. +flag { + name: "edit_shortcuts_in_full_screen" + namespace: "accessibility" + description: "Show the edit shorcuts screen in full screen, since the shortcut options are increasing." + bug: "300302098" +} + + flag { name: "remove_qs_tooltip_in_suw" namespace: "accessibility" description: "Don't show quick settings tooltip in SUW, since the user can't use quick settings there." bug: "294560581" -} \ No newline at end of file +} diff --git a/res/layout/accessibility_shortcut_option_checkable.xml b/res/layout/accessibility_shortcut_option_checkable.xml new file mode 100644 index 00000000000..a5c55950f77 --- /dev/null +++ b/res/layout/accessibility_shortcut_option_checkable.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/accessibility_shortcut_keys.xml b/res/values/accessibility_shortcut_keys.xml new file mode 100644 index 00000000000..0d409a8304a --- /dev/null +++ b/res/values/accessibility_shortcut_keys.xml @@ -0,0 +1,26 @@ + + + + + shortcut_volume_keys_pref + shortcut_gesture_pref + shortcut_nav_button_pref + shortcut_fab_pref + shortcut_triple_tap_pref + shortcut_two_fingers_double_tap_pref + advanced_shortcuts_collapsed + \ No newline at end of file diff --git a/res/xml/accessibility_edit_shortcuts.xml b/res/xml/accessibility_edit_shortcuts.xml new file mode 100644 index 00000000000..06cbeddc6ab --- /dev/null +++ b/res/xml/accessibility_edit_shortcuts.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java b/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java index a50e00b0f85..81f3fa8b742 100644 --- a/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java +++ b/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java @@ -44,6 +44,7 @@ import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType; +import com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragment; import com.android.settings.dashboard.RestrictedDashboardFragment; import com.android.settings.utils.LocaleUtils; @@ -251,7 +252,17 @@ public abstract class AccessibilityShortcutPreferenceFragment extends Restricted @Override public void onSettingsClicked(ShortcutPreference preference) { - showDialog(DialogEnums.EDIT_SHORTCUT); + if (Flags.editShortcutsInFullScreen()) { + EditShortcutsPreferenceFragment.showEditShortcutScreen( + getContext(), + getMetricsCategory(), + getShortcutTitle(), + getComponentName(), + getIntent() + ); + } else { + showDialog(DialogEnums.EDIT_SHORTCUT); + } } @Override diff --git a/src/com/android/settings/accessibility/ExpandablePreference.java b/src/com/android/settings/accessibility/ExpandablePreference.java new file mode 100644 index 00000000000..b84c0c11b0b --- /dev/null +++ b/src/com/android/settings/accessibility/ExpandablePreference.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 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.settings.accessibility; + +/** + * An interface denoting a preference is expandable + */ +public interface ExpandablePreference { + + /** + * Set the expandable state of the preference + */ + void setExpanded(boolean expanded); + + /** + * Returns if the preference is currently expanded. + */ + boolean isExpanded(); +} diff --git a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java index dd6c1d476d2..f31209899bc 100644 --- a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java @@ -48,6 +48,7 @@ import androidx.annotation.Nullable; import com.android.settings.R; import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType; import com.android.settings.accessibility.AccessibilityUtil.UserShortcutType; +import com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragment; import com.android.settingslib.accessibility.AccessibilityUtils; import java.util.List; @@ -539,7 +540,17 @@ public class ToggleAccessibilityServicePreferenceFragment extends private void onAllowButtonFromShortcutClicked() { mIsDialogShown.set(false); - showPopupDialog(DialogEnums.EDIT_SHORTCUT); + if (Flags.editShortcutsInFullScreen()) { + EditShortcutsPreferenceFragment.showEditShortcutScreen( + getContext(), + getMetricsCategory(), + getShortcutTitle(), + mComponentName, + getIntent() + ); + } else { + showPopupDialog(DialogEnums.EDIT_SHORTCUT); + } if (mWarningDialog != null) { mWarningDialog.dismiss(); diff --git a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java index c76bb8bc006..fc0acfeb00a 100644 --- a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java @@ -59,6 +59,7 @@ import com.android.settings.SettingsActivity; import com.android.settings.accessibility.AccessibilityDialogUtils.DialogType; import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType; import com.android.settings.accessibility.AccessibilityUtil.UserShortcutType; +import com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragment; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.flags.Flags; import com.android.settings.utils.LocaleUtils; @@ -832,7 +833,13 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment @Override public void onSettingsClicked(ShortcutPreference preference) { - showDialog(DialogEnums.EDIT_SHORTCUT); + if (com.android.settings.accessibility.Flags.editShortcutsInFullScreen()) { + EditShortcutsPreferenceFragment.showEditShortcutScreen( + requireContext(), getMetricsCategory(), getShortcutTitle(), + mComponentName, getIntent()); + } else { + showDialog(DialogEnums.EDIT_SHORTCUT); + } } /** diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index 1d946fbd3b1..b9481945467 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -16,6 +16,7 @@ package com.android.settings.accessibility; +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; import static com.android.settings.accessibility.AccessibilityDialogUtils.DialogEnums; import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; @@ -55,6 +56,7 @@ import com.android.settings.R; import com.android.settings.accessibility.AccessibilityDialogUtils.DialogType; import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType; import com.android.settings.accessibility.AccessibilityUtil.UserShortcutType; +import com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragment; import com.android.settings.utils.LocaleUtils; import com.android.settingslib.core.AbstractPreferenceController; @@ -584,7 +586,16 @@ public class ToggleScreenMagnificationPreferenceFragment extends @Override public void onSettingsClicked(ShortcutPreference preference) { - showDialog(DialogEnums.MAGNIFICATION_EDIT_SHORTCUT); + if (com.android.settings.accessibility.Flags.editShortcutsInFullScreen()) { + EditShortcutsPreferenceFragment.showEditShortcutScreen( + requireContext(), + getMetricsCategory(), + getShortcutTitle(), + MAGNIFICATION_COMPONENT_NAME, + getIntent()); + } else { + showDialog(DialogEnums.MAGNIFICATION_EDIT_SHORTCUT); + } } @Override diff --git a/src/com/android/settings/accessibility/shortcuts/AdvancedShortcutsPreferenceController.java b/src/com/android/settings/accessibility/shortcuts/AdvancedShortcutsPreferenceController.java new file mode 100644 index 00000000000..61be82fd2ec --- /dev/null +++ b/src/com/android/settings/accessibility/shortcuts/AdvancedShortcutsPreferenceController.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; + +import android.content.Context; + +import com.android.settings.accessibility.ExpandablePreference; + +import java.util.Set; + +/** + * A preference controller that controls an expandable preference that wraps + * the advanced shortcut options. + */ +public class AdvancedShortcutsPreferenceController extends ShortcutOptionPreferenceController + implements ExpandablePreference { + + private boolean mIsExpanded = false; + + public AdvancedShortcutsPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + protected boolean isChecked() { + return false; + } + + @Override + protected void enableShortcutForTargets(boolean enable) { + // do nothing + } + + @Override + public int getAvailabilityStatus() { + if (!isExpanded() && isShortcutAvailable()) { + // "Advanced" is available when the user hasn't clicked on it + return AVAILABLE_UNSEARCHABLE; + } + return CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void setExpanded(boolean expanded) { + mIsExpanded = expanded; + } + + @Override + public boolean isExpanded() { + return mIsExpanded; + } + + @Override + protected boolean isShortcutAvailable() { + // Only Magnification has advanced shortcut options. + Set shortcutTargets = getShortcutTargets(); + return shortcutTargets.size() == 1 + && shortcutTargets.contains(MAGNIFICATION_CONTROLLER_NAME); + } +} diff --git a/src/com/android/settings/accessibility/shortcuts/EditShortcutsPreferenceFragment.java b/src/com/android/settings/accessibility/shortcuts/EditShortcutsPreferenceFragment.java new file mode 100644 index 00000000000..70b3148a2a2 --- /dev/null +++ b/src/com/android/settings/accessibility/shortcuts/EditShortcutsPreferenceFragment.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE; +import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS; +import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED; +import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED; +import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; + +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME; +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; + +import android.app.settings.SettingsEnums; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.Settings; +import android.text.TextUtils; +import android.view.accessibility.AccessibilityManager; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.SetupWizardUtils; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settingslib.core.AbstractPreferenceController; + +import com.google.android.setupcompat.util.WizardManagerHelper; + +import java.util.Collection; +import java.util.Set; + +/** + * A screen show various accessibility shortcut options for the given a11y feature + */ +public class EditShortcutsPreferenceFragment extends DashboardFragment { + private static final String TAG = "EditShortcutsPreferenceFragment"; + + @VisibleForTesting + static final String ARG_KEY_SHORTCUT_TARGETS = "targets"; + @VisibleForTesting + static final String SAVED_STATE_IS_EXPANDED = "isExpanded"; + private ContentObserver mSettingsObserver; + + private static final Uri VOLUME_KEYS_SHORTCUT_SETTING = + Settings.Secure.getUriFor(ACCESSIBILITY_SHORTCUT_TARGET_SERVICE); + private static final Uri BUTTON_SHORTCUT_MODE_SETTING = + Settings.Secure.getUriFor(ACCESSIBILITY_BUTTON_MODE); + private static final Uri BUTTON_SHORTCUT_SETTING = + Settings.Secure.getUriFor(ACCESSIBILITY_BUTTON_TARGETS); + + private static final Uri TRIPLE_TAP_SHORTCUT_SETTING = + Settings.Secure.getUriFor(ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED); + private static final Uri TWO_FINGERS_DOUBLE_TAP_SHORTCUT_SETTING = + Settings.Secure.getUriFor(ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED); + + @VisibleForTesting + static final Uri[] SHORTCUT_SETTINGS = { + VOLUME_KEYS_SHORTCUT_SETTING, + BUTTON_SHORTCUT_MODE_SETTING, + BUTTON_SHORTCUT_SETTING, + TRIPLE_TAP_SHORTCUT_SETTING, + TWO_FINGERS_DOUBLE_TAP_SHORTCUT_SETTING, + }; + + private Set mShortcutTargets; + + @Nullable + private AccessibilityManager.TouchExplorationStateChangeListener + mTouchExplorationStateChangeListener; + + + /** + * Helper method to show the edit shortcut screen + */ + public static void showEditShortcutScreen( + Context context, int metricsCategory, CharSequence screenTitle, + ComponentName target, Intent fromIntent) { + Bundle args = new Bundle(); + + if (MAGNIFICATION_COMPONENT_NAME.equals(target)) { + // We can remove this branch once b/147990389 is completed + args.putStringArray( + ARG_KEY_SHORTCUT_TARGETS, new String[]{MAGNIFICATION_CONTROLLER_NAME}); + } else { + args.putStringArray( + ARG_KEY_SHORTCUT_TARGETS, new String[]{target.flattenToString()}); + } + Intent toIntent = new Intent(); + if (fromIntent != null) { + SetupWizardUtils.copySetupExtras(fromIntent, toIntent); + } + + new SubSettingLauncher(context) + .setDestination(EditShortcutsPreferenceFragment.class.getName()) + .setExtras(toIntent.getExtras()) + .setArguments(args) + .setSourceMetricsCategory(metricsCategory) + .setTitleText(screenTitle) + .launch(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + initializeArguments(); + initializePreferenceControllerArguments(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + boolean isExpanded = savedInstanceState.getBoolean(SAVED_STATE_IS_EXPANDED); + if (isExpanded) { + onExpanded(); + } + } + mSettingsObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange, Uri uri) { + if (VOLUME_KEYS_SHORTCUT_SETTING.equals(uri)) { + refreshPreferenceController(VolumeKeysShortcutOptionController.class); + } else if (BUTTON_SHORTCUT_MODE_SETTING.equals(uri) + || BUTTON_SHORTCUT_SETTING.equals(uri)) { + refreshSoftwareShortcutControllers(); + } else if (TRIPLE_TAP_SHORTCUT_SETTING.equals(uri)) { + refreshPreferenceController(TripleTapShortcutOptionController.class); + } else if (TWO_FINGERS_DOUBLE_TAP_SHORTCUT_SETTING.equals(uri)) { + refreshPreferenceController(TwoFingersDoubleTapShortcutOptionController.class); + } + } + }; + + registerSettingsObserver(); + } + + @Override + public void onResume() { + super.onResume(); + mTouchExplorationStateChangeListener = isTouchExplorationEnabled -> + refreshPreferenceController(GestureShortcutOptionController.class); + + final AccessibilityManager am = getSystemService( + AccessibilityManager.class); + am.addTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); + } + + @Override + public void onPause() { + super.onPause(); + + if (mTouchExplorationStateChangeListener != null) { + final AccessibilityManager am = getSystemService( + AccessibilityManager.class); + am.removeTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean( + SAVED_STATE_IS_EXPANDED, + use(AdvancedShortcutsPreferenceController.class).isExpanded()); + } + + @Override + public void onDestroy() { + super.onDestroy(); + unregisterSettingsObserver(); + } + + private void registerSettingsObserver() { + if (mSettingsObserver != null) { + for (Uri uri : SHORTCUT_SETTINGS) { + getContentResolver().registerContentObserver( + uri, /* notifyForDescendants= */ false, mSettingsObserver); + } + } + } + + private void unregisterSettingsObserver() { + if (mSettingsObserver != null) { + getContentResolver().unregisterContentObserver(mSettingsObserver); + } + } + + private void initializeArguments() { + Bundle args = getArguments(); + if (args == null || args.isEmpty()) { + throw new IllegalArgumentException( + EditShortcutsPreferenceFragment.class.getSimpleName() + + " requires non-empty shortcut targets"); + } + + String[] targets = args.getStringArray(ARG_KEY_SHORTCUT_TARGETS); + if (targets == null) { + throw new IllegalArgumentException( + EditShortcutsPreferenceFragment.class.getSimpleName() + + " requires non-empty shortcut targets"); + } + + mShortcutTargets = Set.of(targets); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_EDIT_SHORTCUT; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.accessibility_edit_shortcuts; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (getString(R.string.accessibility_shortcuts_advanced_collapsed) + .equals(preference.getKey())) { + onExpanded(); + // log here since calling super.onPreferenceTreeClick will be skipped + writePreferenceClickMetric(preference); + return true; + } + return super.onPreferenceTreeClick(preference); + } + + @VisibleForTesting + void initializePreferenceControllerArguments() { + boolean isInSuw = WizardManagerHelper.isAnySetupWizard(getIntent()); + + getPreferenceControllers() + .stream() + .flatMap(Collection::stream) + .filter( + controller -> controller instanceof ShortcutOptionPreferenceController) + .forEach(controller -> { + ShortcutOptionPreferenceController shortcutOptionPreferenceController = + (ShortcutOptionPreferenceController) controller; + shortcutOptionPreferenceController.setShortcutTargets(mShortcutTargets); + shortcutOptionPreferenceController.setInSetupWizard(isInSuw); + }); + } + + private void onExpanded() { + AdvancedShortcutsPreferenceController advanced = + use(AdvancedShortcutsPreferenceController.class); + advanced.setExpanded(true); + + TripleTapShortcutOptionController tripleTapShortcutOptionController = + use(TripleTapShortcutOptionController.class); + tripleTapShortcutOptionController.setExpanded(true); + + refreshPreferenceController(AdvancedShortcutsPreferenceController.class); + refreshPreferenceController(TripleTapShortcutOptionController.class); + } + + private void refreshPreferenceController( + Class controllerClass) { + AbstractPreferenceController controller = use(controllerClass); + if (controller != null) { + controller.displayPreference(getPreferenceScreen()); + if (!TextUtils.isEmpty(controller.getPreferenceKey())) { + controller.updateState(findPreference(controller.getPreferenceKey())); + } + } + } + + private void refreshSoftwareShortcutControllers() { + // Gesture + refreshPreferenceController(GestureShortcutOptionController.class); + + // FAB + refreshPreferenceController(FloatingButtonShortcutOptionController.class); + + // A11y Nav Button + refreshPreferenceController(NavButtonShortcutOptionController.class); + } +} diff --git a/src/com/android/settings/accessibility/shortcuts/FloatingButtonShortcutOptionController.java b/src/com/android/settings/accessibility/shortcuts/FloatingButtonShortcutOptionController.java new file mode 100644 index 00000000000..e72a85d9cae --- /dev/null +++ b/src/com/android/settings/accessibility/shortcuts/FloatingButtonShortcutOptionController.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import android.content.Context; +import android.text.SpannableStringBuilder; + +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.accessibility.AccessibilityUtil; + +/** + * A controller handles displaying the floating action button shortcut option preference and + * configuring the shortcut. + */ +public class FloatingButtonShortcutOptionController + extends SoftwareShortcutOptionPreferenceController { + + public FloatingButtonShortcutOptionController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + final Preference preference = screen.findPreference(getPreferenceKey()); + if (preference instanceof ShortcutOptionPreference shortcutOptionPreference) { + shortcutOptionPreference.setTitle( + R.string.accessibility_shortcut_edit_dialog_title_software); + shortcutOptionPreference.setIntroImageResId( + R.drawable.a11y_shortcut_type_software_floating); + } + } + + @Override + protected boolean isShortcutAvailable() { + return AccessibilityUtil.isFloatingMenuEnabled(mContext); + } + + @Nullable + @Override + public CharSequence getSummary() { + if (isInSetupWizard()) { + return null; + } + return new SpannableStringBuilder().append(getCustomizeAccessibilityButtonLink()); + } +} diff --git a/src/com/android/settings/accessibility/shortcuts/GestureShortcutOptionController.java b/src/com/android/settings/accessibility/shortcuts/GestureShortcutOptionController.java new file mode 100644 index 00000000000..fb312c66293 --- /dev/null +++ b/src/com/android/settings/accessibility/shortcuts/GestureShortcutOptionController.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import android.content.Context; +import android.text.SpannableStringBuilder; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.accessibility.AccessibilityUtil; + +/** + * A controller handles displaying the gesture shortcut option preference and + * configuring the shortcut. + */ +public class GestureShortcutOptionController extends SoftwareShortcutOptionPreferenceController { + + public GestureShortcutOptionController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + final Preference preference = screen.findPreference(getPreferenceKey()); + if (preference instanceof ShortcutOptionPreference shortcutOptionPreference) { + shortcutOptionPreference.setTitle( + R.string.accessibility_shortcut_edit_dialog_title_software_by_gesture); + + int resId = AccessibilityUtil.isTouchExploreEnabled(mContext) + ? R.drawable.a11y_shortcut_type_software_gesture_talkback + : R.drawable.a11y_shortcut_type_software_gesture; + shortcutOptionPreference.setIntroImageResId(resId); + } + } + + @Override + protected boolean isShortcutAvailable() { + return !isInSetupWizard() + && !AccessibilityUtil.isFloatingMenuEnabled(mContext) + && AccessibilityUtil.isGestureNavigateEnabled(mContext); + } + + @Override + public CharSequence getSummary() { + final SpannableStringBuilder sb = new SpannableStringBuilder(); + final int resId = AccessibilityUtil.isTouchExploreEnabled(mContext) + ? R.string.accessibility_shortcut_edit_dialog_summary_software_gesture_talkback + : R.string.accessibility_shortcut_edit_dialog_summary_software_gesture; + sb.append(mContext.getText(resId)); + sb.append("\n\n"); + sb.append(getCustomizeAccessibilityButtonLink()); + + return sb; + } +} diff --git a/src/com/android/settings/accessibility/shortcuts/NavButtonShortcutOptionController.java b/src/com/android/settings/accessibility/shortcuts/NavButtonShortcutOptionController.java new file mode 100644 index 00000000000..32c8da2e7ac --- /dev/null +++ b/src/com/android/settings/accessibility/shortcuts/NavButtonShortcutOptionController.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.style.ImageSpan; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.accessibility.AccessibilityUtil; + +/** + * A controller handles displaying the nav button shortcut option preference and + * configuring the shortcut. + */ +public class NavButtonShortcutOptionController extends SoftwareShortcutOptionPreferenceController { + + public NavButtonShortcutOptionController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + final Preference preference = screen.findPreference(getPreferenceKey()); + if (preference instanceof ShortcutOptionPreference shortcutOptionPreference) { + shortcutOptionPreference.setTitle( + R.string.accessibility_shortcut_edit_dialog_title_software); + shortcutOptionPreference.setIntroImageResId(R.drawable.a11y_shortcut_type_software); + shortcutOptionPreference.setSummaryProvider( + new Preference.SummaryProvider() { + @Override + public CharSequence provideSummary( + @NonNull ShortcutOptionPreference preference) { + return getSummary(preference.getSummaryTextLineHeight()); + } + }); + } + } + + @Override + protected boolean isShortcutAvailable() { + return !AccessibilityUtil.isFloatingMenuEnabled(mContext) + && !AccessibilityUtil.isGestureNavigateEnabled(mContext); + } + + private CharSequence getSummary(int lineHeight) { + final SpannableStringBuilder sb = new SpannableStringBuilder(); + sb.append(getSummaryStringWithIcon(lineHeight)); + + if (!isInSetupWizard()) { + sb.append("\n\n"); + sb.append(getCustomizeAccessibilityButtonLink()); + } + + return sb; + } + + private SpannableString getSummaryStringWithIcon(int lineHeight) { + final String summary = mContext + .getString(R.string.accessibility_shortcut_edit_dialog_summary_software); + final SpannableString spannableMessage = SpannableString.valueOf(summary); + + // Icon + final int indexIconStart = summary.indexOf("%s"); + final int indexIconEnd = indexIconStart + 2; + final Drawable icon = mContext.getDrawable(R.drawable.ic_accessibility_new); + final ImageSpan imageSpan = new ImageSpan(icon); + imageSpan.setContentDescription(""); + icon.setBounds( + /* left= */ 0, /* top= */ 0, /* right= */ lineHeight, /* bottom= */ lineHeight); + spannableMessage.setSpan( + imageSpan, indexIconStart, indexIconEnd, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + return spannableMessage; + } +} diff --git a/src/com/android/settings/accessibility/shortcuts/ShortcutOptionPreference.java b/src/com/android/settings/accessibility/shortcuts/ShortcutOptionPreference.java new file mode 100644 index 00000000000..53d84b3e02b --- /dev/null +++ b/src/com/android/settings/accessibility/shortcuts/ShortcutOptionPreference.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import android.content.Context; +import android.content.res.Resources; +import android.text.method.LinkMovementMethod; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RawRes; +import androidx.preference.CheckBoxPreference; +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; +import com.android.settingslib.widget.LottieColorUtils; + +import com.airbnb.lottie.LottieAnimationView; +import com.airbnb.lottie.LottieDrawable; + +/** + * A preference represents an accessibility shortcut option with a checkbox and a tutorial image + */ +public class ShortcutOptionPreference extends CheckBoxPreference { + + private static final String TAG = "ShortcutOptionPreference"; + + @DrawableRes + private int mIntroImageResId = Resources.ID_NULL; + @RawRes + private int mIntroImageRawResId = Resources.ID_NULL; + + private int mSummaryTextLineHeight; + + public ShortcutOptionPreference( + @NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public ShortcutOptionPreference(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public ShortcutOptionPreference(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ShortcutOptionPreference(@NonNull Context context) { + super(context); + init(); + } + + private void init() { + setLayoutResource(R.layout.accessibility_shortcut_option_checkable); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + if (mIntroImageResId == Resources.ID_NULL && mIntroImageRawResId == Resources.ID_NULL) { + holder.findViewById(R.id.image).setVisibility(View.GONE); + } else { + holder.findViewById(R.id.image).setVisibility(View.VISIBLE); + LottieAnimationView imageView = holder.itemView.findViewById(R.id.image); + + if (mIntroImageRawResId != Resources.ID_NULL) { + imageView.setFailureListener(result -> + Log.w(TAG, + "Invalid image raw resource id: " + + getContext().getResources() + .getResourceEntryName(mIntroImageRawResId), + result)); + imageView.setAnimation(mIntroImageRawResId); + imageView.setRepeatCount(LottieDrawable.INFINITE); + LottieColorUtils.applyDynamicColors(getContext(), imageView); + imageView.playAnimation(); + } else { + imageView.setImageResource(mIntroImageResId); + } + } + + final TextView summaryView = (TextView) holder.findViewById(android.R.id.summary); + if (summaryView != null) { + mSummaryTextLineHeight = summaryView.getLineHeight(); + summaryView.setMovementMethod(LinkMovementMethod.getInstance()); + } + + syncSummaryView(holder); + } + + + /** + * Sets the introduction image for this preference with a drawable resource ID. + */ + public void setIntroImageResId(@DrawableRes int introImageResId) { + if (introImageResId != mIntroImageResId) { + mIntroImageResId = introImageResId; + mIntroImageRawResId = Resources.ID_NULL; + notifyChanged(); + } + } + + /** + * Sets the introduction image for this preference with a raw resource ID for an animated image. + */ + public void setIntroImageRawResId(@RawRes int introImageRawResId) { + if (introImageRawResId != mIntroImageRawResId) { + mIntroImageRawResId = introImageRawResId; + mIntroImageResId = Resources.ID_NULL; + notifyChanged(); + } + } + + public int getSummaryTextLineHeight() { + return mSummaryTextLineHeight; + } +} diff --git a/src/com/android/settings/accessibility/shortcuts/ShortcutOptionPreferenceController.java b/src/com/android/settings/accessibility/shortcuts/ShortcutOptionPreferenceController.java new file mode 100644 index 00000000000..4d3555468f9 --- /dev/null +++ b/src/com/android/settings/accessibility/shortcuts/ShortcutOptionPreferenceController.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import android.content.Context; +import android.os.UserHandle; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; + +import com.android.internal.accessibility.common.ShortcutConstants; +import com.android.internal.accessibility.util.ShortcutUtils; +import com.android.internal.util.Preconditions; +import com.android.settings.core.BasePreferenceController; + +import java.util.Collections; +import java.util.Set; + +/** + * A base preference controller for {@link ShortcutOptionPreference} + */ +public abstract class ShortcutOptionPreferenceController extends BasePreferenceController + implements Preference.OnPreferenceChangeListener { + private Set mShortcutTargets = Collections.emptySet(); + private boolean mIsInSetupWizard; + + public ShortcutOptionPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (getPreferenceKey().equals(preference.getKey()) + && preference instanceof ShortcutOptionPreference) { + ((ShortcutOptionPreference) preference).setChecked(isChecked()); + } + } + + @Override + public int getAvailabilityStatus() { + if (isShortcutAvailable()) { + return AVAILABLE_UNSEARCHABLE; + } + return CONDITIONALLY_UNAVAILABLE; + } + + /** + * Set the targets (i.e. a11y features) to be configured with the a11y shortcut option. + *

+ * Note: the shortcutTargets cannot be empty, since the edit a11y shortcut option + * is meant to configure the shortcut options for an a11y feature. + * + * + * @param shortcutTargets the a11y features, like color correction, Talkback, etc. + * @throws NullPointerException if the {@code shortcutTargets} was {@code null} + * @throws IllegalArgumentException if the {@code shortcutTargets} was empty + */ + public void setShortcutTargets(Set shortcutTargets) { + Preconditions.checkCollectionNotEmpty(shortcutTargets, /* valueName= */ "a11y targets"); + + this.mShortcutTargets = shortcutTargets; + } + + public void setInSetupWizard(boolean isInSetupWizard) { + this.mIsInSetupWizard = isInSetupWizard; + } + + protected Set getShortcutTargets() { + return mShortcutTargets; + } + + protected boolean isInSetupWizard() { + return mIsInSetupWizard; + } + + @Override + public final boolean onPreferenceChange(@NonNull Preference preference, Object newValue) { + enableShortcutForTargets((Boolean) newValue); + return false; + } + + @ShortcutConstants.UserShortcutType + protected int getShortcutType() { + return ShortcutConstants.UserShortcutType.DEFAULT; + } + + /** + * Returns true if the shortcut is associated to the targets + */ + protected boolean isChecked() { + Set targets = ShortcutUtils.getShortcutTargetsFromSettings( + mContext, getShortcutType(), UserHandle.myUserId()); + + return !targets.isEmpty() && targets.containsAll(getShortcutTargets()); + } + + /** + * Enable or disable the shortcut for the given accessibility features. + */ + protected void enableShortcutForTargets(boolean enable) { + Set shortcutTargets = getShortcutTargets(); + @ShortcutConstants.UserShortcutType int shortcutType = getShortcutType(); + + if (enable) { + for (String target : shortcutTargets) { + ShortcutUtils.optInValueToSettings(mContext, shortcutType, target); + } + } else { + for (String target : shortcutTargets) { + ShortcutUtils.optOutValueFromSettings(mContext, shortcutType, target); + } + } + ShortcutUtils.updateInvisibleToggleAccessibilityServiceEnableState( + mContext, shortcutTargets, UserHandle.myUserId()); + } + + /** + * Returns true when the user can associate a shortcut to the targets + */ + protected abstract boolean isShortcutAvailable(); +} diff --git a/src/com/android/settings/accessibility/shortcuts/SoftwareShortcutOptionPreferenceController.java b/src/com/android/settings/accessibility/shortcuts/SoftwareShortcutOptionPreferenceController.java new file mode 100644 index 00000000000..24098c81f38 --- /dev/null +++ b/src/com/android/settings/accessibility/shortcuts/SoftwareShortcutOptionPreferenceController.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; + +import android.content.Context; +import android.provider.Settings; +import android.view.View; + +import com.android.internal.accessibility.common.ShortcutConstants; +import com.android.settings.R; +import com.android.settings.accessibility.AccessibilityButtonFragment; +import com.android.settings.accessibility.FloatingMenuSizePreferenceController; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.utils.AnnotationSpan; + +/** + * A base controller for the preference controller of software shortcuts. + */ +public abstract class SoftwareShortcutOptionPreferenceController + extends ShortcutOptionPreferenceController { + + public SoftwareShortcutOptionPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @ShortcutConstants.UserShortcutType + @Override + protected int getShortcutType() { + return ShortcutConstants.UserShortcutType.SOFTWARE; + } + + private boolean isMagnificationInTargets() { + return getShortcutTargets().contains(MAGNIFICATION_CONTROLLER_NAME); + } + + protected CharSequence getCustomizeAccessibilityButtonLink() { + final View.OnClickListener linkListener = v -> new SubSettingLauncher(mContext) + .setDestination(AccessibilityButtonFragment.class.getName()) + .setSourceMetricsCategory(getMetricsCategory()) + .launch(); + final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo( + AnnotationSpan.LinkInfo.DEFAULT_ANNOTATION, linkListener); + return AnnotationSpan.linkify( + mContext.getText( + R.string.accessibility_shortcut_edit_dialog_summary_software_floating), + linkInfo); + } + + @Override + protected void enableShortcutForTargets(boolean enable) { + super.enableShortcutForTargets(enable); + + if (enable) { + // Update the A11y FAB size to large when the Magnification shortcut is enabled + // and the user hasn't changed the floating button size + if (isMagnificationInTargets() + && Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE, + FloatingMenuSizePreferenceController.Size.UNKNOWN) + == FloatingMenuSizePreferenceController.Size.UNKNOWN) { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE, + FloatingMenuSizePreferenceController.Size.LARGE); + } + } + } +} diff --git a/src/com/android/settings/accessibility/shortcuts/TripleTapShortcutOptionController.java b/src/com/android/settings/accessibility/shortcuts/TripleTapShortcutOptionController.java new file mode 100644 index 00000000000..0eb1ee5acd7 --- /dev/null +++ b/src/com/android/settings/accessibility/shortcuts/TripleTapShortcutOptionController.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; + +import android.content.Context; +import android.icu.text.MessageFormat; +import android.provider.Settings; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.internal.accessibility.common.ShortcutConstants; +import com.android.settings.R; +import com.android.settings.accessibility.AccessibilityUtil; +import com.android.settings.accessibility.ExpandablePreference; + +import java.util.Set; + +/** + * A controller handles displaying the triple tap shortcut option preference and + * configuring the shortcut. + */ +public class TripleTapShortcutOptionController extends ShortcutOptionPreferenceController + implements ExpandablePreference { + + private boolean mIsExpanded = false; + + public TripleTapShortcutOptionController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + final Preference preference = screen.findPreference(getPreferenceKey()); + if (preference instanceof ShortcutOptionPreference shortcutOptionPreference) { + shortcutOptionPreference.setTitle( + R.string.accessibility_shortcut_edit_dialog_title_triple_tap); + String summary = mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_triple_tap); + // Format the number '3' in the summary. + final Object[] arguments = {3}; + summary = MessageFormat.format(summary, arguments); + + shortcutOptionPreference.setSummary(summary); + shortcutOptionPreference.setIntroImageRawResId( + R.raw.a11y_shortcut_type_triple_tap); + } + } + + @Override + public int getAvailabilityStatus() { + if (isExpanded() && isShortcutAvailable()) { + return AVAILABLE_UNSEARCHABLE; + } + return CONDITIONALLY_UNAVAILABLE; + } + + @ShortcutConstants.UserShortcutType + @Override + protected int getShortcutType() { + return ShortcutConstants.UserShortcutType.TRIPLETAP; + } + + @Override + public void setExpanded(boolean expanded) { + mIsExpanded = expanded; + } + + @Override + public boolean isExpanded() { + return mIsExpanded; + } + + @Override + protected boolean isShortcutAvailable() { + Set shortcutTargets = getShortcutTargets(); + return shortcutTargets.size() == 1 + && shortcutTargets.contains(MAGNIFICATION_CONTROLLER_NAME); + } + + @Override + protected boolean isChecked() { + return Settings.Secure.getInt( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, + AccessibilityUtil.State.OFF) == AccessibilityUtil.State.ON; + } + + @Override + protected void enableShortcutForTargets(boolean enable) { + Settings.Secure.putInt( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, + enable ? AccessibilityUtil.State.ON : AccessibilityUtil.State.OFF); + } +} diff --git a/src/com/android/settings/accessibility/shortcuts/TwoFingersDoubleTapShortcutOptionController.java b/src/com/android/settings/accessibility/shortcuts/TwoFingersDoubleTapShortcutOptionController.java new file mode 100644 index 00000000000..64ed7bdbd19 --- /dev/null +++ b/src/com/android/settings/accessibility/shortcuts/TwoFingersDoubleTapShortcutOptionController.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; + +import android.content.Context; +import android.icu.text.MessageFormat; +import android.provider.Settings; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.internal.accessibility.common.ShortcutConstants; +import com.android.server.accessibility.Flags; +import com.android.settings.R; +import com.android.settings.accessibility.AccessibilityUtil; + +import java.util.Set; + +/** + * A controller handles displaying the two fingers double tap shortcut option preference and + * configuring the shortcut. + */ +public class TwoFingersDoubleTapShortcutOptionController + extends ShortcutOptionPreferenceController { + + public TwoFingersDoubleTapShortcutOptionController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @ShortcutConstants.UserShortcutType + @Override + protected int getShortcutType() { + return ShortcutConstants.UserShortcutType.TRIPLETAP; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + final Preference preference = screen.findPreference(getPreferenceKey()); + if (preference instanceof ShortcutOptionPreference shortcutOptionPreference) { + // TODO (b/306153204): Update shortcut string and image when UX provides them + shortcutOptionPreference.setTitle( + R.string.accessibility_shortcut_edit_dialog_title_two_finger_double_tap); + String summary = mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_two_finger_double_tap); + // Format the number '2' in the summary. + final Object[] arguments = {2}; + summary = MessageFormat.format(summary, arguments); + + shortcutOptionPreference.setSummary(summary); + shortcutOptionPreference.setIntroImageRawResId( + R.raw.a11y_shortcut_type_triple_tap); + } + } + + @Override + protected boolean isShortcutAvailable() { + if (!Flags.enableMagnificationMultipleFingerMultipleTapGesture()) { + return false; + } + // Only Magnification has two fingers triple tap shortcut option. + Set shortcutTargets = getShortcutTargets(); + return shortcutTargets.size() == 1 + && shortcutTargets.contains(MAGNIFICATION_CONTROLLER_NAME); + } + + @Override + protected boolean isChecked() { + return Settings.Secure.getInt( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED, + AccessibilityUtil.State.OFF) == AccessibilityUtil.State.ON; + } + + @Override + protected void enableShortcutForTargets(boolean enable) { + Settings.Secure.putInt( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED, + enable ? AccessibilityUtil.State.ON : AccessibilityUtil.State.OFF); + } +} diff --git a/src/com/android/settings/accessibility/shortcuts/VolumeKeysShortcutOptionController.java b/src/com/android/settings/accessibility/shortcuts/VolumeKeysShortcutOptionController.java new file mode 100644 index 00000000000..9083e7ccc88 --- /dev/null +++ b/src/com/android/settings/accessibility/shortcuts/VolumeKeysShortcutOptionController.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import android.content.Context; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.internal.accessibility.common.ShortcutConstants; +import com.android.settings.R; +import com.android.settings.accessibility.AccessibilityUtil; + +/** + * A controller handles displaying the volume keys shortcut option preference and + * configuring the shortcut. + */ +public class VolumeKeysShortcutOptionController extends ShortcutOptionPreferenceController { + + public VolumeKeysShortcutOptionController( + Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @ShortcutConstants.UserShortcutType + @Override + protected int getShortcutType() { + return ShortcutConstants.UserShortcutType.HARDWARE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + final Preference preference = screen.findPreference(getPreferenceKey()); + if (preference instanceof ShortcutOptionPreference shortcutOptionPreference) { + shortcutOptionPreference.setTitle( + R.string.accessibility_shortcut_edit_dialog_title_hardware); + shortcutOptionPreference.setSummary( + R.string.accessibility_shortcut_edit_dialog_summary_hardware); + shortcutOptionPreference.setIntroImageResId( + R.drawable.a11y_shortcut_type_hardware); + } + } + + @Override + protected boolean isShortcutAvailable() { + return true; + } + + @Override + protected void enableShortcutForTargets(boolean enable) { + super.enableShortcutForTargets(enable); + if (enable) { + AccessibilityUtil.skipVolumeShortcutDialogTimeoutRestriction(mContext); + } + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/shortcuts/AdvancedShortcutsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/shortcuts/AdvancedShortcutsPreferenceControllerTest.java new file mode 100644 index 00000000000..0a8273be916 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/shortcuts/AdvancedShortcutsPreferenceControllerTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ComponentName; +import android.content.Context; + +import androidx.preference.Preference; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.Set; + +/** + * Tests for {@link AdvancedShortcutsPreferenceController} + */ +@RunWith(RobolectricTestRunner.class) +public class AdvancedShortcutsPreferenceControllerTest { + private static final String PREF_KEY = "prefKey"; + private static final String TARGET_MAGNIFICATION = + "com.android.server.accessibility.MagnificationController"; + private static final String TARGET_FAKE = + new ComponentName("FakePackage", "FakeClass").flattenToString(); + private final Context mContext = ApplicationProvider.getApplicationContext(); + private AdvancedShortcutsPreferenceController mController; + + @Before + public void setUp() { + mController = new AdvancedShortcutsPreferenceController(mContext, PREF_KEY); + mController.setShortcutTargets(Set.of(TARGET_MAGNIFICATION)); + Preference preference = new Preference(mContext); + preference.setKey(PREF_KEY); + PreferenceScreen preferenceScreen = + new PreferenceManager(mContext).createPreferenceScreen(mContext); + preferenceScreen.addPreference(preference); + } + + @Test + public void getAvailabilityStatus_targetIsMagnificationAndIsExpanded_returnsConditionallyUnavailable() { + mController.setExpanded(true); + mController.setShortcutTargets(Set.of(TARGET_MAGNIFICATION)); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void getAvailabilityStatus_targetIsMagnificationAndIsNotExpanded_returnsAvailableUnsearchable() { + mController.setExpanded(false); + mController.setShortcutTargets(Set.of(TARGET_MAGNIFICATION)); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE_UNSEARCHABLE); + } + + @Test + public void getAvailabilityStatus_targetIsNotMagnificationAndIsNotExpanded_returnsConditionallyUnavailable() { + mController.setExpanded(false); + mController.setShortcutTargets(Set.of(TARGET_FAKE)); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void getAvailabilityStatus_targetIsNotMagnificationAndIsExpanded_returnsConditionallyUnavailable() { + mController.setExpanded(true); + mController.setShortcutTargets(Set.of(TARGET_FAKE)); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void setExpanded_expand_updateExpandedValue() { + mController.setExpanded(true); + + assertThat(mController.isExpanded()).isTrue(); + } + + @Test + public void setExpanded_collapse_updateExpandedValue() { + mController.setExpanded(false); + + assertThat(mController.isExpanded()).isFalse(); + } + + @Test + public void isShortcutAvailable_multipleTargets_returnFalse() { + mController.setShortcutTargets(Set.of(TARGET_FAKE, TARGET_MAGNIFICATION)); + + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + @Test + public void isShortcutAvailable_magnificationTargetOnly_returnTrue() { + mController.setShortcutTargets(Set.of(TARGET_MAGNIFICATION)); + + assertThat(mController.isShortcutAvailable()).isTrue(); + } + + @Test + public void isShortcutAvailable_nonMagnificationTarget_returnFalse() { + mController.setShortcutTargets(Set.of(TARGET_FAKE)); + + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + @Test + public void isChecked_returnFalse() { + assertThat(mController.isChecked()).isFalse(); + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/shortcuts/EditShortcutsPreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/shortcuts/EditShortcutsPreferenceFragmentTest.java new file mode 100644 index 00000000000..d535f1581ee --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/shortcuts/EditShortcutsPreferenceFragmentTest.java @@ -0,0 +1,438 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + + +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; + +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME; +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; +import static com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragment.SHORTCUT_SETTINGS; + +import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_DEFERRED_SETUP; +import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_FIRST_RUN; +import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_PRE_DEFERRED_SETUP; +import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SETUP_FLOW; +import static com.google.common.truth.Truth.assertThat; + +import static org.robolectric.Shadows.shadowOf; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; +import android.view.accessibility.AccessibilityManager; + +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.testing.FragmentScenario; +import androidx.lifecycle.Lifecycle; +import androidx.preference.Preference; +import androidx.preference.TwoStatePreference; +import androidx.test.core.app.ApplicationProvider; + +import com.android.internal.accessibility.common.ShortcutConstants; +import com.android.internal.accessibility.util.ShortcutUtils; +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.SubSettings; +import com.android.settings.accessibility.AccessibilityUtil; +import com.android.settings.testutils.shadow.SettingsShadowResources; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +import com.google.android.setupcompat.util.WizardManagerHelper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowAccessibilityManager; +import org.robolectric.shadows.ShadowContentResolver; +import org.robolectric.shadows.ShadowLooper; +import org.robolectric.util.ReflectionHelpers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * Tests for {@link EditShortcutsPreferenceFragment} + */ +@Config(shadows = SettingsShadowResources.class) +@RunWith(RobolectricTestRunner.class) +public class EditShortcutsPreferenceFragmentTest { + private static final int METRICS_CATEGORY = 123; + private static final CharSequence SCREEN_TITLE = "Fake shortcut title"; + private static final ComponentName TARGET_FAKE_COMPONENT = + new ComponentName("FakePackage", "FakeClass"); + private static final String TARGET = MAGNIFICATION_CONTROLLER_NAME; + private static final Set TARGETS = Set.of(TARGET); + + private final Context mContext = ApplicationProvider.getApplicationContext(); + private FragmentActivity mActivity; + private FragmentScenario mFragmentScenario; + + @Before + public void setUp() { + SettingsShadowResources.overrideResource( + com.android.internal.R.integer.config_navBarInteractionMode, + NAV_BAR_MODE_GESTURAL); + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_BUTTON_MODE, + Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE); + + mActivity = Robolectric.buildActivity(FragmentActivity.class).get(); + } + + @After + public void cleanUp() { + if (mFragmentScenario != null) { + mFragmentScenario.close(); + } + } + + @Test + public void showEditShortcutScreen_targetIsMagnification_launchSubSetting() { + EditShortcutsPreferenceFragment.showEditShortcutScreen( + mActivity, METRICS_CATEGORY, SCREEN_TITLE, + MAGNIFICATION_COMPONENT_NAME, /* fromIntent= */ null); + + assertLaunchSubSettingWithCurrentTargetComponents( + MAGNIFICATION_CONTROLLER_NAME, /* isInSuw= */ false); + } + + @Test + public void showEditShortcutScreen_launchSubSetting() { + EditShortcutsPreferenceFragment.showEditShortcutScreen( + mActivity, METRICS_CATEGORY, SCREEN_TITLE, + TARGET_FAKE_COMPONENT, /* fromIntent= */ null); + + assertLaunchSubSettingWithCurrentTargetComponents( + TARGET_FAKE_COMPONENT.flattenToString(), /* isInSuw= */ false); + } + + @Test + public void showEditShortcutScreen_inSuw_launchSubSettingWithSuw() { + EditShortcutsPreferenceFragment.showEditShortcutScreen( + mActivity, METRICS_CATEGORY, SCREEN_TITLE, + TARGET_FAKE_COMPONENT, createSuwIntent(new Intent(), /* isInSuw= */ true)); + + assertLaunchSubSettingWithCurrentTargetComponents( + TARGET_FAKE_COMPONENT.flattenToString(), /* isInSuw= */ true); + } + + @Test + public void fragmentCreated_inSuw_controllersTargetsSet() { + mFragmentScenario = createFragScenario(/* isInSuw= */ true); + mFragmentScenario.moveToState(Lifecycle.State.CREATED); + + mFragmentScenario.onFragment(fragment -> { + List controllers = + getShortcutOptionPreferenceControllers(fragment); + + for (ShortcutOptionPreferenceController controller : controllers) { + assertThat(controller.getShortcutTargets()).containsExactlyElementsIn(TARGETS); + assertThat(controller.isInSetupWizard()).isTrue(); + } + }); + } + + @Test + public void fragmentCreated_notInSuw_controllersTargetsSet() { + mFragmentScenario = createFragScenario(/* isInSuw= */ false); + mFragmentScenario.moveToState(Lifecycle.State.CREATED); + + mFragmentScenario.onFragment(fragment -> { + List controllers = + getShortcutOptionPreferenceControllers(fragment); + + for (ShortcutOptionPreferenceController controller : controllers) { + assertThat(controller.getShortcutTargets()).containsExactlyElementsIn(TARGETS); + assertThat(controller.isInSetupWizard()).isFalse(); + } + }); + } + + @Test + public void fragmentCreated_settingsObserversAreRegistered() { + ShadowContentResolver contentResolver = shadowOf(mContext.getContentResolver()); + for (Uri uri : SHORTCUT_SETTINGS) { + assertThat(contentResolver.getContentObservers(uri)).isEmpty(); + } + + mFragmentScenario = createFragScenario(/* isInSuw= */ false); + mFragmentScenario.moveToState(Lifecycle.State.CREATED); + + for (Uri uri : SHORTCUT_SETTINGS) { + assertThat(contentResolver.getContentObservers(uri)).isNotEmpty(); + } + } + + @Test + public void fragmentDestroyed_unregisterSettingsObserver() { + ShadowContentResolver contentResolver = shadowOf(mContext.getContentResolver()); + + mFragmentScenario = createFragScenario(/* isInSuw= */ false) + .moveToState(Lifecycle.State.CREATED); + mFragmentScenario.onFragment(EditShortcutsPreferenceFragment::onDestroy); + + for (Uri uri : SHORTCUT_SETTINGS) { + assertThat(contentResolver.getContentObservers(uri)).isEmpty(); + } + } + + @Test + public void onVolumeKeysShortcutSettingChanged_volumeKeyControllerUpdated() { + mFragmentScenario = createFragScenario(/* isInSuw= */ false); + mFragmentScenario.moveToState(Lifecycle.State.CREATED); + + ShortcutUtils.optInValueToSettings( + mContext, ShortcutConstants.UserShortcutType.HARDWARE, TARGET); + + mFragmentScenario.onFragment(fragment -> { + TwoStatePreference preference = fragment.findPreference( + mContext.getString(R.string.accessibility_shortcut_volume_keys_pref)); + assertThat(preference.isChecked()).isTrue(); + }); + } + + @Test + public void onSoftwareShortcutSettingChanged_softwareControllersUpdated() { + mFragmentScenario = createFragScenario(/* isInSuw= */ false); + mFragmentScenario.moveToState(Lifecycle.State.CREATED); + + ShortcutUtils.optInValueToSettings( + mContext, ShortcutConstants.UserShortcutType.SOFTWARE, TARGET); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + mFragmentScenario.onFragment(fragment -> { + TwoStatePreference preference = fragment.findPreference( + mContext.getString(R.string.accessibility_shortcut_gesture_pref)); + assertThat(preference.isChecked()).isTrue(); + }); + } + + @Test + public void onSoftwareShortcutModeChanged_softwareControllersUpdated() { + mFragmentScenario = createFragScenario(/* isInSuw= */ false); + mFragmentScenario.moveToState(Lifecycle.State.CREATED); + + ShortcutUtils.optInValueToSettings( + mContext, ShortcutConstants.UserShortcutType.SOFTWARE, TARGET); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + mFragmentScenario.onFragment(fragment -> { + TwoStatePreference preference = fragment.findPreference( + mContext.getString(R.string.accessibility_shortcut_gesture_pref)); + assertThat(preference.isChecked()).isTrue(); + }); + } + + @Test + public void onTripleTapShortcutSettingChanged_tripleTapShortcutControllerUpdated() { + mFragmentScenario = createFragScenario(/* isInSuw= */ false); + mFragmentScenario.moveToState(Lifecycle.State.CREATED); + + Settings.Secure.putInt( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, + AccessibilityUtil.State.ON); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + mFragmentScenario.onFragment(fragment -> { + TwoStatePreference preference = fragment.findPreference( + mContext.getString(R.string.accessibility_shortcut_triple_tap_pref)); + assertThat(preference.isChecked()).isTrue(); + }); + } + + @Test + public void onTwoFingersShortcutSettingChanged_twoFingersDoubleTapShortcutControllerUpdated() { + mFragmentScenario = createFragScenario(/* isInSuw= */ false); + mFragmentScenario.moveToState(Lifecycle.State.CREATED); + + Settings.Secure.putInt( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED, + AccessibilityUtil.State.ON); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + mFragmentScenario.onFragment(fragment -> { + TwoStatePreference preference = fragment.findPreference( + mContext.getString( + R.string.accessibility_shortcut_two_fingers_double_tap_pref)); + assertThat(preference.isChecked()).isTrue(); + }); + } + + @Test + public void fragmentResumed_enableTouchExploration_gestureShortcutOptionSummaryUpdated() { + String expectedSummary = mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_software_gesture_talkback) + + "\n\n" + + mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_software_floating); + mFragmentScenario = createFragScenario(/* isInSuw= */ false); + mFragmentScenario.moveToState(Lifecycle.State.RESUMED); + + ShadowAccessibilityManager am = shadowOf( + mContext.getSystemService(AccessibilityManager.class)); + am.setTouchExplorationEnabled(true); + + mFragmentScenario.onFragment(fragment -> { + Preference preference = fragment.findPreference( + mContext.getString(R.string.accessibility_shortcut_gesture_pref)); + assertThat(preference.getSummary().toString()).isEqualTo(expectedSummary); + }); + } + + @Test + public void fragmentPaused_enableTouchExploration_gestureShortcutOptionSummaryNotUpdated() { + String expectedSummary = mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_software_gesture) + + "\n\n" + + mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_software_floating); + mFragmentScenario = createFragScenario(/* isInSuw= */ false); + mFragmentScenario.moveToState(Lifecycle.State.RESUMED).moveToState(Lifecycle.State.STARTED); + + ShadowAccessibilityManager am = shadowOf( + mContext.getSystemService(AccessibilityManager.class)); + am.setTouchExplorationEnabled(true); + + mFragmentScenario.onFragment(fragment -> { + Preference preference = fragment.findPreference( + mContext.getString(R.string.accessibility_shortcut_gesture_pref)); + assertThat(preference.getSummary().toString()).isEqualTo(expectedSummary); + }); + } + + @Test + public void onAdvancedPreferenceClicked_advancedShouldBecomeInvisible() { + mFragmentScenario = createFragScenario(/* isInSuw= */ false); + mFragmentScenario.moveToState(Lifecycle.State.RESUMED); + mFragmentScenario.onFragment(fragment -> { + Preference advanced = fragment.findPreference( + mContext.getString(R.string.accessibility_shortcuts_advanced_collapsed)); + assertThat(advanced.isVisible()).isTrue(); + + fragment.onPreferenceTreeClick(advanced); + + assertThat(advanced.isVisible()).isFalse(); + }); + } + + @Test + public void fragmentRecreated_expanded_advancedRemainInvisible() { + onAdvancedPreferenceClicked_advancedShouldBecomeInvisible(); + + mFragmentScenario.recreate(); + + mFragmentScenario.onFragment(fragment -> { + Preference advanced = fragment.findPreference( + mContext.getString(R.string.accessibility_shortcuts_advanced_collapsed)); + assertThat(advanced.isVisible()).isFalse(); + }); + } + + @Test + public void fragmentRecreated_collapsed_advancedRemainVisible() { + mFragmentScenario = createFragScenario(/* isInSuw= */ false); + mFragmentScenario.moveToState(Lifecycle.State.RESUMED); + + mFragmentScenario.recreate(); + + mFragmentScenario.onFragment(fragment -> { + Preference advanced = fragment.findPreference( + mContext.getString(R.string.accessibility_shortcuts_advanced_collapsed)); + assertThat(advanced.isVisible()).isTrue(); + }); + } + + private void assertLaunchSubSettingWithCurrentTargetComponents( + String componentName, boolean isInSuw) { + Intent intent = shadowOf(mActivity.getApplication()).getNextStartedActivity(); + + assertThat(intent).isNotNull(); + assertThat(intent.getAction()).isEqualTo(Intent.ACTION_MAIN); + assertThat(intent.getComponent()).isEqualTo( + new ComponentName(mActivity, SubSettings.class)); + assertThat(intent.getExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(EditShortcutsPreferenceFragment.class.getName()); + assertThat(intent.getExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE)) + .isEqualTo(SCREEN_TITLE.toString()); + assertThat(intent.getExtra( + MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY)).isEqualTo(METRICS_CATEGORY); + Bundle args = (Bundle) intent.getExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); + assertThat(args).isNotNull(); + assertThat(Arrays.stream(args.getStringArray( + EditShortcutsPreferenceFragment.ARG_KEY_SHORTCUT_TARGETS)).toList()) + .containsExactly(componentName); + assertThat(WizardManagerHelper.isAnySetupWizard(intent)).isEqualTo(isInSuw); + } + + private List getShortcutOptionPreferenceControllers( + EditShortcutsPreferenceFragment fragment) { + + Collection> controllers = + ReflectionHelpers.callInstanceMethod(fragment, "getPreferenceControllers"); + List retControllers = new ArrayList<>(); + controllers.stream().flatMap(Collection::stream) + .filter(controller -> controller instanceof ShortcutOptionPreferenceController) + .forEach(controller -> + retControllers.add((ShortcutOptionPreferenceController) controller)); + + return retControllers; + } + + private FragmentScenario createFragScenario(boolean isInSuw) { + Bundle args = new Bundle(); + args.putStringArray( + EditShortcutsPreferenceFragment.ARG_KEY_SHORTCUT_TARGETS, new String[]{TARGET}); + FragmentScenario scenario = + FragmentScenario.launch( + EditShortcutsPreferenceFragment.class, args, + /* themeResId= */ 0, Lifecycle.State.INITIALIZED); + scenario.onFragment(fragment -> { + Intent intent = fragment.requireActivity().getIntent(); + fragment.requireActivity().setIntent(createSuwIntent(intent, isInSuw)); + // Since the fragment is attached before we have a chance + // to modify the activity's intent; initialize controllers again + fragment.initializePreferenceControllerArguments(); + }); + return scenario; + } + + private Intent createSuwIntent(Intent intent, boolean isInSuw) { + + if (intent == null) { + intent = new Intent(); + } + intent.putExtra(EXTRA_IS_SETUP_FLOW, isInSuw); + intent.putExtra(EXTRA_IS_FIRST_RUN, isInSuw); + intent.putExtra(EXTRA_IS_PRE_DEFERRED_SETUP, isInSuw); + intent.putExtra(EXTRA_IS_DEFERRED_SETUP, isInSuw); + return intent; + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/shortcuts/FloatingButtonShortcutOptionControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/shortcuts/FloatingButtonShortcutOptionControllerTest.java new file mode 100644 index 00000000000..b39aa226239 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/shortcuts/FloatingButtonShortcutOptionControllerTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; +import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ComponentName; +import android.content.Context; +import android.provider.Settings; +import android.text.TextUtils; + +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.Set; + +/** + * Tests for {@link FloatingButtonShortcutOptionController} + */ +@RunWith(RobolectricTestRunner.class) +public class FloatingButtonShortcutOptionControllerTest { + private static final String PREF_KEY = "prefKey"; + private static final String TARGET = + new ComponentName("FakePackage", "FakeClass").flattenToString(); + private final Context mContext = ApplicationProvider.getApplicationContext(); + private FloatingButtonShortcutOptionController mController; + private ShortcutOptionPreference mShortcutOptionPreference; + + private PreferenceScreen mPreferenceScreen; + + @Before + public void setUp() { + mController = new FloatingButtonShortcutOptionController( + mContext, PREF_KEY); + mController.setShortcutTargets(Set.of(TARGET)); + mShortcutOptionPreference = new ShortcutOptionPreference(mContext); + mShortcutOptionPreference.setKey(PREF_KEY); + mPreferenceScreen = new PreferenceManager(mContext).createPreferenceScreen(mContext); + mPreferenceScreen.addPreference(mShortcutOptionPreference); + setFloatingButtonEnabled(true); + } + + @Test + public void displayPreference_verifyTitle() { + mController.displayPreference(mPreferenceScreen); + + assertThat(mShortcutOptionPreference.getTitle().toString()).isEqualTo( + mContext.getString(R.string.accessibility_shortcut_edit_dialog_title_software)); + } + + @Test + public void getSummary_inSuw_verifySummaryEmpty() { + mController.setInSetupWizard(true); + + assertThat(TextUtils.isEmpty(mController.getSummary())).isTrue(); + } + + @Test + public void getSummary_notInSuw_verifySummary() { + mController.setInSetupWizard(false); + + assertThat(mController.getSummary().toString()).isEqualTo( + mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_software_floating)); + } + + @Test + public void isShortcutAvailable_floatingMenuEnabled_returnTrue() { + setFloatingButtonEnabled(true); + + assertThat(mController.isShortcutAvailable()).isTrue(); + } + + @Test + public void isShortcutAvailable_floatingMenuDisabled_returnFalse() { + setFloatingButtonEnabled(false); + + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + private void setFloatingButtonEnabled(boolean enable) { + int mode = enable + ? ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU : ACCESSIBILITY_BUTTON_MODE_GESTURE; + + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_BUTTON_MODE, mode); + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/shortcuts/GestureShortcutOptionControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/shortcuts/GestureShortcutOptionControllerTest.java new file mode 100644 index 00000000000..010386cf4c8 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/shortcuts/GestureShortcutOptionControllerTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; +import android.view.accessibility.AccessibilityManager; + +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.testutils.AccessibilityTestUtils; +import com.android.settings.testutils.shadow.SettingsShadowResources; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Set; + +/** + * Tests for {@link GestureShortcutOptionController} + */ +@Config(shadows = SettingsShadowResources.class) +@RunWith(RobolectricTestRunner.class) +public class GestureShortcutOptionControllerTest { + private static final String PREF_KEY = "prefKey"; + private static final String TARGET = + new ComponentName("FakePackage", "FakeClass").flattenToString(); + private final Context mContext = spy(ApplicationProvider.getApplicationContext()); + private GestureShortcutOptionController mController; + private ShortcutOptionPreference mShortcutOptionPreference; + + private PreferenceScreen mPreferenceScreen; + + @Before + public void setUp() { + mController = new GestureShortcutOptionController( + mContext, PREF_KEY); + mController.setShortcutTargets(Set.of(TARGET)); + mShortcutOptionPreference = new ShortcutOptionPreference(mContext); + mShortcutOptionPreference.setKey(PREF_KEY); + mPreferenceScreen = new PreferenceManager(mContext).createPreferenceScreen(mContext); + mPreferenceScreen.addPreference(mShortcutOptionPreference); + AccessibilityTestUtils.setSoftwareShortcutMode( + mContext, /* gestureNavEnabled= */ true, /* floatingButtonEnabled= */ false); + enableTouchExploration(false); + } + + @Test + public void displayPreference_verifyTitle() { + mController.displayPreference(mPreferenceScreen); + + assertThat(mShortcutOptionPreference.getTitle().toString()).isEqualTo( + mContext.getString( + R.string.accessibility_shortcut_edit_dialog_title_software_by_gesture)); + } + + @Test + public void getSummary_touchExplorationDisabled_verifySummary() { + enableTouchExploration(false); + String expected = mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_software_gesture) + + "\n\n" + + mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_software_floating); + + assertThat(mController.getSummary().toString()).isEqualTo(expected); + } + + @Test + public void getSummary_touchExplorationEnabled_verifySummary() { + enableTouchExploration(true); + String expected = mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_software_gesture_talkback) + + "\n\n" + + mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_software_floating); + + assertThat(mController.getSummary().toString()).isEqualTo(expected); + } + + @Test + public void isShortcutAvailable_inSuw_returnFalse() { + mController.setInSetupWizard(true); + + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + @Test + public void isShortcutAvailable_notInSuwUseGestureNavSystemUseFab_returnFalse() { + mController.setInSetupWizard(false); + AccessibilityTestUtils.setSoftwareShortcutMode( + mContext, /* gestureNavEnabled= */ true, /* floatingButtonEnabled= */ true); + + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + @Test + public void isShortcutAvailable_notInSuwUseGestureNavSystemNotUseFab_returnTrue() { + mController.setInSetupWizard(false); + AccessibilityTestUtils.setSoftwareShortcutMode( + mContext, /* gestureNavEnabled= */ true, /* floatingButtonEnabled= */ false); + + assertThat(mController.isShortcutAvailable()).isTrue(); + } + + @Test + public void isShortcutAvailable_notInSuwUseButtonNavSystemUseFab_returnFalse() { + mController.setInSetupWizard(false); + AccessibilityTestUtils.setSoftwareShortcutMode( + mContext, /* gestureNavEnabled= */ false, /* floatingButtonEnabled= */ true); + + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + @Test + public void isShortcutAvailable_notInSuwUseButtonNavSystemNotUseFab_returnFalse() { + mController.setInSetupWizard(false); + AccessibilityTestUtils.setSoftwareShortcutMode( + mContext, /* gestureNavEnabled= */ false, /* floatingButtonEnabled= */ false); + + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + private void enableTouchExploration(boolean enable) { + AccessibilityManager am = mock(AccessibilityManager.class); + when(mContext.getSystemService(AccessibilityManager.class)).thenReturn(am); + when(am.isTouchExplorationEnabled()).thenReturn(enable); + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/shortcuts/NavButtonShortcutOptionControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/shortcuts/NavButtonShortcutOptionControllerTest.java new file mode 100644 index 00000000000..9106d0059cc --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/shortcuts/NavButtonShortcutOptionControllerTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; + +import android.content.ComponentName; +import android.content.Context; + +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.testutils.AccessibilityTestUtils; +import com.android.settings.testutils.shadow.SettingsShadowResources; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Set; + +/** + * Tests for {@link NavButtonShortcutOptionController} + */ +@Config(shadows = SettingsShadowResources.class) +@RunWith(RobolectricTestRunner.class) +public class NavButtonShortcutOptionControllerTest { + private static final String PREF_KEY = "prefKey"; + private static final String TARGET = + new ComponentName("FakePackage", "FakeClass").flattenToString(); + private final Context mContext = spy(ApplicationProvider.getApplicationContext()); + private NavButtonShortcutOptionController mController; + private ShortcutOptionPreference mShortcutOptionPreference; + + private PreferenceScreen mPreferenceScreen; + + @Before + public void setUp() { + mController = new NavButtonShortcutOptionController( + mContext, PREF_KEY); + mController.setShortcutTargets(Set.of(TARGET)); + mShortcutOptionPreference = new ShortcutOptionPreference(mContext); + mShortcutOptionPreference.setKey(PREF_KEY); + mPreferenceScreen = new PreferenceManager(mContext).createPreferenceScreen(mContext); + mPreferenceScreen.addPreference(mShortcutOptionPreference); + AccessibilityTestUtils.setSoftwareShortcutMode( + mContext, /* gestureNavEnabled= */ false, /* floatingButtonEnabled= */ false); + } + + @Test + public void displayPreference_verifyTitle() { + mController.displayPreference(mPreferenceScreen); + + assertThat(mShortcutOptionPreference.getTitle().toString()).isEqualTo( + mContext.getString( + R.string.accessibility_shortcut_edit_dialog_title_software)); + } + + @Test + public void displayPreference_inSuw_verifySummary() { + mController.setInSetupWizard(true); + mController.displayPreference(mPreferenceScreen); + + assertThat(mShortcutOptionPreference.getSummary().toString()).isEqualTo( + mContext.getString(R.string.accessibility_shortcut_edit_dialog_summary_software)); + } + + @Test + public void displayPreference_notInSuw_verifySummary() { + mController.setInSetupWizard(false); + String expected = mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_software) + + "\n\n" + + mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_software_floating); + + mController.displayPreference(mPreferenceScreen); + + assertThat(mShortcutOptionPreference.getSummary().toString()).isEqualTo(expected); + } + + @Test + public void isShortcutAvailable_useGestureNavSystemUseFab_returnFalse() { + AccessibilityTestUtils.setSoftwareShortcutMode( + mContext, /* gestureNavEnabled= */ true, /* floatingButtonEnabled= */ true); + + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + @Test + public void isShortcutAvailable_useGestureNavSystemNotUseFab_returnFalse() { + AccessibilityTestUtils.setSoftwareShortcutMode( + mContext, /* gestureNavEnabled= */ true, /* floatingButtonEnabled= */ false); + + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + @Test + public void isShortcutAvailable_useButtonNavSystemUseFab_returnFalse() { + AccessibilityTestUtils.setSoftwareShortcutMode( + mContext, /* gestureNavEnabled= */ false, /* floatingButtonEnabled= */ true); + + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + @Test + public void isShortcutAvailable_useButtonNavSystemNotUseFab_returnTrue() { + AccessibilityTestUtils.setSoftwareShortcutMode( + mContext, /* gestureNavEnabled= */ false, /* floatingButtonEnabled= */ false); + + assertThat(mController.isShortcutAvailable()).isTrue(); + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/shortcuts/ShortcutOptionPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/shortcuts/ShortcutOptionPreferenceControllerTest.java new file mode 100644 index 00000000000..d3d425ec570 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/shortcuts/ShortcutOptionPreferenceControllerTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; + +import java.util.Set; + +/** + * Tests for {@link ShortcutOptionPreferenceController} + */ +@RunWith(RobolectricTestRunner.class) +public class ShortcutOptionPreferenceControllerTest { + + private static final String PREF_KEY = "prefKey"; + private final Context mContext = ApplicationProvider.getApplicationContext(); + private ShortcutOptionPreference mShortcutOptionPreference; + private ShortcutOptionPreferenceController mController; + + @Before + public void setUp() { + mShortcutOptionPreference = spy(new ShortcutOptionPreference(mContext)); + mShortcutOptionPreference.setKey(PREF_KEY); + mController = spy(new ShortcutOptionPreferenceController(mContext, PREF_KEY) { + @Override + protected boolean isShortcutAvailable() { + return false; + } + + @Override + protected boolean isChecked() { + return false; + } + + @Override + protected void enableShortcutForTargets(boolean enable) { + // do nothing + } + }); + } + + @Test + public void updateState_shortcutControllerIsChecked_shouldSetPreferenceChecked() { + when(mController.isChecked()).thenReturn(true); + + mController.updateState(mShortcutOptionPreference); + + assertThat(mShortcutOptionPreference.isChecked()).isTrue(); + } + + @Test + public void updateState_shortcutControllerIsNotChecked_shouldSetPreferenceUnchecked() { + when(mController.isChecked()).thenReturn(false); + + mController.updateState(mShortcutOptionPreference); + + assertThat(mShortcutOptionPreference.isChecked()).isFalse(); + } + + @Test + public void getAvailabilityStatus_shortcutAvailable_returnAvailableUnsearchable() { + when(mController.isShortcutAvailable()).thenReturn(true); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE_UNSEARCHABLE); + } + + @Test + public void getAvailabilityStatus_shortcutUnavailable_returnConditionallyUnavailable() { + when(mController.isShortcutAvailable()).thenReturn(false); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void onPreferenceChanged_callEnableShortcutForTargets() { + mController.onPreferenceChange(mShortcutOptionPreference, true); + mController.onPreferenceChange(mShortcutOptionPreference, false); + + InOrder inOrder = Mockito.inOrder(mController); + inOrder.verify(mController).enableShortcutForTargets(true); + inOrder.verify(mController).enableShortcutForTargets(false); + } + + @Test + public void getShortcutTargets() { + Set targets = Set.of("target1", "target2"); + mController.setShortcutTargets(targets); + + assertThat(mController.getShortcutTargets()) + .containsExactlyElementsIn(targets); + } + + @Test + public void isInSetupWizard() { + mController.setInSetupWizard(true); + + assertThat(mController.isInSetupWizard()).isTrue(); + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/shortcuts/ShortcutOptionPreferenceTest.java b/tests/robotests/src/com/android/settings/accessibility/shortcuts/ShortcutOptionPreferenceTest.java new file mode 100644 index 00000000000..981b86d03b5 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/shortcuts/ShortcutOptionPreferenceTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; + +import androidx.preference.PreferenceViewHolder; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** + * Test for {@link ShortcutOptionPreferenceTest} + */ +@RunWith(RobolectricTestRunner.class) +public class ShortcutOptionPreferenceTest { + private final Context mContext = ApplicationProvider.getApplicationContext(); + private ShortcutOptionPreference mShortcutOptionPreference; + private PreferenceViewHolder mViewHolder; + private ImageView mImageView; + + @Before + public void setUp() { + mShortcutOptionPreference = new ShortcutOptionPreference(mContext); + int layoutId = mShortcutOptionPreference.getLayoutResource(); + View itemView = LayoutInflater.from(mContext).inflate(layoutId, /* root= */null); + mViewHolder = PreferenceViewHolder.createInstanceForTests(itemView); + mImageView = (ImageView) mViewHolder.findViewById(R.id.image); + } + + @Test + public void bindViewHolder_imageResNotSet_shouldHideImageView() { + mShortcutOptionPreference.onBindViewHolder(mViewHolder); + + assertThat(mImageView.getVisibility()).isEqualTo(View.GONE); + } + + @Test + public void bindViewHolder_imageResIdSet_shouldShowImageView() { + mShortcutOptionPreference.setIntroImageResId(R.drawable.a11y_shortcut_type_hardware); + + mShortcutOptionPreference.onBindViewHolder(mViewHolder); + + assertThat(mImageView.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void bindViewHolder_imageRawResIdSet_shouldShowImageView() { + mShortcutOptionPreference.setIntroImageRawResId( + com.android.settings.R.raw.accessibility_color_inversion_banner); + + mShortcutOptionPreference.onBindViewHolder(mViewHolder); + + assertThat(mImageView.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void bindViewHolder_shouldUpdateSummaryTextLineHeight() { + assertThat(mShortcutOptionPreference.getSummaryTextLineHeight()).isEqualTo(0); + + mShortcutOptionPreference.onBindViewHolder(mViewHolder); + + assertThat(mShortcutOptionPreference.getSummaryTextLineHeight()).isNotEqualTo(0); + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/shortcuts/SoftwareShortcutOptionPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/shortcuts/SoftwareShortcutOptionPreferenceControllerTest.java new file mode 100644 index 00000000000..1f7e0196af8 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/shortcuts/SoftwareShortcutOptionPreferenceControllerTest.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static com.android.internal.accessibility.common.ShortcutConstants.SERVICES_SEPARATOR; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.Application; +import android.content.ComponentName; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Build; +import android.provider.Settings; +import android.text.SpannableStringBuilder; +import android.view.View; +import android.view.accessibility.AccessibilityManager; + +import androidx.fragment.app.FragmentActivity; + +import com.android.internal.accessibility.common.ShortcutConstants; +import com.android.internal.accessibility.util.ShortcutUtils; +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.SubSettings; +import com.android.settings.accessibility.AccessibilityButtonFragment; +import com.android.settings.accessibility.FloatingMenuSizePreferenceController; +import com.android.settings.utils.AnnotationSpan; +import com.android.settingslib.accessibility.AccessibilityUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** + * Tests for {@link SoftwareShortcutOptionPreferenceController} + */ +@RunWith(RobolectricTestRunner.class) +public class SoftwareShortcutOptionPreferenceControllerTest { + private static final String PREF_KEY = "prefKey"; + private static final String TARGET_MAGNIFICATION = + "com.android.server.accessibility.MagnificationController"; + private static final ComponentName TARGET_ALWAYS_ON_A11Y_SERVICE = + new ComponentName("FakePackage", "AlwaysOnA11yService"); + private static final ComponentName TARGET_STANDARD_A11Y_SERVICE = + new ComponentName("FakePackage", "StandardA11yService"); + private static final String SOFTWARE_SHORTCUT_SETTING_NAME = + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS; + + private Context mContext; + private TestSoftwareShortcutOptionPreferenceController mController; + + @Before + public void setUp() { + mContext = spy(Robolectric.buildActivity(FragmentActivity.class).get()); + + AccessibilityServiceInfo mAlwaysOnServiceInfo = createAccessibilityServiceInfo( + TARGET_ALWAYS_ON_A11Y_SERVICE, /* isAlwaysOnService= */ true); + AccessibilityServiceInfo mStandardServiceInfo = createAccessibilityServiceInfo( + TARGET_STANDARD_A11Y_SERVICE, /* isAlwaysOnService= */ false); + AccessibilityManager am = mock(AccessibilityManager.class); + when(mContext.getSystemService(Context.ACCESSIBILITY_SERVICE)).thenReturn(am); + when(am.getInstalledAccessibilityServiceList()).thenReturn( + List.of(mAlwaysOnServiceInfo, mStandardServiceInfo)); + + mController = new TestSoftwareShortcutOptionPreferenceController(mContext, PREF_KEY); + mController.setShortcutTargets(Set.of(TARGET_MAGNIFICATION)); + } + + @Test + public void isChecked_allTargetsHasShortcutConfigured_returnTrue() { + Settings.Secure.putString( + mContext.getContentResolver(), SOFTWARE_SHORTCUT_SETTING_NAME, + String.join(String.valueOf(SERVICES_SEPARATOR), + TARGET_MAGNIFICATION, + TARGET_STANDARD_A11Y_SERVICE.flattenToString(), + TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString()) + ); + mController.setShortcutTargets( + Set.of(TARGET_MAGNIFICATION, + TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString(), + TARGET_STANDARD_A11Y_SERVICE.flattenToString())); + + assertThat(mController.isChecked()).isTrue(); + } + + @Test + public void isChecked_someTargetsHasShortcutConfigured_returnFalse() { + Settings.Secure.putString( + mContext.getContentResolver(), SOFTWARE_SHORTCUT_SETTING_NAME, + String.join(String.valueOf(SERVICES_SEPARATOR), + TARGET_MAGNIFICATION, + TARGET_STANDARD_A11Y_SERVICE.flattenToString()) + ); + mController.setShortcutTargets( + Set.of(TARGET_MAGNIFICATION, + TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString(), + TARGET_STANDARD_A11Y_SERVICE.flattenToString())); + + assertThat(mController.isChecked()).isFalse(); + } + + @Test + public void isChecked_noTargetsHasShortcutConfigured_returnFalse() { + Settings.Secure.putString( + mContext.getContentResolver(), SOFTWARE_SHORTCUT_SETTING_NAME, ""); + mController.setShortcutTargets( + Set.of(TARGET_MAGNIFICATION, + TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString(), + TARGET_STANDARD_A11Y_SERVICE.flattenToString())); + + assertThat(mController.isChecked()).isFalse(); + } + + @Test + public void getCustomizedAccessibilityButtonLink_verifyText() { + String expected = + mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_software_floating); + + CharSequence spannable = mController.getCustomizeAccessibilityButtonLink(); + + assertThat(spannable.toString()).isEqualTo(expected); + } + + @Test + public void getCustomizedAccessibilityButtonLink_verifyClickAction() { + String expected = + mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_software_floating); + + CharSequence spannable = mController.getCustomizeAccessibilityButtonLink(); + + assertThat(spannable).isInstanceOf(SpannableStringBuilder.class); + AnnotationSpan[] spans = ((SpannableStringBuilder) spannable).getSpans( + 0, expected.length(), AnnotationSpan.class); + spans[0].onClick(new View(mContext)); + assertLaunchSettingsPage(AccessibilityButtonFragment.class.getName()); + } + + @Test + public void enableShortcutForTargets_enableShortcut_shortcutTurnedOn() { + String target = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString(); + mController.setShortcutTargets(Set.of(target)); + assertThat(ShortcutUtils.isComponentIdExistingInSettings( + mContext, ShortcutConstants.UserShortcutType.SOFTWARE, target + )).isFalse(); + + mController.enableShortcutForTargets(true); + + assertThat(ShortcutUtils.isComponentIdExistingInSettings( + mContext, ShortcutConstants.UserShortcutType.SOFTWARE, target + )).isTrue(); + } + + @Test + public void enableShortcutForTargets_disableShortcut_shortcutTurnedOff() { + String target = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString(); + ShortcutUtils.optInValueToSettings( + mContext, ShortcutConstants.UserShortcutType.SOFTWARE, target); + assertThat(ShortcutUtils.isComponentIdExistingInSettings( + mContext, ShortcutConstants.UserShortcutType.SOFTWARE, target + )).isTrue(); + mController.setShortcutTargets(Set.of(target)); + + mController.enableShortcutForTargets(false); + + assertThat(ShortcutUtils.isComponentIdExistingInSettings( + mContext, ShortcutConstants.UserShortcutType.SOFTWARE, target + )).isFalse(); + } + + @Test + public void enableShortcutForTargets_enableShortcutWithMagnification_menuSizeIncreased() { + mController.setShortcutTargets(Set.of(TARGET_MAGNIFICATION)); + + mController.enableShortcutForTargets(true); + + assertThat( + Settings.Secure.getInt( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE, + FloatingMenuSizePreferenceController.Size.UNKNOWN)) + .isEqualTo(FloatingMenuSizePreferenceController.Size.LARGE); + } + + @Test + public void enableShortcutForTargets_enableShortcutWithMagnification_userConfigureSmallMenuSize_menuSizeNotChanged() { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE, + FloatingMenuSizePreferenceController.Size.SMALL); + mController.setShortcutTargets(Set.of(TARGET_MAGNIFICATION)); + + mController.enableShortcutForTargets(true); + + assertThat( + Settings.Secure.getInt( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE, + FloatingMenuSizePreferenceController.Size.UNKNOWN)) + .isEqualTo(FloatingMenuSizePreferenceController.Size.SMALL); + } + + @Test + public void enableShortcutForTargets_enableAlwaysOnServiceShortcut_turnsOnAlwaysOnService() { + mController.setShortcutTargets( + Set.of(TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString())); + + mController.enableShortcutForTargets(true); + + assertThat(AccessibilityUtils.getEnabledServicesFromSettings(mContext)) + .contains(TARGET_ALWAYS_ON_A11Y_SERVICE); + } + + @Test + public void enableShortcutForTargets_disableAlwaysOnServiceShortcut_turnsOffAlwaysOnService() { + mController.setShortcutTargets( + Set.of(TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString())); + + mController.enableShortcutForTargets(false); + + assertThat(AccessibilityUtils.getEnabledServicesFromSettings(mContext)) + .doesNotContain(TARGET_ALWAYS_ON_A11Y_SERVICE); + } + + @Test + public void enableShortcutForTargets_enableStandardServiceShortcut_wontTurnOnService() { + mController.setShortcutTargets( + Set.of(TARGET_STANDARD_A11Y_SERVICE.flattenToString())); + + mController.enableShortcutForTargets(true); + + assertThat(AccessibilityUtils.getEnabledServicesFromSettings(mContext)) + .doesNotContain(TARGET_STANDARD_A11Y_SERVICE); + } + + @Test + public void enableShortcutForTargets_disableStandardServiceShortcutWithServiceOn_wontTurnOffService() { + mController.setShortcutTargets( + Set.of(TARGET_STANDARD_A11Y_SERVICE.flattenToString())); + AccessibilityUtils.setAccessibilityServiceState( + mContext, TARGET_STANDARD_A11Y_SERVICE, /* enabled= */ true); + + mController.enableShortcutForTargets(false); + + assertThat(AccessibilityUtils.getEnabledServicesFromSettings(mContext)) + .contains(TARGET_STANDARD_A11Y_SERVICE); + } + + private void assertLaunchSettingsPage(String page) { + ContextWrapper applicationContext = (Application) mContext.getApplicationContext(); + final Intent intent = Shadows.shadowOf(applicationContext).getNextStartedActivity(); + assertThat(intent).isNotNull(); + assertThat(intent.getAction()).isEqualTo(Intent.ACTION_MAIN); + assertThat(intent.getComponent()).isEqualTo( + new ComponentName(applicationContext, SubSettings.class)); + assertThat(intent.getExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)).isEqualTo(page); + } + + private AccessibilityServiceInfo createAccessibilityServiceInfo( + ComponentName componentName, boolean isAlwaysOnService) { + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.targetSdkVersion = Build.VERSION_CODES.R; + final ServiceInfo serviceInfo = new ServiceInfo(); + applicationInfo.packageName = componentName.getPackageName(); + serviceInfo.packageName = componentName.getPackageName(); + serviceInfo.name = componentName.getClassName(); + serviceInfo.applicationInfo = applicationInfo; + + final ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.serviceInfo = serviceInfo; + try { + final AccessibilityServiceInfo info = new AccessibilityServiceInfo(resolveInfo, + mContext); + info.setComponentName(componentName); + if (isAlwaysOnService) { + info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON; + } + return info; + } catch (XmlPullParserException | IOException e) { + // Do nothing + } + return null; + } + + private static class TestSoftwareShortcutOptionPreferenceController + extends SoftwareShortcutOptionPreferenceController { + + TestSoftwareShortcutOptionPreferenceController( + Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + protected boolean isShortcutAvailable() { + return true; + } + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/shortcuts/TripleTapShortcutOptionControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/shortcuts/TripleTapShortcutOptionControllerTest.java new file mode 100644 index 00000000000..800640a84f4 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/shortcuts/TripleTapShortcutOptionControllerTest.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ComponentName; +import android.content.Context; +import android.icu.text.MessageFormat; +import android.provider.Settings; + +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.accessibility.AccessibilityUtil; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.Set; + +/** + * Tests for {@link TripleTapShortcutOptionController} + */ +@RunWith(RobolectricTestRunner.class) +public class TripleTapShortcutOptionControllerTest { + private static final String PREF_KEY = "prefKey"; + private static final String TARGET_MAGNIFICATION = + "com.android.server.accessibility.MagnificationController"; + private static final String TARGET_FAKE = + new ComponentName("FakePackage", "FakeClass").flattenToString(); + private final Context mContext = ApplicationProvider.getApplicationContext(); + private TripleTapShortcutOptionController mController; + private ShortcutOptionPreference mShortcutOptionPreference; + private PreferenceScreen mPreferenceScreen; + + @Before + public void setUp() { + mController = new TripleTapShortcutOptionController(mContext, PREF_KEY); + mController.setShortcutTargets(Set.of(TARGET_MAGNIFICATION)); + mShortcutOptionPreference = new ShortcutOptionPreference(mContext); + mShortcutOptionPreference.setKey(PREF_KEY); + mPreferenceScreen = new PreferenceManager(mContext).createPreferenceScreen(mContext); + mPreferenceScreen.addPreference(mShortcutOptionPreference); + } + + @Test + public void displayPreference_verifyScreenTestSet() { + mController.displayPreference(mPreferenceScreen); + + assertThat(mShortcutOptionPreference.getTitle().toString()).isEqualTo( + mContext.getString(R.string.accessibility_shortcut_edit_dialog_title_triple_tap)); + assertThat(mShortcutOptionPreference.getSummary().toString()).isEqualTo( + MessageFormat.format( + mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_triple_tap), + 3)); + } + + @Test + public void getAvailabilityStatus_targetIsMagnificationAndIsExpanded_returnsAvailableUnsearchable() { + mController.setExpanded(true); + mController.setShortcutTargets(Set.of(TARGET_MAGNIFICATION)); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE_UNSEARCHABLE); + } + + @Test + public void getAvailabilityStatus_targetIsMagnificationAndIsNotExpanded_returnsConditionallyUnavailable() { + mController.setExpanded(false); + mController.setShortcutTargets(Set.of(TARGET_MAGNIFICATION)); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void getAvailabilityStatus_targetIsNotMagnificationAndIsNotExpanded_returnsConditionallyUnavailable() { + mController.setExpanded(false); + mController.setShortcutTargets(Set.of(TARGET_FAKE)); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void getAvailabilityStatus_targetIsNotMagnificationAndIsExpanded_returnsConditionallyUnavailable() { + mController.setExpanded(true); + mController.setShortcutTargets(Set.of(TARGET_FAKE)); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void setExpanded_expand_updateExpandedValue() { + mController.setExpanded(true); + + assertThat(mController.isExpanded()).isTrue(); + } + + @Test + public void setExpanded_collapse_updateExpandedValue() { + mController.setExpanded(false); + + assertThat(mController.isExpanded()).isFalse(); + } + + @Test + public void isShortcutAvailable_multipleTargets_returnFalse() { + mController.setShortcutTargets(Set.of(TARGET_FAKE, TARGET_MAGNIFICATION)); + + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + @Test + public void isShortcutAvailable_magnificationTargetOnly_returnTrue() { + mController.setShortcutTargets(Set.of(TARGET_MAGNIFICATION)); + + assertThat(mController.isShortcutAvailable()).isTrue(); + } + + @Test + public void isShortcutAvailable_nonMagnificationTarget_returnFalse() { + mController.setShortcutTargets(Set.of(TARGET_FAKE)); + + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + @Test + public void isChecked_tripleTapConfigured_returnTrue() { + mController.enableShortcutForTargets(true); + + assertThat(mController.isChecked()).isTrue(); + } + + @Test + public void isChecked_tripleTapNotConfigured_returnFalse() { + mController.enableShortcutForTargets(false); + + assertThat(mController.isChecked()).isFalse(); + } + + @Test + public void enableShortcutForTargets_enableShortcut_settingUpdated() { + mController.enableShortcutForTargets(true); + + assertThat( + Settings.Secure.getInt( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, + AccessibilityUtil.State.OFF) + ).isEqualTo(AccessibilityUtil.State.ON); + } + + @Test + public void enableShortcutForTargets_disableShortcut_settingUpdated() { + mController.enableShortcutForTargets(false); + + assertThat( + Settings.Secure.getInt( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, + AccessibilityUtil.State.OFF) + ).isEqualTo(AccessibilityUtil.State.OFF); + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/shortcuts/TwoFingersDoubleTapShortcutOptionControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/shortcuts/TwoFingersDoubleTapShortcutOptionControllerTest.java new file mode 100644 index 00000000000..d27560b35c5 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/shortcuts/TwoFingersDoubleTapShortcutOptionControllerTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ComponentName; +import android.content.Context; +import android.icu.text.MessageFormat; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.provider.Settings; + +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.server.accessibility.Flags; +import com.android.settings.R; +import com.android.settings.accessibility.AccessibilityUtil; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.Set; + +/** + * Tests for {@link TwoFingersDoubleTapShortcutOptionController} + */ +@RunWith(RobolectricTestRunner.class) +public class TwoFingersDoubleTapShortcutOptionControllerTest { + private static final String PREF_KEY = "prefKey"; + private static final String TARGET_MAGNIFICATION = + "com.android.server.accessibility.MagnificationController"; + private static final String TARGET_FAKE = + new ComponentName("FakePackage", "FakeClass").flattenToString(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private final Context mContext = ApplicationProvider.getApplicationContext(); + private TwoFingersDoubleTapShortcutOptionController mController; + private ShortcutOptionPreference mShortcutOptionPreference; + + private PreferenceScreen mPreferenceScreen; + + @Before + public void setUp() { + mController = new TwoFingersDoubleTapShortcutOptionController(mContext, PREF_KEY); + mController.setShortcutTargets(Set.of(TARGET_MAGNIFICATION)); + mShortcutOptionPreference = new ShortcutOptionPreference(mContext); + mShortcutOptionPreference.setKey(PREF_KEY); + mPreferenceScreen = new PreferenceManager(mContext).createPreferenceScreen(mContext); + mPreferenceScreen.addPreference(mShortcutOptionPreference); + } + + @Test + public void displayPreference_verifyScreenTextSet() { + mController.displayPreference(mPreferenceScreen); + + assertThat(mShortcutOptionPreference.getTitle().toString()).isEqualTo( + mContext.getString( + R.string.accessibility_shortcut_edit_dialog_title_two_finger_double_tap)); + assertThat(mShortcutOptionPreference.getSummary().toString()).isEqualTo( + MessageFormat.format(mContext.getString( + R.string.accessibility_shortcut_edit_dialog_summary_two_finger_double_tap), + 2)); + } + + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) + @Test + public void isShortcutAvailable_featureFlagTurnedOff_returnFalse() { + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) + @Test + public void isShortcutAvailable_multipleTargets_returnFalse() { + mController.setShortcutTargets(Set.of(TARGET_FAKE, TARGET_MAGNIFICATION)); + + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) + @Test + public void isShortcutAvailable_magnificationTargetOnly_returnTrue() { + mController.setShortcutTargets(Set.of(TARGET_MAGNIFICATION)); + + assertThat(mController.isShortcutAvailable()).isTrue(); + } + + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) + @Test + public void isShortcutAvailable_nonMagnificationTarget_returnFalse() { + mController.setShortcutTargets(Set.of(TARGET_FAKE)); + + assertThat(mController.isShortcutAvailable()).isFalse(); + } + + @Test + public void isChecked_twoFingersDoubleTapConfigured_returnTrue() { + mController.enableShortcutForTargets(true); + + assertThat(mController.isChecked()).isTrue(); + } + + @Test + public void isChecked_twoFingersDoubleTapNotConfigured_returnFalse() { + mController.enableShortcutForTargets(false); + + assertThat(mController.isChecked()).isFalse(); + } + + @Test + public void enableShortcutForTargets_enableShortcut_settingUpdated() { + mController.enableShortcutForTargets(true); + + assertThat( + Settings.Secure.getInt( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED, + AccessibilityUtil.State.OFF) + ).isEqualTo(AccessibilityUtil.State.ON); + } + + @Test + public void enableShortcutForTargets_disableShortcut_settingUpdated() { + mController.enableShortcutForTargets(false); + + assertThat( + Settings.Secure.getInt( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED, + AccessibilityUtil.State.OFF) + ).isEqualTo(AccessibilityUtil.State.OFF); + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/shortcuts/VolumeKeysShortcutOptionControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/shortcuts/VolumeKeysShortcutOptionControllerTest.java new file mode 100644 index 00000000000..48a90a7e6a6 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/shortcuts/VolumeKeysShortcutOptionControllerTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 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.settings.accessibility.shortcuts; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ComponentName; +import android.content.Context; + +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.internal.accessibility.common.ShortcutConstants; +import com.android.internal.accessibility.util.ShortcutUtils; +import com.android.settings.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.Set; + +/** + * Tests for {@link VolumeKeysShortcutOptionController} + */ +@RunWith(RobolectricTestRunner.class) +public class VolumeKeysShortcutOptionControllerTest { + + private static final String PREF_KEY = "prefKey"; + private static final String TARGET = + new ComponentName("FakePackage", "FakeClass").flattenToString(); + private final Context mContext = ApplicationProvider.getApplicationContext(); + private VolumeKeysShortcutOptionController mController; + private ShortcutOptionPreference mShortcutOptionPreference; + + private PreferenceScreen mPreferenceScreen; + + @Before + public void setUp() { + mController = new VolumeKeysShortcutOptionController( + mContext, PREF_KEY); + mController.setShortcutTargets(Set.of(TARGET)); + mShortcutOptionPreference = new ShortcutOptionPreference(mContext); + mShortcutOptionPreference.setKey(PREF_KEY); + mPreferenceScreen = new PreferenceManager(mContext).createPreferenceScreen(mContext); + mPreferenceScreen.addPreference(mShortcutOptionPreference); + } + + @Test + public void displayPreference_verifyScreenTextSet() { + mController.displayPreference(mPreferenceScreen); + + assertThat(mShortcutOptionPreference.getTitle().toString()).isEqualTo( + mContext.getString(R.string.accessibility_shortcut_edit_dialog_title_hardware)); + assertThat(mShortcutOptionPreference.getSummary().toString()).isEqualTo( + mContext.getString(R.string.accessibility_shortcut_edit_dialog_summary_hardware)); + } + + @Test + public void isShortcutAvailable_returnsTrue() { + assertThat(mController.isShortcutAvailable()).isTrue(); + } + + @Test + public void isChecked_targetUseVolumeKeyShortcut_returnTrue() { + ShortcutUtils.optInValueToSettings( + mContext, ShortcutConstants.UserShortcutType.HARDWARE, TARGET); + + assertThat(mController.isChecked()).isTrue(); + } + + @Test + public void isChecked_targetNotUseVolumeKeyShortcut_returnFalse() { + ShortcutUtils.optOutValueFromSettings( + mContext, ShortcutConstants.UserShortcutType.HARDWARE, TARGET); + + assertThat(mController.isChecked()).isFalse(); + } + + @Test + public void enableShortcutForTargets_enableVolumeKeysShortcut_shortcutSet() { + mController.enableShortcutForTargets(true); + + assertThat( + ShortcutUtils.isComponentIdExistingInSettings( + mContext, ShortcutConstants.UserShortcutType.HARDWARE, TARGET)).isTrue(); + } + + @Test + public void enableShortcutForTargets_disableVolumeKeysShortcut_shortcutNotSet() { + mController.enableShortcutForTargets(false); + + assertThat( + ShortcutUtils.isComponentIdExistingInSettings( + mContext, ShortcutConstants.UserShortcutType.HARDWARE, TARGET)).isFalse(); + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/AccessibilityTestUtils.java b/tests/robotests/src/com/android/settings/testutils/AccessibilityTestUtils.java new file mode 100644 index 00000000000..8cda2d94686 --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/AccessibilityTestUtils.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 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.settings.testutils; + +import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; + +import android.content.Context; +import android.provider.Settings; + +import com.android.settings.testutils.shadow.SettingsShadowResources; + +/** + * Utility class for common methods used in the accessibility feature related tests + */ +public class AccessibilityTestUtils { + + public static void setSoftwareShortcutMode( + Context context, boolean gestureNavEnabled, boolean floatingButtonEnabled) { + int mode = floatingButtonEnabled ? ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU : -1; + + Settings.Secure.putInt(context.getContentResolver(), + Settings.Secure.ACCESSIBILITY_BUTTON_MODE, mode); + + if (gestureNavEnabled) { + SettingsShadowResources.overrideResource( + com.android.internal.R.integer.config_navBarInteractionMode, + NAV_BAR_MODE_GESTURAL); + } else { + SettingsShadowResources.overrideResource( + com.android.internal.R.integer.config_navBarInteractionMode, + NAV_BAR_MODE_3BUTTON); + } + } +}