From f4dfdda4b78a75056c7feba2e411c8a029a667f2 Mon Sep 17 00:00:00 2001 From: Menghan Li Date: Tue, 31 Dec 2024 02:18:38 +0000 Subject: [PATCH 1/3] Add a new API: checkSurveyAvailable This change introduces a new API within SettingsIntelligence to enable SettingsGoogle to identify available surveys for Pixel device users. This API will be used by SettingsGoogle to determine if there are any surveys available for a user to take. Bug: 380346685 Flag: com.android.server.accessibility.enable_low_vision_hats Test: atest SurveyFeatureProviderImplTest Change-Id: Ia4e694b6c8a240af4422baff61386d9ee5ffc346 --- .../settings/overlay/SurveyFeatureProvider.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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); } From 3f67747684214cadf886d55b4caea84b7bdea4c3 Mon Sep 17 00:00:00 2001 From: Menghan Li Date: Fri, 3 Jan 2025 12:13:01 +0000 Subject: [PATCH 2/3] Add controller for magnification feedback preference The controller's logic determines the visibility of this preference based on the availability of surveys, as reported by SurveyFeatureProvider.isSurveyAvailable. Bug: 380346799 Test: atest MagnificationFeedbackPreferenceControllerTest Flag: com.android.server.accessibility.enable_low_vision_hats Change-Id: I7fe7aa4418a6be38e9e7af7efc76a9a25266198b --- res/values/strings.xml | 6 + ...ificationFeedbackPreferenceController.java | 85 ++++++++++++ ...ationFeedbackPreferenceControllerTest.java | 121 ++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 src/com/android/settings/accessibility/MagnificationFeedbackPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/accessibility/MagnificationFeedbackPreferenceControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 236a66183ce..5163dc67e32 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -5084,6 +5084,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/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); + } +} From e2182809022ccb75597dcbb9544785ae99336dd9 Mon Sep 17 00:00:00 2001 From: Menghan Li Date: Thu, 9 Jan 2025 07:00:17 +0000 Subject: [PATCH 3/3] Add HaTS entrypoint for Magnification page This entry point allows users to access and adjust Magnification settings for low vision accessibility. Visibility is controlled by the aconfig flag and SurveyFeatureProvider#isSurveyAvailable. NO_IFTTT=Revisit preference_list scope Bug: 380346799 Test: atest ToggleScreenMagnificationPreferenceFragmentTest Flag: com.android.server.accessibility.enable_low_vision_hats Change-Id: I81a55487734fe7b139391a6c95834c7313e54d7a --- ...ScreenMagnificationPreferenceFragment.java | 56 +++++++++++---- ...enMagnificationPreferenceFragmentTest.java | 68 +++++++++++++++++-- 2 files changed, 103 insertions(+), 21 deletions(-) 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/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); }