From fc29b1c331db4a5c65b27411417edd3d8c790197 Mon Sep 17 00:00:00 2001 From: Leon Liao Date: Tue, 13 Nov 2018 20:31:54 +0800 Subject: [PATCH] Implement a11y UI for Accessibility timeout feature. This UI control the Accessibility Timeout API in Settings App. Add a user control to set timeout length to suit their need. Bug: 119283926 Test: atest AccessibilityTimeoutControllerTest Test: make RunSettingsRoboTests -j ROBOTEST_FILTER=CodeInspectionTest Test: atest UniquePreferenceTest Change-Id: Ic8f7337e6bd7739c1189382cc87d45465b64c6ed --- res/values/arrays.xml | 27 +++ res/values/strings.xml | 18 ++ ...accessibility_content_timeout_settings.xml | 63 +++++++ ...accessibility_control_timeout_settings.xml | 63 +++++++ res/xml/accessibility_settings.xml | 15 ++ ...ilityContentTimeoutPreferenceFragment.java | 130 ++++++++++++++ ...ilityControlTimeoutPreferenceFragment.java | 130 ++++++++++++++ .../AccessibilityTimeoutController.java | 167 ++++++++++++++++++ .../AccessibilityTimeoutControllerTest.java | 120 +++++++++++++ 9 files changed, 733 insertions(+) create mode 100644 res/xml/accessibility_content_timeout_settings.xml create mode 100644 res/xml/accessibility_control_timeout_settings.xml create mode 100644 src/com/android/settings/accessibility/AccessibilityContentTimeoutPreferenceFragment.java create mode 100644 src/com/android/settings/accessibility/AccessibilityControlTimeoutPreferenceFragment.java create mode 100644 src/com/android/settings/accessibility/AccessibilityTimeoutController.java create mode 100644 tests/robotests/src/com/android/settings/accessibility/AccessibilityTimeoutControllerTest.java diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 4bd86dce0d6..80224bd2fec 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -697,6 +697,33 @@ Accessibility volume + + + accessibility_content_timeout_default + accessibility_content_timeout_10secs + accessibility_content_timeout_30secs + accessibility_content_timeout_1min + accessibility_content_timeout_2mins + + + + + accessibility_control_timeout_default + accessibility_control_timeout_10secs + accessibility_control_timeout_30secs + accessibility_control_timeout_1min + accessibility_control_timeout_2mins + + + + + 0 + 10000 + 30000 + 60000 + 120000 + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 9e5cb2ade08..b67618c537d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4575,6 +4575,24 @@ Mono audio Combine channels when playing audio + + + Default + + 10 seconds + + 30 seconds + + 1 minute + + 2 minutes + + Content Timeout + + Control Timeout + + Choose how long it takes before automatically disappearing messages go away.\nSome apps may not support this setting yet. + Touch & hold delay diff --git a/res/xml/accessibility_content_timeout_settings.xml b/res/xml/accessibility_content_timeout_settings.xml new file mode 100644 index 00000000000..51573c958e5 --- /dev/null +++ b/res/xml/accessibility_content_timeout_settings.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/res/xml/accessibility_control_timeout_settings.xml b/res/xml/accessibility_control_timeout_settings.xml new file mode 100644 index 00000000000..45596503f32 --- /dev/null +++ b/res/xml/accessibility_control_timeout_settings.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/res/xml/accessibility_settings.xml b/res/xml/accessibility_settings.xml index a76bef7f185..1cd4a4816b1 100644 --- a/res/xml/accessibility_settings.xml +++ b/res/xml/accessibility_settings.xml @@ -71,6 +71,14 @@ + + + + + sControllers = new ArrayList<>(); + + @Override + public void onCheckedChanged(Preference preference) { + for (AbstractPreferenceController controller : sControllers) { + controller.updateState(preference); + } + } + + @Override + public void onResume() { + super.onResume(); + + for (AbstractPreferenceController controller : + buildPreferenceControllers(getPrefContext(), getSettingsLifecycle())) { + ((AccessibilityTimeoutController)controller).setOnChangeListener(this); + } + } + + @Override + public void onPause() { + super.onPause(); + + for (AbstractPreferenceController controller : + buildPreferenceControllers(getPrefContext(), getSettingsLifecycle())) { + ((AccessibilityTimeoutController)controller).setOnChangeListener(null); + } + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.ACCESSIBILITY; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.accessibility_content_timeout_settings; + } + + @Override + protected List createPreferenceControllers(Context context) { + return buildPreferenceControllers(context, getSettingsLifecycle()); + } + + private static List buildPreferenceControllers(Context context, + Lifecycle lifecycle) { + if (sControllers.size() == 0) { + Resources resources = context.getResources(); + + String[] timeoutKeys = resources.getStringArray( + R.array.accessibility_timeout_content_selector_keys); + + for (int i=0; i < timeoutKeys.length; i++) { + sControllers.add(new AccessibilityTimeoutController( + context, lifecycle, timeoutKeys[i], TAG)); + } + } + return sControllers; + } + + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List getXmlResourcesToIndex(Context context, + boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.accessibility_content_timeout_settings; + return Arrays.asList(sir); + } + + @Override + public List getNonIndexableKeys(Context context) { + final List keys = super.getNonIndexableKeys(context); + return keys; + } + + @Override + public List createPreferenceControllers( + Context context) { + return buildPreferenceControllers(context, null); + } + }; +} \ No newline at end of file diff --git a/src/com/android/settings/accessibility/AccessibilityControlTimeoutPreferenceFragment.java b/src/com/android/settings/accessibility/AccessibilityControlTimeoutPreferenceFragment.java new file mode 100644 index 00000000000..56424af36c0 --- /dev/null +++ b/src/com/android/settings/accessibility/AccessibilityControlTimeoutPreferenceFragment.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2018 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.provider.SearchIndexableResource; +import android.content.Context; +import android.content.res.Resources; + +import androidx.preference.Preference; + +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@SearchIndexable +public final class AccessibilityControlTimeoutPreferenceFragment extends DashboardFragment + implements AccessibilityTimeoutController.OnChangeListener { + + static final String TAG = "AccessibilityControlTimeoutPreferenceFragment"; + private static final List sControllers = new ArrayList<>(); + + @Override + public void onCheckedChanged(Preference preference) { + for (AbstractPreferenceController controller : sControllers) { + controller.updateState(preference); + } + } + + @Override + public void onResume() { + super.onResume(); + + for (AbstractPreferenceController controller : + buildPreferenceControllers(getPrefContext(), getSettingsLifecycle())) { + ((AccessibilityTimeoutController)controller).setOnChangeListener(this); + } + } + + @Override + public void onPause() { + super.onPause(); + + for (AbstractPreferenceController controller : + buildPreferenceControllers(getPrefContext(), getSettingsLifecycle())) { + ((AccessibilityTimeoutController)controller).setOnChangeListener(null); + } + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.ACCESSIBILITY; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.accessibility_control_timeout_settings; + } + + @Override + protected List createPreferenceControllers(Context context) { + return buildPreferenceControllers(context, getSettingsLifecycle()); + } + + private static List buildPreferenceControllers(Context context, + Lifecycle lifecycle) { + if (sControllers.size() == 0) { + Resources resources = context.getResources(); + + String[] timeoutKeys = resources.getStringArray( + R.array.accessibility_timeout_control_selector_keys); + + for (int i=0; i < timeoutKeys.length; i++) { + sControllers.add(new AccessibilityTimeoutController( + context, lifecycle, timeoutKeys[i], TAG)); + } + } + return sControllers; + } + + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List getXmlResourcesToIndex(Context context, + boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.accessibility_control_timeout_settings; + return Arrays.asList(sir); + } + + @Override + public List getNonIndexableKeys(Context context) { + final List keys = super.getNonIndexableKeys(context); + return keys; + } + + @Override + public List createPreferenceControllers( + Context context) { + return buildPreferenceControllers(context, null); + } + }; +} \ No newline at end of file diff --git a/src/com/android/settings/accessibility/AccessibilityTimeoutController.java b/src/com/android/settings/accessibility/AccessibilityTimeoutController.java new file mode 100644 index 00000000000..2995df542ee --- /dev/null +++ b/src/com/android/settings/accessibility/AccessibilityTimeoutController.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2018 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.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.provider.Settings; + +import androidx.lifecycle.LifecycleObserver; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.widget.RadioButtonPreference; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import java.lang.Integer; + +import java.util.HashMap; +import java.util.Map; + +public class AccessibilityTimeoutController extends AbstractPreferenceController implements + LifecycleObserver, RadioButtonPreference.OnClickListener, PreferenceControllerMixin { + + // pair the preference key and timeout value + private final Map mAccessibilityTimeoutKeyToValueMap = new HashMap<>(); + + private final String mPreferenceKey; + private final String mfragmentTag; + private final ContentResolver mContentResolver; + private final Resources mResources; + private OnChangeListener mOnChangeListener; + private RadioButtonPreference mPreference; + private int mAccessibilityUiTimeoutValue; + + public AccessibilityTimeoutController(Context context, Lifecycle lifecycle, + String preferenceKey, String fragmentTag) { + super(context); + + mContentResolver = context.getContentResolver(); + mResources = context.getResources(); + + if (lifecycle != null) { + lifecycle.addObserver(this); + } + mPreferenceKey = preferenceKey; + mfragmentTag = fragmentTag; + } + + public void setOnChangeListener(OnChangeListener listener) { + mOnChangeListener = listener; + } + + private Map getTimeoutValueToKeyMap() { + if (mAccessibilityTimeoutKeyToValueMap.size() == 0) { + + String[] timeoutKeys = null; + if (mfragmentTag.equals(AccessibilityContentTimeoutPreferenceFragment.TAG)) { + timeoutKeys = mResources.getStringArray( + R.array.accessibility_timeout_content_selector_keys); + } else if (mfragmentTag.equals(AccessibilityControlTimeoutPreferenceFragment.TAG)) { + timeoutKeys = mResources.getStringArray( + R.array.accessibility_timeout_control_selector_keys); + } + + int[] timeoutValues = mResources.getIntArray( + R.array.accessibility_timeout_selector_values); + + final int timeoutValueCount = timeoutValues.length; + for (int i = 0; i < timeoutValueCount; i++) { + mAccessibilityTimeoutKeyToValueMap.put(timeoutKeys[i], timeoutValues[i]); + } + } + return mAccessibilityTimeoutKeyToValueMap; + } + + private void putSecureString(String name, String value) { + Settings.Secure.putString(mContentResolver, name, value); + } + + private void handlePreferenceChange(String value) { + if (mfragmentTag.equals(AccessibilityContentTimeoutPreferenceFragment.TAG)) { + putSecureString(Settings.Secure.ACCESSIBILITY_NON_INTERACTIVE_UI_TIMEOUT_MS, value); + } else if (mfragmentTag.equals(AccessibilityControlTimeoutPreferenceFragment.TAG)) { + putSecureString(Settings.Secure.ACCESSIBILITY_INTERACTIVE_UI_TIMEOUT_MS, value); + } + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public String getPreferenceKey() { + return mPreferenceKey; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + + mPreference = (RadioButtonPreference) + screen.findPreference(getPreferenceKey()); + mPreference.setOnClickListener(this); + } + + @Override + public void onRadioButtonClicked(RadioButtonPreference preference) { + int value = getTimeoutValueToKeyMap().get(mPreferenceKey); + handlePreferenceChange(String.valueOf(value)); + if (mOnChangeListener != null) { + mOnChangeListener.onCheckedChanged(mPreference); + } + } + + protected void getAccessibilityUiValue() { + String timeoutValue = null; + if (mfragmentTag.equals(AccessibilityContentTimeoutPreferenceFragment.TAG)) { + timeoutValue = Settings.Secure.getString(mContentResolver, + Settings.Secure.ACCESSIBILITY_NON_INTERACTIVE_UI_TIMEOUT_MS); + } else if (mfragmentTag.equals(AccessibilityControlTimeoutPreferenceFragment.TAG)) { + timeoutValue = Settings.Secure.getString(mContentResolver, + Settings.Secure.ACCESSIBILITY_INTERACTIVE_UI_TIMEOUT_MS); + } + mAccessibilityUiTimeoutValue = timeoutValue == null? 0: Integer.parseInt(timeoutValue); + } + + protected void updatePreferenceCheckedState(int value) { + if (mAccessibilityUiTimeoutValue == value) { + mPreference.setChecked(true); + } + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + getAccessibilityUiValue(); + + // reset RadioButton + mPreference.setChecked(false); + int preferenceValue = getTimeoutValueToKeyMap().get(mPreference.getKey()); + updatePreferenceCheckedState(preferenceValue); + } + + public static interface OnChangeListener { + void onCheckedChanged(Preference preference); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilityTimeoutControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilityTimeoutControllerTest.java new file mode 100644 index 00000000000..632eea77aa7 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilityTimeoutControllerTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2018 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.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ContentResolver; +import android.content.Context; +import android.provider.Settings; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.widget.RadioButtonPreference; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; + +@RunWith(SettingsRobolectricTestRunner.class) +public class AccessibilityTimeoutControllerTest + implements AccessibilityTimeoutController.OnChangeListener { + private static final String PREF_KEY = "accessibility_content_timeout_30secs"; + private static String PREF_TITLE; + + private AccessibilityTimeoutController mController; + + @Mock + private RadioButtonPreference mMockPref; + private Context mContext; + private ContentResolver mContentResolver; + + @Mock + private PreferenceScreen mScreen; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mController = new AccessibilityTimeoutController(mContext, mock(Lifecycle.class), + PREF_KEY, AccessibilityContentTimeoutPreferenceFragment.TAG); + mController.setOnChangeListener(this); + mContentResolver = mContext.getContentResolver(); + PREF_TITLE = mContext.getResources().getString(R.string.accessibility_timeout_30secs); + + when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mMockPref); + when(mMockPref.getKey()).thenReturn(PREF_KEY); + when(mMockPref.getTitle()).thenReturn(PREF_TITLE); + mController.displayPreference(mScreen); + } + + @Override + public void onCheckedChanged(Preference preference) { + mController.updateState(preference); + } + + @Test + public void isAvailable() { + assertTrue(mController.isAvailable()); + } + + @Test + public void updateState_notChecked() { + Settings.Secure.putString(mContentResolver, + Settings.Secure.ACCESSIBILITY_NON_INTERACTIVE_UI_TIMEOUT_MS, "0"); + + mController.updateState(mMockPref); + + // the first checked state is seted to false by control + verify(mMockPref).setChecked(false); + verify(mMockPref).setChecked(false); + } + + @Test + public void updateState_checked() { + Settings.Secure.putString(mContentResolver, + Settings.Secure.ACCESSIBILITY_NON_INTERACTIVE_UI_TIMEOUT_MS, "30000"); + + mController.updateState(mMockPref); + + // the first checked state is seted to false by control + verify(mMockPref).setChecked(false); + verify(mMockPref).setChecked(true); + } + + @Test + public void onRadioButtonClick() { + mController.onRadioButtonClicked(mMockPref); + + String accessibilityUiTimeoutValue = Settings.Secure.getString(mContentResolver, + Settings.Secure.ACCESSIBILITY_NON_INTERACTIVE_UI_TIMEOUT_MS); + + assertThat(accessibilityUiTimeoutValue).isEqualTo("30000"); + } +} \ No newline at end of file