diff --git a/res/values/config.xml b/res/values/config.xml index 145c6c53c9a..4eba076a08c 100755 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -226,6 +226,9 @@ Can be overridden for specific product builds if the target device does not support it --> true + + false + Media vibration + + Keyboard vibration Ring vibration @@ -7339,6 +7341,9 @@ nfc, tag, reader + + keyboard, haptics, vibrate, + Volume, vibration, Do Not Disturb diff --git a/res/xml/accessibility_vibration_intensity_settings.xml b/res/xml/accessibility_vibration_intensity_settings.xml index d9505b5d0a0..13dc03b0068 100644 --- a/res/xml/accessibility_vibration_intensity_settings.xml +++ b/res/xml/accessibility_vibration_intensity_settings.xml @@ -77,6 +77,12 @@ app:keywords="@string/keywords_media_vibration" app:controller="com.android.settings.accessibility.MediaVibrationIntensityPreferenceController" /> + + diff --git a/res/xml/accessibility_vibration_settings.xml b/res/xml/accessibility_vibration_settings.xml index 5e2f923ad4c..0435cb49c6d 100644 --- a/res/xml/accessibility_vibration_settings.xml +++ b/res/xml/accessibility_vibration_settings.xml @@ -77,6 +77,12 @@ app:keywords="@string/keywords_media_vibration" app:controller="com.android.settings.accessibility.MediaVibrationTogglePreferenceController" /> + + diff --git a/src/com/android/settings/accessibility/KeyboardVibrationTogglePreferenceController.java b/src/com/android/settings/accessibility/KeyboardVibrationTogglePreferenceController.java new file mode 100644 index 00000000000..29d5928e1cb --- /dev/null +++ b/src/com/android/settings/accessibility/KeyboardVibrationTogglePreferenceController.java @@ -0,0 +1,159 @@ +/* + * 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; + +import static android.provider.Settings.System.KEYBOARD_VIBRATION_ENABLED; + +import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; +import static com.android.settings.accessibility.AccessibilityUtil.State.ON; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.VibrationAttributes; +import android.os.Vibrator; +import android.os.vibrator.Flags; +import android.provider.Settings; +import android.util.Log; + +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.settings.R; +import com.android.settings.core.TogglePreferenceController; + + +/** + * A preference controller to turn on/off keyboard vibration state with a single toggle. + */ +public class KeyboardVibrationTogglePreferenceController extends TogglePreferenceController + implements DefaultLifecycleObserver { + + private static final String TAG = "KeyboardVibrateControl"; + + private static final Uri MAIN_VIBRATION_SWITCH_URI = + Settings.System.getUriFor(VibrationPreferenceConfig.MAIN_SWITCH_SETTING_KEY); + + private final ContentObserver mContentObserver; + + private final Vibrator mVibrator; + + @Nullable + private SwitchPreference mPreference; + + public KeyboardVibrationTogglePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mVibrator = context.getSystemService(Vibrator.class); + mContentObserver = new ContentObserver(new Handler(/* async= */ true)) { + @Override + public void onChange(boolean selfChange, Uri uri) { + if (uri.equals(MAIN_VIBRATION_SWITCH_URI)) { + updateState(mPreference); + } else { + Log.w(TAG, "Unexpected uri change:" + uri); + } + } + }; + } + + @Override + public void onStart(@NonNull LifecycleOwner owner) { + mContext.getContentResolver().registerContentObserver(MAIN_VIBRATION_SWITCH_URI, + /* notifyForDescendants= */ false, mContentObserver); + } + + @Override + public void onStop(@NonNull LifecycleOwner owner) { + mContext.getContentResolver().unregisterContentObserver(mContentObserver); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void updateState(@Nullable Preference preference) { + if (preference != null) { + super.updateState(preference); + preference.setEnabled(isPreferenceEnabled()); + } + } + + @Override + public int getAvailabilityStatus() { + if (Flags.keyboardCategoryEnabled() + && mContext.getResources().getBoolean(R.bool.config_keyboard_vibration_supported)) { + return AVAILABLE; + } + return UNSUPPORTED_ON_DEVICE; + } + + @Override + public boolean isChecked() { + // Always unchecked if the preference disabled + return isPreferenceEnabled() && isKeyboardVibrationSwitchEnabled(); + } + + @Override + public boolean setChecked(boolean isChecked) { + final boolean success = updateKeyboardVibrationSetting(isChecked); + if (success && isChecked) { + // Play the preview vibration effect when the toggle is on. + final VibrationAttributes touchAttrs = + VibrationPreferenceConfig.createPreviewVibrationAttributes( + VibrationAttributes.USAGE_TOUCH); + final VibrationAttributes keyboardAttrs = + new VibrationAttributes.Builder(touchAttrs) + .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) + .build(); + VibrationPreferenceConfig.playVibrationPreview(mVibrator, keyboardAttrs); + } + return true; + } + + @Override + public int getSliceHighlightMenuRes() { + return R.string.menu_key_accessibility; + } + + private boolean isPreferenceEnabled() { + return VibrationPreferenceConfig.isMainVibrationSwitchEnabled( + mContext.getContentResolver()); + } + + private boolean isKeyboardVibrationSwitchEnabled() { + return Settings.System.getInt(mContext.getContentResolver(), KEYBOARD_VIBRATION_ENABLED, + mVibrator.isDefaultKeyboardVibrationEnabled() ? ON : OFF) == ON; + } + + private boolean updateKeyboardVibrationSetting(boolean enable) { + final boolean success = Settings.System.putInt(mContext.getContentResolver(), + KEYBOARD_VIBRATION_ENABLED, enable ? ON : OFF); + if (!success) { + Log.w(TAG, "Update settings database error!"); + } + return success; + } +} diff --git a/src/com/android/settings/accessibility/VibrationPreferenceConfig.java b/src/com/android/settings/accessibility/VibrationPreferenceConfig.java index b4be5281a0c..c25c38e5a6d 100644 --- a/src/com/android/settings/accessibility/VibrationPreferenceConfig.java +++ b/src/com/android/settings/accessibility/VibrationPreferenceConfig.java @@ -68,8 +68,19 @@ public abstract class VibrationPreferenceConfig { /** Play a vibration effect with intensity just selected by the user. */ public static void playVibrationPreview(Vibrator vibrator, @VibrationAttributes.Usage int vibrationUsage) { - vibrator.vibrate(PREVIEW_VIBRATION_EFFECT, - createPreviewVibrationAttributes(vibrationUsage)); + playVibrationPreview(vibrator, createPreviewVibrationAttributes(vibrationUsage)); + } + + /** + * Play a vibration effect with intensity just selected by the user. + * + * @param vibrator The {@link Vibrator} used to play the vibration. + * @param vibrationAttributes The {@link VibrationAttributes} to indicate the + * vibration information. + */ + public static void playVibrationPreview(Vibrator vibrator, + VibrationAttributes vibrationAttributes) { + vibrator.vibrate(PREVIEW_VIBRATION_EFFECT, vibrationAttributes); } public VibrationPreferenceConfig(Context context, String settingKey, @@ -135,7 +146,7 @@ public abstract class VibrationPreferenceConfig { return mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_SILENT; } - private static VibrationAttributes createPreviewVibrationAttributes( + static VibrationAttributes createPreviewVibrationAttributes( @VibrationAttributes.Usage int vibrationUsage) { return new VibrationAttributes.Builder() .setUsage(vibrationUsage) diff --git a/tests/robotests/src/com/android/settings/accessibility/KeyboardVibrationTogglePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/KeyboardVibrationTogglePreferenceControllerTest.java new file mode 100644 index 00000000000..6bf83c6126a --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/KeyboardVibrationTogglePreferenceControllerTest.java @@ -0,0 +1,172 @@ +/* + * 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; + +import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; +import static com.android.settings.accessibility.AccessibilityUtil.State.ON; +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; + +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 android.content.res.Resources; +import android.os.vibrator.Flags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.Settings; + +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link KeyboardVibrationTogglePreferenceController}. */ +@RunWith(RobolectricTestRunner.class) +public class KeyboardVibrationTogglePreferenceControllerTest { + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Mock + private PreferenceScreen mPreferenceScreen; + + private Context mContext; + private Resources mResources; + private KeyboardVibrationTogglePreferenceController mController; + + private SwitchPreference mPreference; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(ApplicationProvider.getApplicationContext()); + mResources = spy(mContext.getResources()); + when(mContext.getResources()).thenReturn(mResources); + mController = new KeyboardVibrationTogglePreferenceController(mContext, "preferenceKey"); + mPreference = new SwitchPreference(mContext); + when(mPreferenceScreen.findPreference( + mController.getPreferenceKey())).thenReturn(mPreference); + mController.displayPreference(mPreferenceScreen); + } + + @Test + public void getAvailabilityStatus_featureSupported_available() { + mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED); + when(mResources.getBoolean(R.bool.config_keyboard_vibration_supported)).thenReturn(true); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailabilityStatus_featureNotSupported_unavailable() { + mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED); + when(mResources.getBoolean(R.bool.config_keyboard_vibration_supported)).thenReturn(false); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + public void getAvailabilityStatus_keyboardCategoryDisabled_unavailable() { + mSetFlagsRule.disableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED); + when(mResources.getBoolean(R.bool.config_keyboard_vibration_supported)).thenReturn(true); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + public void updateState_mainVibrateDisabled_shouldReturnFalseForCheckedAndEnabled() { + updateSystemSetting(VibrationPreferenceConfig.MAIN_SWITCH_SETTING_KEY, OFF); + + mController.updateState(mPreference); + + assertThat(mPreference.isEnabled()).isFalse(); + assertThat(mPreference.isChecked()).isFalse(); + } + + @Test + public void updateState_mainVibrateEnabled_shouldReturnTrueForEnabled() { + updateSystemSetting(VibrationPreferenceConfig.MAIN_SWITCH_SETTING_KEY, ON); + + mController.updateState(mPreference); + + assertThat(mPreference.isEnabled()).isTrue(); + } + + @Test + public void isChecked_keyboardVibrateEnabled_shouldReturnTrue() { + updateSystemSetting(VibrationPreferenceConfig.MAIN_SWITCH_SETTING_KEY, ON); + updateSystemSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, ON); + + mController.updateState(mPreference); + + assertThat(mPreference.isChecked()).isTrue(); + } + + @Test + public void isChecked_keyboardVibrateDisabled_shouldReturnFalse() { + updateSystemSetting(VibrationPreferenceConfig.MAIN_SWITCH_SETTING_KEY, ON); + updateSystemSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, OFF); + + mController.updateState(mPreference); + + assertThat(mPreference.isChecked()).isFalse(); + } + + @Test + public void setChecked_checked_updateSettings() throws Settings.SettingNotFoundException { + // set an off state initially + updateSystemSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, OFF); + + assertThat(readSystemSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED)).isEqualTo(OFF); + + mController.setChecked(true); + + assertThat(readSystemSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED)).isEqualTo(ON); + } + + @Test + public void setChecked_unchecked_updateSettings() throws Settings.SettingNotFoundException { + // set an on state initially + updateSystemSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, ON); + + assertThat(readSystemSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED)).isEqualTo(ON); + + mController.setChecked(false); + + assertThat(readSystemSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED)).isEqualTo(OFF); + } + + private void updateSystemSetting(String key, int value) { + Settings.System.putInt(mContext.getContentResolver(), key, value); + } + + private int readSystemSetting(String key) throws Settings.SettingNotFoundException { + return Settings.System.getInt(mContext.getContentResolver(), key); + } +}