diff --git a/res/values/strings.xml b/res/values/strings.xml index c3fb2f94ead..9aa9d8485dc 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -5088,6 +5088,12 @@ Timing controls System controls + + Feedback + + Help improve by taking a survey + + No surveys available Downloaded apps diff --git a/src/com/android/settings/accessibility/MagnificationFeedbackPreferenceController.java b/src/com/android/settings/accessibility/MagnificationFeedbackPreferenceController.java new file mode 100644 index 00000000000..bfcd2930513 --- /dev/null +++ b/src/com/android/settings/accessibility/MagnificationFeedbackPreferenceController.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2025 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; + +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.overlay.SurveyFeatureProvider; + +/** + * PreferenceController for magnification feedback preference. This controller manages the + * visibility and click behavior of the preference based on the availability of a user survey + * related to magnification. + */ +public class MagnificationFeedbackPreferenceController extends BasePreferenceController + implements DefaultLifecycleObserver { + private static final String TAG = "MagnificationFeedbackPreferenceController"; + public static final String PREF_KEY = "magnification_feedback"; + public static final String FEEDBACK_KEY = "A11yMagnificationUser"; + private final DashboardFragment mParent; + private final @Nullable SurveyFeatureProvider mSurveyFeatureProvider; + + public MagnificationFeedbackPreferenceController(@NonNull Context context, + @NonNull DashboardFragment parent, @NonNull String preferenceKey) { + super(context, preferenceKey); + mParent = parent; + mSurveyFeatureProvider = + FeatureFactory.getFeatureFactory().getSurveyFeatureProvider(context); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void updateState(@NonNull Preference preference) { + super.updateState(preference); + if (mSurveyFeatureProvider != null) { + mSurveyFeatureProvider.checkSurveyAvailable( + mParent.getViewLifecycleOwner(), + FEEDBACK_KEY, + enabled -> { + final String summary = mContext.getString(enabled + ? R.string.accessibility_feedback_summary + : R.string.accessibility_feedback_disabled_summary); + preference.setSummary(summary); + preference.setEnabled(enabled); + }); + } else { + Log.w(TAG, "SurveyFeatureProvider is not ready"); + } + } + + @Override + public boolean handlePreferenceTreeClick(@NonNull Preference preference) { + if (mSurveyFeatureProvider != null) { + mSurveyFeatureProvider.sendActivityIfAvailable(FEEDBACK_KEY); + } + return true; + } +} diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index 73b31c33d6f..fef83ae2ef1 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -196,20 +196,17 @@ public class ToggleScreenMagnificationPreferenceFragment extends @Override protected void initSettingsPreference() { - // If the device doesn't support window magnification feature, it should hide the - // settings preference. - if (!isWindowMagnificationSupported(getContext())) { - return; - } - final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY); - // LINT.IfChange(preference_list) - addMagnificationModeSetting(generalCategory); - addFollowTypingSetting(generalCategory); - addOneFingerPanningSetting(generalCategory); - addAlwaysOnSetting(generalCategory); - addJoystickSetting(generalCategory); - // LINT.ThenChange(search_data) + if (isWindowMagnificationSupported(getContext())) { + // LINT.IfChange(preference_list) + addMagnificationModeSetting(generalCategory); + addFollowTypingSetting(generalCategory); + addOneFingerPanningSetting(generalCategory); + addAlwaysOnSetting(generalCategory); + addJoystickSetting(generalCategory); + // LINT.ThenChange(:search_data) + } + addFeedbackSetting(generalCategory); } @Override @@ -346,6 +343,14 @@ public class ToggleScreenMagnificationPreferenceFragment extends return pref; } + private static Preference createFeedbackPreference(Context context) { + final Preference pref = new Preference(context); + pref.setTitle(R.string.accessibility_feedback_title); + pref.setSummary(R.string.accessibility_feedback_summary); + pref.setKey(MagnificationFeedbackPreferenceController.PREF_KEY); + return pref; + } + private static boolean isJoystickSupported() { return DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_WINDOW_MANAGER, @@ -371,6 +376,21 @@ public class ToggleScreenMagnificationPreferenceFragment extends addPreferenceController(joystickPreferenceController); } + private void addFeedbackSetting(PreferenceCategory generalCategory) { + if (!Flags.enableLowVisionHats()) { + return; + } + + final Preference feedbackPreference = createFeedbackPreference(getPrefContext()); + generalCategory.addPreference(feedbackPreference); + + final MagnificationFeedbackPreferenceController magnificationFeedbackPreferenceController = + new MagnificationFeedbackPreferenceController(getContext(), this, + MagnificationFeedbackPreferenceController.PREF_KEY); + magnificationFeedbackPreferenceController.displayPreference(getPreferenceScreen()); + addPreferenceController(magnificationFeedbackPreferenceController); + } + @Override public void showDialog(int dialogId) { super.showDialog(dialogId); @@ -773,7 +793,8 @@ public class ToggleScreenMagnificationPreferenceFragment extends createFollowTypingPreference(context), createOneFingerPanningPreference(context), createAlwaysOnPreference(context), - createJoystickPreference(context) + createJoystickPreference(context), + createFeedbackPreference(context) ) .forEach(pref -> rawData.add(createPreferenceSearchData(context, pref))); @@ -810,9 +831,14 @@ public class ToggleScreenMagnificationPreferenceFragment extends niks.add(MagnificationJoystickPreferenceController.PREF_KEY); } } + + if (!Flags.enableLowVisionHats()) { + niks.add(MagnificationFeedbackPreferenceController.PREF_KEY); + } + return niks; } - // LINT.ThenChange(preference_list) + // LINT.ThenChange(:preference_list) private SearchIndexableRaw createPreferenceSearchData( Context context, Preference pref) { diff --git a/src/com/android/settings/overlay/SurveyFeatureProvider.java b/src/com/android/settings/overlay/SurveyFeatureProvider.java index ce5be981282..85d123d5958 100644 --- a/src/com/android/settings/overlay/SurveyFeatureProvider.java +++ b/src/com/android/settings/overlay/SurveyFeatureProvider.java @@ -19,7 +19,10 @@ import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.Consumer; +import androidx.lifecycle.LifecycleOwner; import androidx.localbroadcastmanager.content.LocalBroadcastManager; /** @@ -109,4 +112,14 @@ public interface SurveyFeatureProvider { * @param simpleKey The simple name of the key to get the surveyId for. */ void sendActivityIfAvailable(String simpleKey); + + /** + * Checks if a survey is available for the given key by binding to the survey service. + * + * @param lifecycleOwner The lifecycle owner to manage the service connection. + * @param simpleKey The simple name of the key to get the surveyId for. + * @param listener The callback to be invoked when the survey availability is checked. + */ + void checkSurveyAvailable(@NonNull LifecycleOwner lifecycleOwner, @NonNull String simpleKey, + @NonNull Consumer listener); } diff --git a/tests/robotests/src/com/android/settings/accessibility/MagnificationFeedbackPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/MagnificationFeedbackPreferenceControllerTest.java new file mode 100644 index 00000000000..389e12756bf --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/MagnificationFeedbackPreferenceControllerTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2025 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; + +import static com.android.settings.accessibility.MagnificationFeedbackPreferenceController.FEEDBACK_KEY; +import static com.android.settings.accessibility.MagnificationFeedbackPreferenceController.PREF_KEY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; + +import androidx.core.util.Consumer; +import androidx.preference.Preference; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.overlay.SurveyFeatureProvider; +import com.android.settings.testutils.FakeFeatureFactory; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link MagnificationFeedbackPreferenceController}. */ +@RunWith(RobolectricTestRunner.class) +public class MagnificationFeedbackPreferenceControllerTest { + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + private final Context mContext = ApplicationProvider.getApplicationContext(); + @Mock private PreferenceScreen mScreen; + @Mock private PreferenceManager mPreferenceManager; + @Mock private DashboardFragment mFragment; + private SurveyFeatureProvider mSurveyFeatureProvider; + private MagnificationFeedbackPreferenceController mController; + private Preference mPreference; + + @Before + public void setUp() { + FakeFeatureFactory.setupForTest(); + mSurveyFeatureProvider = + FakeFeatureFactory.getFeatureFactory().getSurveyFeatureProvider(mContext); + mController = new MagnificationFeedbackPreferenceController(mContext, mFragment, PREF_KEY); + mPreference = new Preference(mContext); + when(mFragment.getPreferenceManager()).thenReturn(mPreferenceManager); + when(mPreferenceManager.findPreference(PREF_KEY)).thenReturn(mPreference); + when(mFragment.getPreferenceScreen()).thenReturn(mScreen); + } + + @Test + public void getAvailabilityStatus_shouldAlwaysBeAvailable() { + assertThat(mController.getAvailabilityStatus()).isEqualTo( + MagnificationFeedbackPreferenceController.AVAILABLE); + } + + @Test + public void updateState_surveyAvailable_preferenceEnabledWithSummary() { + doAnswer(invocation -> { + Consumer consumer = invocation.getArgument(2); + consumer.accept(true); + return null; + }).when(mSurveyFeatureProvider).checkSurveyAvailable(any(), eq(FEEDBACK_KEY), any()); + + mController.updateState(mPreference); + + assertThat(mPreference.isEnabled()).isTrue(); + assertThat(mPreference.getSummary()).isEqualTo( + mContext.getString(R.string.accessibility_feedback_summary)); + } + + @Test + public void updateState_surveyUnavailable_preferenceDisabledWithSummary() { + doAnswer(invocation -> { + Consumer consumer = invocation.getArgument(2); + consumer.accept(false); + return null; + }).when(mSurveyFeatureProvider).checkSurveyAvailable(any(), eq(FEEDBACK_KEY), any()); + + mController.updateState(mPreference); + + assertThat(mPreference.isEnabled()).isFalse(); + assertThat(mPreference.getSummary()).isEqualTo( + mContext.getString(R.string.accessibility_feedback_disabled_summary)); + } + + @Test + public void handlePreferenceTreeClick_shouldStartSurvey() { + mController.handlePreferenceTreeClick(mPreference); + + verify(mSurveyFeatureProvider).sendActivityIfAvailable(FEEDBACK_KEY); + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java index 0b385941997..863452fb645 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java @@ -336,6 +336,26 @@ public class ToggleScreenMagnificationPreferenceFragmentTest { assertThat(switchPreference.isChecked()).isFalse(); } + @Test + @EnableFlags(Flags.FLAG_ENABLE_LOW_VISION_HATS) + public void onResume_enableLowVisionHaTS_feedbackPreferenceShouldReturnNotNull() { + mFragController.create(R.id.main_content, /* bundle= */ null).start().resume(); + + final Preference feedbackPreference = mFragController.get().findPreference( + MagnificationFeedbackPreferenceController.PREF_KEY); + assertThat(feedbackPreference).isNotNull(); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_LOW_VISION_HATS) + public void onResume_disableLowVisionHaTS_feedbackPreferenceShouldReturnNull() { + mFragController.create(R.id.main_content, /* bundle= */ null).start().resume(); + + final Preference feedbackPreference = mFragController.get().findPreference( + MagnificationFeedbackPreferenceController.PREF_KEY); + assertThat(feedbackPreference).isNull(); + } + @Test public void onResume_haveRegisterToSpecificUris() { ShadowContentResolver shadowContentResolver = Shadows.shadowOf( @@ -893,13 +913,14 @@ public class ToggleScreenMagnificationPreferenceFragmentTest { @Test @EnableFlags(com.android.settings.accessibility.Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH) public void getRawDataToIndex_returnsAllPreferenceKeys() { - List expectedSearchKeys = List.of( + final List expectedSearchKeys = List.of( KEY_MAGNIFICATION_SHORTCUT_PREFERENCE, MagnificationModePreferenceController.PREF_KEY, MagnificationFollowTypingPreferenceController.PREF_KEY, MagnificationOneFingerPanningPreferenceController.PREF_KEY, MagnificationAlwaysOnPreferenceController.PREF_KEY, - MagnificationJoystickPreferenceController.PREF_KEY); + MagnificationJoystickPreferenceController.PREF_KEY, + MagnificationFeedbackPreferenceController.PREF_KEY); final List rawData = ToggleScreenMagnificationPreferenceFragment .SEARCH_INDEX_DATA_PROVIDER.getRawDataToIndex(mContext, true); @@ -910,8 +931,7 @@ public class ToggleScreenMagnificationPreferenceFragmentTest { @Test @EnableFlags(com.android.settings.accessibility.Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH) - public void - getNonIndexableKeys_windowMagnificationNotSupported_onlyShortcutPreferenceSearchable() { + public void getNonIndexableKeys_windowMagnificationNotSupported_onlyShortcutSearchable() { setWindowMagnificationSupported(false, false); final List niks = ToggleScreenMagnificationPreferenceFragment @@ -920,7 +940,8 @@ public class ToggleScreenMagnificationPreferenceFragmentTest { .SEARCH_INDEX_DATA_PROVIDER.getRawDataToIndex(mContext, true); // Expect all search data, except the shortcut preference, to be in NIKs. final List expectedNiks = rawData.stream().map(raw -> raw.key) - .filter(key -> !key.equals(KEY_MAGNIFICATION_SHORTCUT_PREFERENCE)).toList(); + .filter(key -> !key.equals(KEY_MAGNIFICATION_SHORTCUT_PREFERENCE)) + .toList(); // In NonIndexableKeys == not searchable assertThat(niks).containsExactlyElementsIn(expectedNiks); @@ -929,7 +950,32 @@ public class ToggleScreenMagnificationPreferenceFragmentTest { @Test @EnableFlags({ com.android.settings.accessibility.Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH, - Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE}) + Flags.FLAG_ENABLE_LOW_VISION_HATS}) + public void + getNonIndexableKeys_windowMagnificationNotSupportedHatsOn_shortcutFeedbackSearchable() { + setWindowMagnificationSupported(false, false); + + final List niks = ToggleScreenMagnificationPreferenceFragment + .SEARCH_INDEX_DATA_PROVIDER.getNonIndexableKeys(mContext); + final List rawData = ToggleScreenMagnificationPreferenceFragment + .SEARCH_INDEX_DATA_PROVIDER.getRawDataToIndex(mContext, true); + // Expect all search data, except the shortcut preference and feedback preference, to be in + // NIKs. + final List expectedNiks = rawData.stream().map(raw -> raw.key) + .filter(key -> + !key.equals(KEY_MAGNIFICATION_SHORTCUT_PREFERENCE) + && !key.equals(MagnificationFeedbackPreferenceController.PREF_KEY)) + .toList(); + + // In NonIndexableKeys == not searchable + assertThat(niks).containsExactlyElementsIn(expectedNiks); + } + + @Test + @EnableFlags({ + com.android.settings.accessibility.Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH, + Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE, + Flags.FLAG_ENABLE_LOW_VISION_HATS}) public void getNonIndexableKeys_hasShortcutAndAllFeaturesEnabled_allItemsSearchable() { setMagnificationTripleTapEnabled(true); setAlwaysOnSupported(true); @@ -991,6 +1037,16 @@ public class ToggleScreenMagnificationPreferenceFragmentTest { assertThat(niks).contains(MagnificationJoystickPreferenceController.PREF_KEY); } + @Test + @DisableFlags(Flags.FLAG_ENABLE_LOW_VISION_HATS) + public void getNonIndexableKeys_hatsNotSupported_notSearchable() { + final List niks = ToggleScreenMagnificationPreferenceFragment + .SEARCH_INDEX_DATA_PROVIDER.getNonIndexableKeys(mContext); + + // In NonIndexableKeys == not searchable + assertThat(niks).contains(MagnificationFeedbackPreferenceController.PREF_KEY); + } + private void putStringIntoSettings(String key, String componentName) { Settings.Secure.putString(mContext.getContentResolver(), key, componentName); }