From 3384913c65d696fa4045e0df9f01df3e2f56f861 Mon Sep 17 00:00:00 2001 From: Edgar Wang Date: Fri, 17 Jan 2025 07:52:07 +0000 Subject: [PATCH 01/23] Update background color of Settings search Bug: 371849249 Test: visual Flag: EXEMPT resource update Change-Id: I6070273fa564c6b41d2b850c005967e455f155e9 --- res/drawable/search_bar_selected_background.xml | 2 +- res/values-night/colors.xml | 1 - res/values/colors.xml | 3 --- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/res/drawable/search_bar_selected_background.xml b/res/drawable/search_bar_selected_background.xml index b98ed9f7502..a1299c5969b 100644 --- a/res/drawable/search_bar_selected_background.xml +++ b/res/drawable/search_bar_selected_background.xml @@ -16,5 +16,5 @@ - + diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml index 3892138bc25..a05751046e6 100644 --- a/res/values-night/colors.xml +++ b/res/values-night/colors.xml @@ -19,7 +19,6 @@ #783BE5 #3F5FBD @*android:color/material_grey_900 - @androidprv:color/materialColorSurfaceBright #5F6368 diff --git a/res/values/colors.xml b/res/values/colors.xml index 6e3f96fc32c..4bec2284995 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -93,9 +93,6 @@ @*android:color/accent_device_default_light - - @androidprv:color/materialColorSurfaceBright - #42a5f5 #4182ef From d0abb3f7350777b191c7e308dbc91ef372d9d110 Mon Sep 17 00:00:00 2001 From: chelseahao Date: Tue, 7 Jan 2025 17:57:18 +0800 Subject: [PATCH 02/23] Show "Stream paused" when hysteresis mode in UMO. Test: atest Bug: 308368124 Flag: com.android.settingslib.flags.audio_sharing_hysteresis_mode_fix Change-Id: I72897d9ed00ca0ed584513381d20f417db8e522c --- res/values/strings.xml | 2 +- .../audiostreams/AudioStreamMediaService.java | 57 ++++++++++++++++- .../AudioStreamMediaServiceTest.java | 61 +++++++++++++++++++ 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 9930c3685c0..4e5ac916340 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -13981,7 +13981,7 @@ Listening now - Paused by host + Stream paused Stop listening diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java index 50231665865..031f29fcaa6 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java @@ -16,6 +16,9 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING; + import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -23,6 +26,7 @@ import android.app.Service; import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothVolumeControl; import android.content.Intent; @@ -64,7 +68,8 @@ public class AudioStreamMediaService extends Service { static final String DEVICES = "audio_stream_media_service_devices"; private static final String TAG = "AudioStreamMediaService"; private static final int NOTIFICATION_ID = 1; - private static final int BROADCAST_CONTENT_TEXT = R.string.audio_streams_listening_now; + private static final int BROADCAST_LISTENING_NOW_TEXT = R.string.audio_streams_listening_now; + private static final int BROADCAST_STREAM_PAUSED_TEXT = R.string.audio_streams_present_now; @VisibleForTesting static final String LEAVE_BROADCAST_ACTION = "leave_broadcast_action"; private static final String LEAVE_BROADCAST_TEXT = "Leave Broadcast"; private static final String CHANNEL_ID = "bluetooth_notification_channel"; @@ -94,11 +99,22 @@ public class AudioStreamMediaService extends Service { LEAVE_BROADCAST_ACTION, LEAVE_BROADCAST_TEXT, com.android.settings.R.drawable.ic_clear); + private final PlaybackState.Builder mPlayStateHysteresisBuilder = + new PlaybackState.Builder() + .setState( + PlaybackState.STATE_STOPPED, + STATIC_PLAYBACK_POSITION, + ZERO_PLAYBACK_SPEED) + .addCustomAction( + LEAVE_BROADCAST_ACTION, + LEAVE_BROADCAST_TEXT, + com.android.settings.R.drawable.ic_clear); private final MetricsFeatureProvider mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); private final AtomicBoolean mIsMuted = new AtomicBoolean(false); + private final AtomicBoolean mIsHysteresis = new AtomicBoolean(false); // Set 25 as default as the volume range from `VolumeControlProfile` is from 0 to 255. // If the initial volume from `onDeviceVolumeChanged` is larger than zero (not muted), we will // override this value. Otherwise, we raise the volume to 25 when the play button is clicked. @@ -255,6 +271,9 @@ public class AudioStreamMediaService extends Service { } private PlaybackState getPlaybackState() { + if (mIsHysteresis.get()) { + return mPlayStateHysteresisBuilder.build(); + } return mIsMuted.get() ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build(); } @@ -283,7 +302,9 @@ public class AudioStreamMediaService extends Service { new Notification.Builder(this, CHANNEL_ID) .setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing) .setStyle(mediaStyle) - .setContentText(getString(BROADCAST_CONTENT_TEXT)) + .setContentText(getString( + mIsHysteresis.get() ? BROADCAST_STREAM_PAUSED_TEXT : + BROADCAST_LISTENING_NOW_TEXT)) .setSilent(true); return notificationBuilder.build(); } @@ -307,6 +328,38 @@ public class AudioStreamMediaService extends Service { handleRemoveSource(); } + @Override + public void onReceiveStateChanged( + BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) { + super.onReceiveStateChanged(sink, sourceId, state); + if (!mHysteresisModeFixAvailable || mDevices == null || !mDevices.contains(sink)) { + return; + } + var sourceState = LocalBluetoothLeBroadcastAssistant.getLocalSourceState(state); + boolean streaming = sourceState == STREAMING; + boolean paused = sourceState == PAUSED; + // Exit early if the state is neither streaming nor paused + if (!streaming && !paused) { + return; + } + // Atomically update mIsHysteresis if its current value is not the current paused state + if (mIsHysteresis.compareAndSet(!paused, paused)) { + synchronized (mLocalSessionLock) { + if (mLocalSession == null) { + return; + } + mLocalSession.setPlaybackState(getPlaybackState()); + if (mNotificationManager != null) { + mNotificationManager.notify( + NOTIFICATION_ID, + buildNotification(mLocalSession.getSessionToken()) + ); + } + Log.d(TAG, "updating hysteresis mode to : " + paused); + } + } + } + private void handleRemoveSource() { if (mAudioStreamsHelper != null && !mAudioStreamsHelper.getConnectedBroadcastIdAndState( diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java index bfb474b711f..a0e971b1d45 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java @@ -40,6 +40,7 @@ import android.app.NotificationManager; import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothStatusCodes; import android.content.Context; @@ -86,6 +87,7 @@ import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers; import java.util.ArrayList; +import java.util.List; import java.util.Set; @RunWith(RobolectricTestRunner.class) @@ -99,11 +101,13 @@ import java.util.Set; public class AudioStreamMediaServiceTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private static final String DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"; private static final String CHANNEL_ID = "bluetooth_notification_channel"; private static final String DEVICE_NAME = "name"; @Mock private Resources mResources; @Mock private LocalBluetoothManager mLocalBtManager; @Mock private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; + @Mock private BluetoothLeBroadcastReceiveState mBroadcastReceiveState; @Mock private AudioStreamsHelper mAudioStreamsHelper; @Mock private NotificationManager mNotificationManager; @Mock private MediaSessionManager mMediaSessionManager; @@ -304,6 +308,63 @@ public class AudioStreamMediaServiceTest { verify(mAudioStreamMediaService).stopSelf(); } + @Test + public void assistantCallback_onReceiveStateChanged_connected_doNothing() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + + mAudioStreamMediaService.onCreate(); + mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0); + + assertThat(mAudioStreamMediaService.mBroadcastAssistantCallback).isNotNull(); + List bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState); + when(mDevice.getAddress()).thenReturn(DEVICE_ADDRESS); + when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mDevice); + + mAudioStreamMediaService.mBroadcastAssistantCallback.onReceiveStateChanged( + mDevice, /* sourceId= */ 0, /* state= */ mBroadcastReceiveState); + + verify(mNotificationManager, never()).notify(anyInt(), any()); + } + + @Test + public void assistantCallback_onReceiveStateChanged_hysteresis_updateNotification() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + + mAudioStreamMediaService.onCreate(); + mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0); + + assertThat(mAudioStreamMediaService.mBroadcastAssistantCallback).isNotNull(); + when(mBroadcastReceiveState.getBisSyncState()).thenReturn(new ArrayList<>()); + when(mDevice.getAddress()).thenReturn(DEVICE_ADDRESS); + when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mDevice); + + mAudioStreamMediaService.mBroadcastAssistantCallback.onReceiveStateChanged( + mDevice, /* sourceId= */ 0, /* state= */ mBroadcastReceiveState); + + verify(mNotificationManager).notify(anyInt(), any()); + } + + @Test + public void assistantCallback_onReceiveStateChanged_hysteresis_flagOff_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + + mAudioStreamMediaService.onCreate(); + mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0); + + assertThat(mAudioStreamMediaService.mBroadcastAssistantCallback).isNotNull(); + mAudioStreamMediaService.mBroadcastAssistantCallback.onReceiveStateChanged( + mDevice, /* sourceId= */ 0, /* state= */ mBroadcastReceiveState); + + verify(mBroadcastReceiveState, never()).getBisSyncState(); + verify(mBroadcastReceiveState, never()).getSourceDevice(); + verify(mNotificationManager, never()).notify(anyInt(), any()); + } + @Test public void bluetoothCallback_onBluetoothOff_stopSelf() { mAudioStreamMediaService.onCreate(); From 3aee441d2c55cdc29cc39bc209bd1ea4a021234b Mon Sep 17 00:00:00 2001 From: Owner Cleanup Bot Date: Sun, 26 Jan 2025 08:24:40 -0800 Subject: [PATCH 03/23] [owners] Remove aroederer@google.com from src/com/android/settings/notification/OWNERS This suggested change is automatically generated based on group memberships and affiliations. If this change is unnecessary or in error, vote the lowest CR value (i.e. reject the CL) and the bot will abandon it. Vote the highest CR to approve this change. You may also abandon this change. See the owner's recent activity for context: https://android-review.googlesource.com/q/aroederer@google.com To report an issue, file a bug in the Infra>Codereview component. Change-Id: Ifa12fa8e045776b6351530a5f1341311046e9a1c --- src/com/android/settings/notification/OWNERS | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/com/android/settings/notification/OWNERS b/src/com/android/settings/notification/OWNERS index 29484c69ff4..424e6cca1c0 100644 --- a/src/com/android/settings/notification/OWNERS +++ b/src/com/android/settings/notification/OWNERS @@ -1,7 +1,6 @@ # Default reviewers for this and subdirectories. -aroederer@google.com beverlyt@google.com dsandler@android.com juliacr@google.com matiashe@google.com -yurilin@google.com \ No newline at end of file +yurilin@google.com From 484c83f3d9bc38d2d71991d068974adc89d9e8bd Mon Sep 17 00:00:00 2001 From: Yvonne Jiang Date: Thu, 30 Jan 2025 16:37:38 -0800 Subject: [PATCH 04/23] Register the category key mapping for the SupervisionDashboardFragment. Bug: 383404606 Change-Id: I9acf51e5a4d0ee2e33e1aaf2f7ca895854723d59 Test: manual Flag: android.app.supervision.flags.enable_supervision_settings_screen --- .../android/settings/dashboard/DashboardFragmentRegistry.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/com/android/settings/dashboard/DashboardFragmentRegistry.java b/src/com/android/settings/dashboard/DashboardFragmentRegistry.java index 849a80b3592..4afe987dece 100644 --- a/src/com/android/settings/dashboard/DashboardFragmentRegistry.java +++ b/src/com/android/settings/dashboard/DashboardFragmentRegistry.java @@ -47,6 +47,7 @@ import com.android.settings.safetycenter.MoreSecurityPrivacyFragment; import com.android.settings.security.LockscreenDashboardFragment; import com.android.settings.security.SecurityAdvancedSettings; import com.android.settings.security.SecuritySettings; +import com.android.settings.supervision.SupervisionDashboardFragment; import com.android.settings.system.SystemDashboardFragment; import com.android.settingslib.drawer.CategoryKey; @@ -132,6 +133,8 @@ public class DashboardFragmentRegistry { CategoryKey.CATEGORY_SPECIAL_APP_ACCESS); PARENT_TO_CATEGORY_KEY_MAP.put(MoreSecurityPrivacyFragment.class.getName(), CategoryKey.CATEGORY_MORE_SECURITY_PRIVACY_SETTINGS); + PARENT_TO_CATEGORY_KEY_MAP.put(SupervisionDashboardFragment.class.getName(), + CategoryKey.CATEGORY_SUPERVISION); CATEGORY_KEY_TO_PARENT_MAP = new ArrayMap<>(PARENT_TO_CATEGORY_KEY_MAP.size()); From 120b2d46f71476119c33f5ed4855d991700afe6e Mon Sep 17 00:00:00 2001 From: shaoweishen Date: Mon, 20 Jan 2025 08:27:54 +0000 Subject: [PATCH 05/23] [Physical Keyboard] Move Dialog to DialogFragment Move Bounce key and Slow key Dialog to DialogFragment, dialog will not be dismissed when screen rotate. Bug: 390243451 Bug: 374037603 Flag: com.android.settings.keyboard.keyboard_and_touchpad_a11y_new_page_enabled Test: atest packages/apps/Settings/tests/robotests/src/com/android/settings/inputmethod/ Change-Id: Ide87dbf8214f411941114281e7a5e8c81f75bdd4 --- .../InputSettingPreferenceController.java | 135 +------------ ...oardAccessibilityBounceKeysController.java | 23 +-- ...AccessibilityBounceKeysDialogFragment.java | 54 +++++ ...yboardAccessibilityKeysDialogFragment.java | 187 ++++++++++++++++++ ...yboardAccessibilitySlowKeysController.java | 22 +-- ...rdAccessibilitySlowKeysDialogFragment.java | 54 +++++ .../PhysicalKeyboardA11yFragment.java | 2 + ...AccessibilityBounceKeysControllerTest.java | 58 ++++-- ...ssibilityBounceKeysDialogFragmentTest.java | 81 ++++++++ ...rdAccessibilitySlowKeysControllerTest.java | 58 ++++-- ...cessibilitySlowKeysDialogFragmentTest.java | 81 ++++++++ 11 files changed, 555 insertions(+), 200 deletions(-) create mode 100644 src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysDialogFragment.java create mode 100644 src/com/android/settings/inputmethod/KeyboardAccessibilityKeysDialogFragment.java create mode 100644 src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysDialogFragment.java create mode 100644 tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysDialogFragmentTest.java create mode 100644 tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysDialogFragmentTest.java diff --git a/src/com/android/settings/inputmethod/InputSettingPreferenceController.java b/src/com/android/settings/inputmethod/InputSettingPreferenceController.java index f18c9f4ed49..17b70b3da3a 100644 --- a/src/com/android/settings/inputmethod/InputSettingPreferenceController.java +++ b/src/com/android/settings/inputmethod/InputSettingPreferenceController.java @@ -25,36 +25,29 @@ import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; import android.os.UserHandle; -import android.view.View; -import android.widget.RadioButton; -import android.widget.RadioGroup; -import android.widget.SeekBar; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.OnLifecycleEvent; import androidx.preference.Preference; -import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; import com.android.settings.keyboard.Flags; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import java.text.NumberFormat; -import java.util.concurrent.TimeUnit; - /** * Abstract class for toggle controllers of Keyboard input setting related function. */ public abstract class InputSettingPreferenceController extends TogglePreferenceController implements LifecycleObserver { - private static final int CUSTOM_PROGRESS_INTERVAL = 100; - private static final long MILLISECOND_IN_SECONDS = TimeUnit.SECONDS.toMillis(1); private final ContentResolver mContentResolver; protected final MetricsFeatureProvider mMetricsFeatureProvider; + protected FragmentManager mFragmentManager; + private final ContentObserver mContentObserver = new ContentObserver(new Handler(true)) { @Override public void onChange(boolean selfChange, Uri uri) { @@ -72,13 +65,6 @@ public abstract class InputSettingPreferenceController extends TogglePreferenceC protected void updateInputSettingKeysValue(int thresholdTimeMillis) { } - protected int getInputSettingKeysValue() { - return 0; - } - - protected void onCustomValueUpdated(int thresholdTimeMillis) { - } - public InputSettingPreferenceController(@NonNull Context context, @NonNull String preferenceKey) { super(context, preferenceKey); @@ -86,6 +72,10 @@ public abstract class InputSettingPreferenceController extends TogglePreferenceC mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); } + public void setFragment(Fragment fragment) { + mFragmentManager = fragment.getParentFragmentManager(); + } + @Override public void updateState(@NonNull Preference preference) { super.updateState(preference); @@ -127,113 +117,4 @@ public abstract class InputSettingPreferenceController extends TogglePreferenceC private void unregisterSettingsObserver() { mContentResolver.unregisterContentObserver(mContentObserver); } - - protected void constructDialog(Context context, int titleRes, int subtitleRes) { - mAlertDialog = new AlertDialog.Builder(context) - .setView(R.layout.dialog_keyboard_a11y_input_setting_keys) - .setPositiveButton(android.R.string.ok, - (dialog, which) -> { - RadioGroup radioGroup = - mAlertDialog.findViewById( - R.id.input_setting_keys_value_group); - SeekBar seekbar = mAlertDialog.findViewById( - R.id.input_setting_keys_value_custom_slider); - RadioButton customRadioButton = mAlertDialog.findViewById( - R.id.input_setting_keys_value_custom); - int threshold; - if (customRadioButton.isChecked()) { - threshold = seekbar.getProgress() * CUSTOM_PROGRESS_INTERVAL; - } else { - int checkedRadioButtonId = radioGroup.getCheckedRadioButtonId(); - if (checkedRadioButtonId == R.id.input_setting_keys_value_600) { - threshold = 600; - } else if (checkedRadioButtonId - == R.id.input_setting_keys_value_400) { - threshold = 400; - } else if (checkedRadioButtonId - == R.id.input_setting_keys_value_200) { - threshold = 200; - } else { - threshold = 0; - } - } - updateInputSettingKeysValue(threshold); - onCustomValueUpdated(threshold); - }) - .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss()) - .create(); - mAlertDialog.setOnShowListener(dialog -> { - RadioGroup cannedValueRadioGroup = mAlertDialog.findViewById( - R.id.input_setting_keys_value_group); - RadioButton customRadioButton = mAlertDialog.findViewById( - R.id.input_setting_keys_value_custom); - TextView customValueTextView = mAlertDialog.findViewById( - R.id.input_setting_keys_value_custom_value); - SeekBar customProgressBar = mAlertDialog.findViewById( - R.id.input_setting_keys_value_custom_slider); - TextView titleTextView = mAlertDialog.findViewById( - R.id.input_setting_keys_dialog_title); - TextView subTitleTextView = mAlertDialog.findViewById( - R.id.input_setting_keys_dialog_subtitle); - titleTextView.setText(titleRes); - subTitleTextView.setText(subtitleRes); - - customProgressBar.incrementProgressBy(CUSTOM_PROGRESS_INTERVAL); - customProgressBar.setProgress(1); - View customValueView = mAlertDialog.findViewById( - R.id.input_setting_keys_custom_value_option); - customValueView.setOnClickListener(l -> customRadioButton.performClick()); - customRadioButton.setOnCheckedChangeListener((buttonView, isChecked) -> { - if (isChecked) { - cannedValueRadioGroup.clearCheck(); - } - customValueTextView.setVisibility(isChecked ? View.VISIBLE : View.GONE); - customValueTextView.setText( - progressToThresholdInSecond(customProgressBar.getProgress())); - customProgressBar.setVisibility(isChecked ? View.VISIBLE : View.GONE); - buttonView.setChecked(isChecked); - }); - cannedValueRadioGroup.setOnCheckedChangeListener( - (group, checkedId) -> customRadioButton.setChecked(false)); - customProgressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - customValueTextView.setText(progressToThresholdInSecond(progress)); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - } - }); - initStateBasedOnThreshold(cannedValueRadioGroup, customRadioButton, customValueTextView, - customProgressBar); - }); - } - - private static String progressToThresholdInSecond(int progress) { - return NumberFormat.getInstance().format((float) progress * CUSTOM_PROGRESS_INTERVAL - / MILLISECOND_IN_SECONDS); - } - - private void initStateBasedOnThreshold(RadioGroup cannedValueRadioGroup, - RadioButton customRadioButton, TextView customValueTextView, - SeekBar customProgressBar) { - int inputSettingKeysThreshold = getInputSettingKeysValue(); - switch (inputSettingKeysThreshold) { - case 600 -> cannedValueRadioGroup.check(R.id.input_setting_keys_value_600); - case 400 -> cannedValueRadioGroup.check(R.id.input_setting_keys_value_400); - case 0, 200 -> cannedValueRadioGroup.check(R.id.input_setting_keys_value_200); - default -> { - customValueTextView.setText( - String.valueOf( - (double) inputSettingKeysThreshold / MILLISECOND_IN_SECONDS)); - customProgressBar.setProgress(inputSettingKeysThreshold / CUSTOM_PROGRESS_INTERVAL); - customRadioButton.setChecked(true); - } - } - } } diff --git a/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysController.java b/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysController.java index 9a0f1c0d6e3..69fbe6b7e3a 100644 --- a/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysController.java +++ b/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysController.java @@ -16,7 +16,6 @@ package com.android.settings.inputmethod; -import static android.app.settings.SettingsEnums.ACTION_BOUNCE_KEYS_CUSTOM_VALUE_CHANGE; import static android.app.settings.SettingsEnums.ACTION_BOUNCE_KEYS_DISABLED; import static android.app.settings.SettingsEnums.ACTION_BOUNCE_KEYS_ENABLED; @@ -32,13 +31,13 @@ import androidx.lifecycle.LifecycleObserver; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.settings.R; import com.android.settingslib.PrimarySwitchPreference; public class KeyboardAccessibilityBounceKeysController extends InputSettingPreferenceController implements LifecycleObserver { public static final int BOUNCE_KEYS_THRESHOLD = 500; + private static final String KEY_TAG = "bounce_keys_dialog_tag"; @Nullable private PrimarySwitchPreference mPrimaryPreference; @@ -46,8 +45,6 @@ public class KeyboardAccessibilityBounceKeysController extends public KeyboardAccessibilityBounceKeysController(@NonNull Context context, @NonNull String key) { super(context, key); - constructDialog(context, R.string.bounce_keys_dialog_title, - R.string.bounce_keys_dialog_subtitle); } @Override @@ -65,12 +62,11 @@ public class KeyboardAccessibilityBounceKeysController extends @Override public boolean handlePreferenceTreeClick(@NonNull Preference preference) { - if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { + if (!TextUtils.equals(preference.getKey(), getPreferenceKey()) + || mFragmentManager == null) { return false; } - if (mAlertDialog != null) { - mAlertDialog.show(); - } + KeyboardAccessibilityBounceKeysDialogFragment.getInstance().show(mFragmentManager, KEY_TAG); return true; } @@ -87,12 +83,6 @@ public class KeyboardAccessibilityBounceKeysController extends return true; } - @Override - protected void onCustomValueUpdated(int thresholdTimeMillis) { - mMetricsFeatureProvider.action(mContext, ACTION_BOUNCE_KEYS_CUSTOM_VALUE_CHANGE, - thresholdTimeMillis); - } - @Override protected void onInputSettingUpdated() { if (mPrimaryPreference != null) { @@ -111,9 +101,4 @@ public class KeyboardAccessibilityBounceKeysController extends protected void updateInputSettingKeysValue(int thresholdTimeMillis) { InputSettings.setAccessibilityBounceKeysThreshold(mContext, thresholdTimeMillis); } - - @Override - protected int getInputSettingKeysValue() { - return InputSettings.getAccessibilityBounceKeysThreshold(mContext); - } } diff --git a/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysDialogFragment.java b/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysDialogFragment.java new file mode 100644 index 00000000000..2dc90de84b1 --- /dev/null +++ b/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysDialogFragment.java @@ -0,0 +1,54 @@ +/* + * Copyright 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.inputmethod; + +import static android.app.settings.SettingsEnums.ACTION_BOUNCE_KEYS_CUSTOM_VALUE_CHANGE; + +import android.hardware.input.InputSettings; +import android.os.Bundle; + +import com.android.settings.R; + +public class KeyboardAccessibilityBounceKeysDialogFragment extends + KeyboardAccessibilityKeysDialogFragment { + + static KeyboardAccessibilityBounceKeysDialogFragment getInstance() { + final KeyboardAccessibilityBounceKeysDialogFragment result = + new KeyboardAccessibilityBounceKeysDialogFragment(); + Bundle bundle = new Bundle(); + bundle.putInt(EXTRA_TITLE_RES, R.string.bounce_keys_dialog_title); + bundle.putInt(EXTRA_SUBTITLE_RES, R.string.bounce_keys_dialog_subtitle); + result.setArguments(bundle); + return result; + } + + @Override + protected void updateInputSettingKeysValue(int thresholdTimeMillis) { + InputSettings.setAccessibilityBounceKeysThreshold(getContext(), thresholdTimeMillis); + } + + @Override + protected void onCustomValueUpdated(int thresholdTimeMillis) { + mMetricsFeatureProvider.action(getContext(), ACTION_BOUNCE_KEYS_CUSTOM_VALUE_CHANGE, + thresholdTimeMillis); + } + + @Override + protected int getInputSettingKeysValue() { + return InputSettings.getAccessibilityBounceKeysThreshold(getContext()); + } +} diff --git a/src/com/android/settings/inputmethod/KeyboardAccessibilityKeysDialogFragment.java b/src/com/android/settings/inputmethod/KeyboardAccessibilityKeysDialogFragment.java new file mode 100644 index 00000000000..bba47a72fd7 --- /dev/null +++ b/src/com/android/settings/inputmethod/KeyboardAccessibilityKeysDialogFragment.java @@ -0,0 +1,187 @@ +/* + * Copyright 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.inputmethod; + +import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.Window; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.fragment.app.DialogFragment; + +import com.android.settings.R; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +import org.jspecify.annotations.Nullable; + +import java.util.concurrent.TimeUnit; + +public abstract class KeyboardAccessibilityKeysDialogFragment extends DialogFragment { + private static final int CUSTOM_PROGRESS_INTERVAL = 100; + private static final long MILLISECOND_IN_SECONDS = TimeUnit.SECONDS.toMillis(1); + protected static final String EXTRA_TITLE_RES = "extra_title_res"; + protected static final String EXTRA_SUBTITLE_RES = "extra_subtitle_res"; + + protected final MetricsFeatureProvider mMetricsFeatureProvider; + + public KeyboardAccessibilityKeysDialogFragment() { + mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); + } + + protected void updateInputSettingKeysValue(int thresholdTimeMillis) { + } + + protected void onCustomValueUpdated(int thresholdTimeMillis) { + } + + protected int getInputSettingKeysValue() { + return 0; + } + + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + super.onCreateDialog(savedInstanceState); + int titleRes = getArguments().getInt(EXTRA_TITLE_RES); + int subtitleRes = getArguments().getInt(EXTRA_SUBTITLE_RES); + + Activity activity = getActivity(); + View dialoglayout = + LayoutInflater.from(activity).inflate( + R.layout.dialog_keyboard_a11y_input_setting_keys, null); + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity); + dialogBuilder.setView(dialoglayout); + dialogBuilder.setPositiveButton(android.R.string.ok, + (dialog, which) -> { + RadioGroup radioGroup = + dialoglayout.findViewById( + R.id.input_setting_keys_value_group); + SeekBar seekbar = dialoglayout.findViewById( + R.id.input_setting_keys_value_custom_slider); + RadioButton customRadioButton = dialoglayout.findViewById( + R.id.input_setting_keys_value_custom); + int threshold; + if (customRadioButton.isChecked()) { + threshold = seekbar.getProgress() * CUSTOM_PROGRESS_INTERVAL; + } else { + int checkedRadioButtonId = radioGroup.getCheckedRadioButtonId(); + if (checkedRadioButtonId == R.id.input_setting_keys_value_600) { + threshold = 600; + } else if (checkedRadioButtonId + == R.id.input_setting_keys_value_400) { + threshold = 400; + } else if (checkedRadioButtonId + == R.id.input_setting_keys_value_200) { + threshold = 200; + } else { + threshold = 0; + } + } + updateInputSettingKeysValue(threshold); + onCustomValueUpdated(threshold); + }) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss()); + AlertDialog accessibilityKeyDialog = dialogBuilder.create(); + accessibilityKeyDialog.setOnShowListener(dialog -> { + RadioGroup cannedValueRadioGroup = accessibilityKeyDialog.findViewById( + R.id.input_setting_keys_value_group); + RadioButton customRadioButton = accessibilityKeyDialog.findViewById( + R.id.input_setting_keys_value_custom); + TextView customValueTextView = accessibilityKeyDialog.findViewById( + R.id.input_setting_keys_value_custom_value); + SeekBar customProgressBar = accessibilityKeyDialog.findViewById( + R.id.input_setting_keys_value_custom_slider); + TextView titleTextView = accessibilityKeyDialog.findViewById( + R.id.input_setting_keys_dialog_title); + TextView subTitleTextView = accessibilityKeyDialog.findViewById( + R.id.input_setting_keys_dialog_subtitle); + titleTextView.setText(titleRes); + subTitleTextView.setText(subtitleRes); + + customProgressBar.incrementProgressBy(CUSTOM_PROGRESS_INTERVAL); + customProgressBar.setProgress(1); + View customValueView = accessibilityKeyDialog.findViewById( + R.id.input_setting_keys_custom_value_option); + customValueView.setOnClickListener(l -> customRadioButton.performClick()); + customRadioButton.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + cannedValueRadioGroup.clearCheck(); + } + customValueTextView.setVisibility(isChecked ? View.VISIBLE : View.GONE); + customValueTextView.setText( + progressToThresholdInSecond(customProgressBar.getProgress())); + customProgressBar.setVisibility(isChecked ? View.VISIBLE : View.GONE); + buttonView.setChecked(isChecked); + }); + cannedValueRadioGroup.setOnCheckedChangeListener( + (group, checkedId) -> customRadioButton.setChecked(false)); + customProgressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + customValueTextView.setText(progressToThresholdInSecond(progress)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + initStateBasedOnThreshold(cannedValueRadioGroup, customRadioButton, customValueTextView, + customProgressBar); + }); + + final Window window = accessibilityKeyDialog.getWindow(); + window.setType(TYPE_SYSTEM_DIALOG); + + return accessibilityKeyDialog; + } + + private static String progressToThresholdInSecond(int progress) { + return String.valueOf((double) progress * CUSTOM_PROGRESS_INTERVAL + / MILLISECOND_IN_SECONDS); + } + + private void initStateBasedOnThreshold(RadioGroup cannedValueRadioGroup, + RadioButton customRadioButton, TextView customValueTextView, + SeekBar customProgressBar) { + int inputSettingKeysThreshold = getInputSettingKeysValue(); + switch (inputSettingKeysThreshold) { + case 600 -> cannedValueRadioGroup.check(R.id.input_setting_keys_value_600); + case 400 -> cannedValueRadioGroup.check(R.id.input_setting_keys_value_400); + case 0, 200 -> cannedValueRadioGroup.check(R.id.input_setting_keys_value_200); + default -> { + customValueTextView.setText( + String.valueOf( + (double) inputSettingKeysThreshold / MILLISECOND_IN_SECONDS)); + customProgressBar.setProgress(inputSettingKeysThreshold / CUSTOM_PROGRESS_INTERVAL); + customRadioButton.setChecked(true); + } + } + } +} diff --git a/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysController.java b/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysController.java index 451742ff2d4..f3d74075846 100644 --- a/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysController.java +++ b/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysController.java @@ -16,7 +16,6 @@ package com.android.settings.inputmethod; -import static android.app.settings.SettingsEnums.ACTION_SLOW_KEYS_CUSTOM_VALUE_CHANGE; import static android.app.settings.SettingsEnums.ACTION_SLOW_KEYS_DISABLED; import static android.app.settings.SettingsEnums.ACTION_SLOW_KEYS_ENABLED; @@ -32,20 +31,19 @@ import androidx.lifecycle.LifecycleObserver; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.settings.R; import com.android.settingslib.PrimarySwitchPreference; public class KeyboardAccessibilitySlowKeysController extends InputSettingPreferenceController implements LifecycleObserver { public static final int SLOW_KEYS_THRESHOLD = 500; + private static final String KEY_TAG = "slow_keys_dialog_tag"; @Nullable private PrimarySwitchPreference mPrimarySwitchPreference; public KeyboardAccessibilitySlowKeysController(@NonNull Context context, @NonNull String key) { super(context, key); - constructDialog(context, R.string.slow_keys, R.string.slow_keys_summary); } @Override @@ -90,12 +88,11 @@ public class KeyboardAccessibilitySlowKeysController extends @Override public boolean handlePreferenceTreeClick(@NonNull Preference preference) { - if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { + if (!TextUtils.equals(preference.getKey(), getPreferenceKey()) + || mFragmentManager == null) { return false; } - if (mAlertDialog != null) { - mAlertDialog.show(); - } + KeyboardAccessibilitySlowKeysDialogFragment.getInstance().show(mFragmentManager, KEY_TAG); return true; } @@ -103,15 +100,4 @@ public class KeyboardAccessibilitySlowKeysController extends protected void updateInputSettingKeysValue(int thresholdTimeMillis) { InputSettings.setAccessibilitySlowKeysThreshold(mContext, thresholdTimeMillis); } - - @Override - protected void onCustomValueUpdated(int thresholdTimeMillis) { - mMetricsFeatureProvider.action(mContext, - ACTION_SLOW_KEYS_CUSTOM_VALUE_CHANGE, thresholdTimeMillis); - } - - @Override - protected int getInputSettingKeysValue() { - return InputSettings.getAccessibilitySlowKeysThreshold(mContext); - } } diff --git a/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysDialogFragment.java b/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysDialogFragment.java new file mode 100644 index 00000000000..fa3b6858cd5 --- /dev/null +++ b/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysDialogFragment.java @@ -0,0 +1,54 @@ +/* + * Copyright 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.inputmethod; + +import static android.app.settings.SettingsEnums.ACTION_SLOW_KEYS_CUSTOM_VALUE_CHANGE; + +import android.hardware.input.InputSettings; +import android.os.Bundle; + +import com.android.settings.R; + +public class KeyboardAccessibilitySlowKeysDialogFragment extends + KeyboardAccessibilityKeysDialogFragment { + + static KeyboardAccessibilitySlowKeysDialogFragment getInstance() { + final KeyboardAccessibilitySlowKeysDialogFragment result = + new KeyboardAccessibilitySlowKeysDialogFragment(); + Bundle bundle = new Bundle(); + bundle.putInt(EXTRA_TITLE_RES, R.string.slow_keys); + bundle.putInt(EXTRA_SUBTITLE_RES, R.string.slow_keys_summary); + result.setArguments(bundle); + return result; + } + + @Override + protected void updateInputSettingKeysValue(int thresholdTimeMillis) { + InputSettings.setAccessibilitySlowKeysThreshold(getContext(), thresholdTimeMillis); + } + + @Override + protected void onCustomValueUpdated(int thresholdTimeMillis) { + mMetricsFeatureProvider.action(getContext(), + ACTION_SLOW_KEYS_CUSTOM_VALUE_CHANGE, thresholdTimeMillis); + } + + @Override + protected int getInputSettingKeysValue() { + return InputSettings.getAccessibilitySlowKeysThreshold(getContext()); + } +} diff --git a/src/com/android/settings/inputmethod/PhysicalKeyboardA11yFragment.java b/src/com/android/settings/inputmethod/PhysicalKeyboardA11yFragment.java index 117aadbe4c8..67a808f48fa 100644 --- a/src/com/android/settings/inputmethod/PhysicalKeyboardA11yFragment.java +++ b/src/com/android/settings/inputmethod/PhysicalKeyboardA11yFragment.java @@ -51,6 +51,8 @@ public class PhysicalKeyboardA11yFragment extends DashboardFragment super.onAttach(context); mInputManager = Preconditions.checkNotNull(getActivity() .getSystemService(InputManager.class)); + use(KeyboardAccessibilitySlowKeysController.class).setFragment(this /*parent*/); + use(KeyboardAccessibilityBounceKeysController.class).setFragment(this /*parent*/); } @Override diff --git a/tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysControllerTest.java index b385b2f32a5..d9aa79c19b7 100644 --- a/tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysControllerTest.java +++ b/tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysControllerTest.java @@ -18,6 +18,9 @@ package com.android.settings.inputmethod; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; @@ -25,12 +28,12 @@ import android.hardware.input.InputSettings; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; -import android.widget.RadioGroup; -import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; import androidx.preference.Preference; -import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settings.keyboard.Flags; import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; @@ -45,10 +48,13 @@ import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import org.robolectric.shadows.ShadowLooper; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; @RunWith(RobolectricTestRunner.class) @Config(shadows = { + KeyboardAccessibilityBounceKeysControllerTest + .ShadowKeyboardAccessibilityBounceKeysDialogFragment.class, com.android.settings.testutils.shadow.ShadowFragment.class, ShadowAlertDialogCompat.class, }) @@ -60,6 +66,15 @@ public class KeyboardAccessibilityBounceKeysControllerTest { private static final String PREFERENCE_KEY = "keyboard_a11y_page_bounce_keys"; @Mock private Preference mPreference; + @Mock + private Fragment mFragment; + @Mock + private FragmentManager mFragmentManager; + @Mock + private FragmentTransaction mFragmentTransaction; + @Mock + private KeyboardAccessibilityBounceKeysDialogFragment + mKeyboardAccessibilityBounceKeysDialogFragment; private Context mContext; private KeyboardAccessibilityBounceKeysController mKeyboardAccessibilityBounceKeysController; @@ -71,6 +86,11 @@ public class KeyboardAccessibilityBounceKeysControllerTest { mContext, PREFERENCE_KEY); when(mPreference.getKey()).thenReturn(PREFERENCE_KEY); + when(mFragment.getParentFragmentManager()).thenReturn(mFragmentManager); + when(mFragmentManager.beginTransaction()).thenReturn(mFragmentTransaction); + mKeyboardAccessibilityBounceKeysController.setFragment(mFragment); + ShadowKeyboardAccessibilityBounceKeysDialogFragment.setInstance( + mKeyboardAccessibilityBounceKeysDialogFragment); } @Test @@ -107,23 +127,25 @@ public class KeyboardAccessibilityBounceKeysControllerTest { public void handlePreferenceTreeClick_dialogShows() { mKeyboardAccessibilityBounceKeysController.handlePreferenceTreeClick(mPreference); - AlertDialog alertDialog = ShadowAlertDialogCompat.getLatestAlertDialog(); - - assertThat(alertDialog.isShowing()).isTrue(); + verify(mKeyboardAccessibilityBounceKeysDialogFragment).show(any(FragmentManager.class), + anyString()); } - @Test - public void handlePreferenceTreeClick_performClickOn200_updatesBounceKeysThreshold() { - mKeyboardAccessibilityBounceKeysController.handlePreferenceTreeClick(mPreference); - AlertDialog alertDialog = ShadowAlertDialogCompat.getLatestAlertDialog(); - RadioGroup radioGroup = alertDialog.findViewById(R.id.input_setting_keys_value_group); - radioGroup.check(R.id.input_setting_keys_value_200); + /** + * Note: Actually, shadow of KeyboardAccessibilitySlowKeysDialogFragment will not be used. + * Instance that returned with {@link #getInstance} should be set with {@link #setInstance} + */ + @Implements(KeyboardAccessibilityBounceKeysDialogFragment.class) + public static class ShadowKeyboardAccessibilityBounceKeysDialogFragment { + static KeyboardAccessibilityBounceKeysDialogFragment sInstance = null; - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick(); - ShadowLooper.idleMainLooper(); + @Implementation + protected static KeyboardAccessibilityBounceKeysDialogFragment getInstance() { + return sInstance; + } - assertThat(alertDialog.isShowing()).isFalse(); - int threshold = InputSettings.getAccessibilityBounceKeysThreshold(mContext); - assertThat(threshold).isEqualTo(200); + public static void setInstance(KeyboardAccessibilityBounceKeysDialogFragment instance) { + sInstance = instance; + } } } diff --git a/tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysDialogFragmentTest.java b/tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysDialogFragmentTest.java new file mode 100644 index 00000000000..8e5f84e2191 --- /dev/null +++ b/tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysDialogFragmentTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 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.inputmethod; + +import static com.android.settings.inputmethod.KeyboardAccessibilityKeysDialogFragment.EXTRA_SUBTITLE_RES; +import static com.android.settings.inputmethod.KeyboardAccessibilityKeysDialogFragment.EXTRA_TITLE_RES; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.AlertDialog; +import android.hardware.input.InputSettings; +import android.os.Bundle; +import android.widget.RadioGroup; + +import androidx.fragment.app.testing.FragmentScenario; +import androidx.lifecycle.Lifecycle; +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 org.robolectric.shadows.ShadowLooper; + +@RunWith(RobolectricTestRunner.class) +public class KeyboardAccessibilityBounceKeysDialogFragmentTest { + private AlertDialog mAlertDialog; + + @Before + public void setUp() { + Bundle bundle = new Bundle(); + bundle.putInt(EXTRA_TITLE_RES, R.string.bounce_keys_dialog_title); + bundle.putInt(EXTRA_SUBTITLE_RES, R.string.bounce_keys_dialog_subtitle); + + FragmentScenario mFragmentScenario = + FragmentScenario.launch( + KeyboardAccessibilityBounceKeysDialogFragment.class, + bundle, + R.style.Theme_AlertDialog_SettingsLib, + Lifecycle.State.INITIALIZED); + mFragmentScenario.moveToState(Lifecycle.State.RESUMED); + + mFragmentScenario.onFragment(fragment -> { + assertThat(fragment.getDialog()).isNotNull(); + assertThat(fragment.requireDialog().isShowing()).isTrue(); + assertThat(fragment.requireDialog()).isInstanceOf(AlertDialog.class); + mAlertDialog = (AlertDialog) fragment.requireDialog(); + }); + } + + @Test + public void handlePreferenceTreeClick_performClickOn200_updatesBounceKeysThreshold() { + assertThat(mAlertDialog.isShowing()).isTrue(); + RadioGroup radioGroup = mAlertDialog.findViewById(R.id.input_setting_keys_value_group); + radioGroup.check(R.id.input_setting_keys_value_200); + + mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick(); + ShadowLooper.idleMainLooper(); + + assertThat(mAlertDialog.isShowing()).isFalse(); + int threshold = InputSettings.getAccessibilityBounceKeysThreshold( + ApplicationProvider.getApplicationContext()); + assertThat(threshold).isEqualTo(200); + } +} diff --git a/tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysControllerTest.java index 9f82b759cdc..137f15ba9d8 100644 --- a/tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysControllerTest.java +++ b/tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysControllerTest.java @@ -18,6 +18,9 @@ package com.android.settings.inputmethod; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; @@ -25,12 +28,12 @@ import android.hardware.input.InputSettings; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; -import android.widget.RadioGroup; -import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; import androidx.preference.Preference; -import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settings.keyboard.Flags; import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; @@ -45,10 +48,13 @@ import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import org.robolectric.shadows.ShadowLooper; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; @RunWith(RobolectricTestRunner.class) @Config(shadows = { + KeyboardAccessibilitySlowKeysControllerTest + .ShadowKeyboardAccessibilitySlowKeysDialogFragment.class, com.android.settings.testutils.shadow.ShadowFragment.class, ShadowAlertDialogCompat.class, }) @@ -60,6 +66,15 @@ public class KeyboardAccessibilitySlowKeysControllerTest { private static final String PREFERENCE_KEY = "keyboard_a11y_page_slow_keys"; @Mock private Preference mPreference; + @Mock + private Fragment mFragment; + @Mock + private FragmentManager mFragmentManager; + @Mock + private FragmentTransaction mFragmentTransaction; + @Mock + private KeyboardAccessibilitySlowKeysDialogFragment + mKeyboardAccessibilitySlowKeysDialogFragment; private Context mContext; private KeyboardAccessibilitySlowKeysController mKeyboardAccessibilitySlowKeysController; @@ -71,6 +86,11 @@ public class KeyboardAccessibilitySlowKeysControllerTest { mContext, PREFERENCE_KEY); when(mPreference.getKey()).thenReturn(PREFERENCE_KEY); + when(mFragment.getParentFragmentManager()).thenReturn(mFragmentManager); + when(mFragmentManager.beginTransaction()).thenReturn(mFragmentTransaction); + mKeyboardAccessibilitySlowKeysController.setFragment(mFragment); + ShadowKeyboardAccessibilitySlowKeysDialogFragment.setInstance( + mKeyboardAccessibilitySlowKeysDialogFragment); } @Test @@ -107,23 +127,25 @@ public class KeyboardAccessibilitySlowKeysControllerTest { public void handlePreferenceTreeClick_dialogShows() { mKeyboardAccessibilitySlowKeysController.handlePreferenceTreeClick(mPreference); - AlertDialog alertDialog = ShadowAlertDialogCompat.getLatestAlertDialog(); - - assertThat(alertDialog.isShowing()).isTrue(); + verify(mKeyboardAccessibilitySlowKeysDialogFragment).show(any(FragmentManager.class), + anyString()); } - @Test - public void handlePreferenceTreeClick_performClickOn200_updatesSlowKeysThreshold() { - mKeyboardAccessibilitySlowKeysController.handlePreferenceTreeClick(mPreference); - AlertDialog alertDialog = ShadowAlertDialogCompat.getLatestAlertDialog(); - RadioGroup radioGroup = alertDialog.findViewById(R.id.input_setting_keys_value_group); - radioGroup.check(R.id.input_setting_keys_value_200); + /** + * Note: Actually, shadow of KeyboardAccessibilitySlowKeysDialogFragment will not be used. + * Instance that returned with {@link #getInstance} should be set with {@link #setInstance} + */ + @Implements(KeyboardAccessibilitySlowKeysDialogFragment.class) + public static class ShadowKeyboardAccessibilitySlowKeysDialogFragment { + static KeyboardAccessibilitySlowKeysDialogFragment sInstance = null; - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick(); - ShadowLooper.idleMainLooper(); + @Implementation + protected static KeyboardAccessibilitySlowKeysDialogFragment getInstance() { + return sInstance; + } - assertThat(alertDialog.isShowing()).isFalse(); - int threshold = InputSettings.getAccessibilitySlowKeysThreshold(mContext); - assertThat(threshold).isEqualTo(200); + public static void setInstance(KeyboardAccessibilitySlowKeysDialogFragment instance) { + sInstance = instance; + } } } diff --git a/tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysDialogFragmentTest.java b/tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysDialogFragmentTest.java new file mode 100644 index 00000000000..3a3010e7293 --- /dev/null +++ b/tests/robotests/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysDialogFragmentTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 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.inputmethod; + +import static com.android.settings.inputmethod.KeyboardAccessibilityKeysDialogFragment.EXTRA_SUBTITLE_RES; +import static com.android.settings.inputmethod.KeyboardAccessibilityKeysDialogFragment.EXTRA_TITLE_RES; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.AlertDialog; +import android.hardware.input.InputSettings; +import android.os.Bundle; +import android.widget.RadioGroup; + +import androidx.fragment.app.testing.FragmentScenario; +import androidx.lifecycle.Lifecycle; +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 org.robolectric.shadows.ShadowLooper; + +@RunWith(RobolectricTestRunner.class) +public class KeyboardAccessibilitySlowKeysDialogFragmentTest { + private AlertDialog mAlertDialog; + + @Before + public void setUp() { + Bundle bundle = new Bundle(); + bundle.putInt(EXTRA_TITLE_RES, R.string.slow_keys); + bundle.putInt(EXTRA_SUBTITLE_RES, R.string.slow_keys_summary); + + FragmentScenario mFragmentScenario = + FragmentScenario.launch( + KeyboardAccessibilitySlowKeysDialogFragment.class, + bundle, + R.style.Theme_AlertDialog_SettingsLib, + Lifecycle.State.INITIALIZED); + mFragmentScenario.moveToState(Lifecycle.State.RESUMED); + + mFragmentScenario.onFragment(fragment -> { + assertThat(fragment.getDialog()).isNotNull(); + assertThat(fragment.requireDialog().isShowing()).isTrue(); + assertThat(fragment.requireDialog()).isInstanceOf(AlertDialog.class); + mAlertDialog = (AlertDialog) fragment.requireDialog(); + }); + } + + @Test + public void handlePreferenceTreeClick_performClickOn200_updatesSlowKeysThreshold() { + assertThat(mAlertDialog.isShowing()).isTrue(); + RadioGroup radioGroup = mAlertDialog.findViewById(R.id.input_setting_keys_value_group); + radioGroup.check(R.id.input_setting_keys_value_200); + + mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick(); + ShadowLooper.idleMainLooper(); + + assertThat(mAlertDialog.isShowing()).isFalse(); + int threshold = InputSettings.getAccessibilitySlowKeysThreshold( + ApplicationProvider.getApplicationContext()); + assertThat(threshold).isEqualTo(200); + } +} From d533c3e568263689db75f0e4d94a8553e2470d4f Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Wed, 5 Feb 2025 05:58:39 +0000 Subject: [PATCH 06/23] [Catalyst] Remove the INTERACT_ACROSS_PROFILES for Mobile data NO_IFTTT=Catalyst only Bug: 385282727 Flag: com.android.settings.flags.catalyst_mobile_network_list Test: devtool Change-Id: Ic664f6e8974eceaf74c8741d6122a2d0e3e679c9 --- src/com/android/settings/network/MobileDataPreference.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/com/android/settings/network/MobileDataPreference.kt b/src/com/android/settings/network/MobileDataPreference.kt index 2f2dbe2c760..80f58e0c704 100644 --- a/src/com/android/settings/network/MobileDataPreference.kt +++ b/src/com/android/settings/network/MobileDataPreference.kt @@ -58,8 +58,6 @@ class MobileDataPreference : override fun getWritePermissions(context: Context) = Permissions.allOf( - // SubscriptionManager.createForAllUserProfiles - Manifest.permission.INTERACT_ACROSS_PROFILES, // TelephonyManager.setDataEnabledForReason Manifest.permission.MODIFY_PHONE_STATE, ) From 01ba868b1e9b9cc240735f70593eb8bfa12b1d99 Mon Sep 17 00:00:00 2001 From: SongFerng Wang Date: Tue, 4 Feb 2025 22:22:23 -0800 Subject: [PATCH 07/23] Add the missed call of super.onCreateView() Bug: 393662651 Change-Id: I7f16fa45f3e32c6e1e78745248258b46836b7cbc Test: build pass Flag: EXEMPT bugfix --- src/com/android/settings/wifi/calling/WifiCallingSettings.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/com/android/settings/wifi/calling/WifiCallingSettings.java b/src/com/android/settings/wifi/calling/WifiCallingSettings.java index 0c3457ca77f..6110d2844b8 100644 --- a/src/com/android/settings/wifi/calling/WifiCallingSettings.java +++ b/src/com/android/settings/wifi/calling/WifiCallingSettings.java @@ -94,6 +94,7 @@ public class WifiCallingSettings extends SettingsPreferenceFragment @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); if (MobileNetworkUtils.isMobileNetworkUserRestricted(getActivity())) { return new ViewStub(getActivity()); } From 0afe4b3e90edd8cfe7d4e034af7acbb95df2932b Mon Sep 17 00:00:00 2001 From: shaoweishen Date: Wed, 5 Feb 2025 06:50:49 +0000 Subject: [PATCH 08/23] [Physical Keyboard] Correct key string for mouse key Bug:381247635 Flag: com.android.settings.keyboard.keyboard_and_touchpad_a11y_new_page_enabled Test: atest packages/apps/Settings/tests/robotests/src/com/android/settings/inputmethod/ Change-Id: I3ebeee285d33d3e25082a523da2d1d188f3b36bc --- .../inputmethod/KeyboardAccessibilityMouseKeysController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/settings/inputmethod/KeyboardAccessibilityMouseKeysController.java b/src/com/android/settings/inputmethod/KeyboardAccessibilityMouseKeysController.java index 2db0e05a2de..a8d9f30de0d 100644 --- a/src/com/android/settings/inputmethod/KeyboardAccessibilityMouseKeysController.java +++ b/src/com/android/settings/inputmethod/KeyboardAccessibilityMouseKeysController.java @@ -35,7 +35,7 @@ import com.android.settingslib.widget.MainSwitchPreference; public class KeyboardAccessibilityMouseKeysController extends InputSettingPreferenceController implements LifecycleObserver { - private static final String KEY_MOUSE_KEY = "accessibility_mouse_keys"; + private static final String KEY_MOUSE_KEY = "keyboard_a11y_page_mouse_keys"; private static final String KEY_MOUSE_KEY_MAIN_PAGE = "mouse_keys_main_switch"; @Nullable From e2051d50e308d05eef1e88a3008201f176c29620 Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Fri, 24 Jan 2025 07:08:46 +0000 Subject: [PATCH 09/23] [Biometric Onboarding & Edu] Update fingerprint settings page - Added a feature provider for fingerprint settings page in FingerprintFeatureProvider for customization - When no fingerprint enrolled, disabled the settings buttons instead of hiding them. - Update new UX style for add fingerprint button Bug: 370940762 Test: manual - 1. Enroll a fingerprint 2. Go Fingerprint Settings page and remove fingerprint 3. Enroll fingerprint again Test: atest FingerprintSettingsFragmentTest Flag: com.android.settings.flags.biometrics_onboarding_education Change-Id: Ibe47bb241c4b20e8e0c5b4a9172aef90bf3727ea --- res/values/strings.xml | 2 + res/xml/security_settings_fingerprint.xml | 5 + .../FingerprintFeatureProvider.java | 9 ++ .../fingerprint/FingerprintSettings.java | 95 +++++++++++++++---- ...printSettingsAppsPreferenceController.java | 15 +++ .../FingerprintSettingsFeatureProvider.kt | 43 +++++++++ ...ngsKeyguardUnlockPreferenceController.java | 19 ++++ .../FingerprintSettingsFragmentTest.java | 9 +- 8 files changed, 177 insertions(+), 20 deletions(-) create mode 100644 src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFeatureProvider.kt diff --git a/res/values/strings.xml b/res/values/strings.xml index 80b0e8aad8e..e6f4341bb9e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -955,6 +955,8 @@ Fingerprint Fingerprints + + Use fingerprint to diff --git a/res/xml/security_settings_fingerprint.xml b/res/xml/security_settings_fingerprint.xml index 701d493e749..2868351f802 100644 --- a/res/xml/security_settings_fingerprint.xml +++ b/res/xml/security_settings_fingerprint.xml @@ -19,6 +19,11 @@ xmlns:settings="http://schemas.android.com/apk/res-auto" android:title="@string/security_settings_fingerprint_preference_title"> + + items = mFingerprintManager.getEnrolledFingerprints(mUserId); final int fingerprintCount = items.size(); for (int i = 0; i < fingerprintCount; i++) { @@ -785,8 +800,7 @@ public class FingerprintSettings extends SubSettings { mFingerprintsEnrolledCategory.addPreference(pref); pref.setOnPreferenceChangeListener(this); } - mAddFingerprintPreference = findPreference(mIsExpressiveThemeStyle - ? KEY_FINGERPRINT_ADD_EXPRESSIVE : KEY_FINGERPRINT_ADD); + mAddFingerprintPreference = findPreference(getAddFingerprintPreferenceKey()); setupAddFingerprintPreference(); return keyToReturn; } @@ -839,18 +853,31 @@ public class FingerprintSettings extends SubSettings { } private void updateFingerprintUnlockCategoryVisibility() { - final boolean fingerprintUnlockCategoryAvailable = - mFingerprintUnlockCategoryPreferenceController.isAvailable(); - if (mFingerprintUnlockCategory.isVisible() != fingerprintUnlockCategoryAvailable) { - mFingerprintUnlockCategory.setVisible(fingerprintUnlockCategoryAvailable); - } + final int categoryStatus = + mFingerprintUnlockCategoryPreferenceController.getAvailabilityStatus(); + updatePreferenceVisibility(categoryStatus, mFingerprintUnlockCategory); + if (mRequireScreenOnToAuthPreferenceController != null) { - mRequireScreenOnToAuthPreference.setVisible( - mRequireScreenOnToAuthPreferenceController.isAvailable()); + final int status = + mRequireScreenOnToAuthPreferenceController.getAvailabilityStatus(); + updatePreferenceVisibility(status, mRequireScreenOnToAuthPreference); } if (mScreenOffUnlockUdfpsPreferenceController != null) { - mScreenOffUnlockUdfpsPreference.setVisible( - mScreenOffUnlockUdfpsPreferenceController.isAvailable()); + final int status = + mScreenOffUnlockUdfpsPreferenceController.getAvailabilityStatus(); + updatePreferenceVisibility(status, mScreenOffUnlockUdfpsPreference); + } + } + + private void updatePreferenceVisibility(int availabilityStatus, Preference preference) { + if (availabilityStatus == AVAILABLE) { + preference.setVisible(true); + preference.setEnabled(true); + } else if (availabilityStatus == CONDITIONALLY_UNAVAILABLE) { + preference.setVisible(true); + preference.setEnabled(false); + } else { + preference.setVisible(false); } } @@ -898,8 +925,29 @@ public class FingerprintSettings extends SubSettings { .setOnPreferenceChangeListener(fingerprintAppController); } + private void updateUseFingerprintToEnableStatus() { + final PreferenceCategory category = + findPreference(KEY_BIOMETRICS_USE_FINGERPRINT_TO_CATEGORY); + if (!category.isVisible()) { + return; + } + final boolean hasFingerprintEnrolled = + mFingerprintManager.getEnrolledFingerprints(mUserId).size() > 0; + + final FingerprintSettingsKeyguardUnlockPreferenceController fpUnlockController = + use(FingerprintSettingsKeyguardUnlockPreferenceController.class); + findPreference(fpUnlockController.getPreferenceKey()) + .setEnabled(hasFingerprintEnrolled); + + final FingerprintSettingsAppsPreferenceController fingerprintAppController = + use(FingerprintSettingsAppsPreferenceController.class); + findPreference(fingerprintAppController.getPreferenceKey()) + .setEnabled(hasFingerprintEnrolled); + } + private void updatePreferencesAfterFingerprintRemoved() { updateAddPreference(); + updateUseFingerprintToEnableStatus(); if (isSfps() || (screenOffUnlockUdfps() && isScreenOffUnlcokSupported())) { updateFingerprintUnlockCategoryVisibility(); } @@ -911,8 +959,7 @@ public class FingerprintSettings extends SubSettings { return; // Activity went away } - mAddFingerprintPreference = findPreference( - mIsExpressiveThemeStyle ? KEY_FINGERPRINT_ADD_EXPRESSIVE : KEY_FINGERPRINT_ADD); + mAddFingerprintPreference = findPreference(getAddFingerprintPreferenceKey()); if (mAddFingerprintPreference == null) { return; // b/275519315 Skip if updateAddPreference() invoke before addPreference() @@ -945,11 +992,11 @@ public class FingerprintSettings extends SubSettings { findPreference(KEY_FINGERPRINT_ADD_EXPRESSIVE); if (nonExpressiveBtnPreference != null) { - nonExpressiveBtnPreference.setVisible(!mIsExpressiveThemeStyle); + nonExpressiveBtnPreference.setVisible(!shouldShowExpressiveAddFingerprintPref()); } if (expressiveBtnPreference != null) { - expressiveBtnPreference.setVisible(mIsExpressiveThemeStyle); + expressiveBtnPreference.setVisible(shouldShowExpressiveAddFingerprintPref()); } } @@ -1053,7 +1100,7 @@ public class FingerprintSettings extends SubSettings { @Override public boolean onPreferenceTreeClick(Preference pref) { final String key = pref.getKey(); - if (!mIsExpressiveThemeStyle && KEY_FINGERPRINT_ADD.equals(key)) { + if (KEY_FINGERPRINT_ADD.equals(key)) { mIsEnrolling = true; Intent intent = new Intent(); intent.setClassName(SETTINGS_PACKAGE_NAME, @@ -1451,6 +1498,16 @@ public class FingerprintSettings extends SubSettings { return Utils.isPrivateProfile(mUserId, getContext()); } + private String getAddFingerprintPreferenceKey() { + return shouldShowExpressiveAddFingerprintPref() + ? KEY_FINGERPRINT_ADD_EXPRESSIVE : KEY_FINGERPRINT_ADD; + } + + private boolean shouldShowExpressiveAddFingerprintPref() { + return Flags.biometricsOnboardingEducation() && mIsExpressiveThemeStyle + && mFingerprintManager.hasEnrolledTemplates(mUserId); + } + public static class DeleteFingerprintDialog extends InstrumentedDialogFragment implements DialogInterface.OnClickListener { diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsAppsPreferenceController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsAppsPreferenceController.java index 2cd92fc03e4..e53ba85bea5 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsAppsPreferenceController.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsAppsPreferenceController.java @@ -23,6 +23,7 @@ import android.hardware.fingerprint.FingerprintManager; import android.provider.Settings; import androidx.annotation.NonNull; +import androidx.preference.Preference; import com.android.settings.Utils; import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils; @@ -53,6 +54,20 @@ public class FingerprintSettingsAppsPreferenceController isChecked ? ON : OFF, getUserId()); } + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (!FingerprintSettings.isFingerprintHardwareDetected(mContext)) { + preference.setEnabled(false); + } else if (!mFingerprintManager.hasEnrolledTemplates(getUserId())) { + preference.setEnabled(false); + } else if (getRestrictingAdmin() != null) { + preference.setEnabled(false); + } else { + preference.setEnabled(true); + } + } + @Override public int getAvailabilityStatus() { final ActiveUnlockStatusUtils activeUnlockStatusUtils = diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFeatureProvider.kt b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFeatureProvider.kt new file mode 100644 index 00000000000..3138a0bbe00 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFeatureProvider.kt @@ -0,0 +1,43 @@ +/* + * 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.biometrics.fingerprint + +import com.android.settings.R + +/** + * Provide features for FingerprintSettings page. + */ +open class FingerprintSettingsFeatureProvider { + /** + * Get the description shown in the FingerprintSetting page. + */ + open fun getSettingPageDescription(): Int { + return 0 + } + + /** + * Get the learn more description shown in the footer of the FingerprintSetting page. + */ + open fun getSettingPageFooterLearnMoreDescription(): Int { + return R.string.security_settings_fingerprint_settings_footer_learn_more + } + + companion object { + @JvmStatic + val instance = FingerprintSettingsFeatureProvider() + } +} \ No newline at end of file diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsKeyguardUnlockPreferenceController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsKeyguardUnlockPreferenceController.java index 55c75abbcae..6b17584320f 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsKeyguardUnlockPreferenceController.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsKeyguardUnlockPreferenceController.java @@ -19,9 +19,11 @@ package com.android.settings.biometrics.fingerprint; import static android.provider.Settings.Secure.FINGERPRINT_KEYGUARD_ENABLED; import android.content.Context; +import android.hardware.fingerprint.FingerprintManager; import android.provider.Settings; import androidx.annotation.NonNull; +import androidx.preference.Preference; import com.android.settings.Utils; import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils; @@ -33,9 +35,12 @@ public class FingerprintSettingsKeyguardUnlockPreferenceController private static final int OFF = 0; private static final int DEFAULT = ON; + private FingerprintManager mFingerprintManager; + public FingerprintSettingsKeyguardUnlockPreferenceController( @NonNull Context context, @NonNull String key) { super(context, key); + mFingerprintManager = Utils.getFingerprintManagerOrNull(context); } @Override @@ -50,6 +55,20 @@ public class FingerprintSettingsKeyguardUnlockPreferenceController FINGERPRINT_KEYGUARD_ENABLED, isChecked ? ON : OFF, getUserId()); } + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (!FingerprintSettings.isFingerprintHardwareDetected(mContext)) { + preference.setEnabled(false); + } else if (!mFingerprintManager.hasEnrolledTemplates(getUserId())) { + preference.setEnabled(false); + } else if (getRestrictingAdmin() != null) { + preference.setEnabled(false); + } else { + preference.setEnabled(true); + } + } + @Override public int getAvailabilityStatus() { final ActiveUnlockStatusUtils activeUnlockStatusUtils = diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java index 02825d2aa50..47ba523c7b2 100644 --- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java +++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java @@ -135,16 +135,20 @@ public class FingerprintSettingsFragmentTest { @Mock private Vibrator mVibrator; + private FingerprintSettingsFeatureProvider mFingerprintSettingsFeatureProvider; + + private FakeFeatureFactory mFakeFeatureFactory; private FingerprintAuthenticateSidecar mFingerprintAuthenticateSidecar; private FingerprintRemoveSidecar mFingerprintRemoveSidecar; @Before public void setUp() { ShadowUtils.setFingerprintManager(mFingerprintManager); - FakeFeatureFactory.setupForTest(); + mFakeFeatureFactory = FakeFeatureFactory.setupForTest(); mContext = spy(ApplicationProvider.getApplicationContext()); mFragment = spy(new FingerprintSettingsFragment()); + mFingerprintSettingsFeatureProvider = new FingerprintSettingsFeatureProvider(); doReturn(mContext).when(mFragment).getContext(); doReturn(mBiometricManager).when(mContext).getSystemService(BiometricManager.class); doReturn(true).when(mFingerprintManager).isHardwareDetected(); @@ -152,6 +156,9 @@ public class FingerprintSettingsFragmentTest { when(mBiometricManager.canAuthenticate(PRIMARY_USER_ID, BiometricManager.Authenticators.IDENTITY_CHECK)) .thenReturn(BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE); + when(mFakeFeatureFactory.getFingerprintFeatureProvider() + .getFingerprintSettingsFeatureProvider()) + .thenReturn(mFingerprintSettingsFeatureProvider); } @After From 55b7b0613d31dd632ba7d3dec68d958d4b77c0f2 Mon Sep 17 00:00:00 2001 From: Yuri Ufimtsev Date: Tue, 4 Feb 2025 14:57:33 +0000 Subject: [PATCH 10/23] Allow the LockPatterUtils to be null In some tests it could inconsistently become a null, crashing with NPE, for the reason I can't understand (maybe there is a race condition somewhere between mocking and accessing the LockPatterUtils in such tests) Let's accept it gracefully Test: atest SettingsUnitTests Bug: 382637173 Flag: EXEMPT bugfix Change-Id: I35d5faa1a29307fc780207a816680a32b2796bc0 --- .../safetycenter/LockScreenSafetySource.java | 9 ++++- .../ScreenLockPreferenceDetailsUtils.java | 9 ++++- .../ScreenLockPreferenceDetailsUtilsTest.java | 37 ++++++++++++++++++- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/com/android/settings/safetycenter/LockScreenSafetySource.java b/src/com/android/settings/safetycenter/LockScreenSafetySource.java index 14ad268c3cf..00a4c676a80 100644 --- a/src/com/android/settings/safetycenter/LockScreenSafetySource.java +++ b/src/com/android/settings/safetycenter/LockScreenSafetySource.java @@ -95,8 +95,7 @@ public final class LockScreenSafetySource { new SafetySourceStatus.Builder( context.getString(R.string.unlock_set_unlock_launch_picker_title), lockScreenAllowedByAdmin - ? screenLockPreferenceDetailsUtils.getSummary( - UserHandle.myUserId()) + ? getScreenLockSummary(screenLockPreferenceDetailsUtils) : context.getString(R.string.disabled_by_policy_title), severityLevel) .setPendingIntent(lockScreenAllowedByAdmin ? pendingIntent : null) @@ -114,6 +113,12 @@ public final class LockScreenSafetySource { .setSafetySourceData(context, SAFETY_SOURCE_ID, safetySourceData, safetyEvent); } + private static String getScreenLockSummary( + ScreenLockPreferenceDetailsUtils screenLockPreferenceDetailsUtils) { + String summary = screenLockPreferenceDetailsUtils.getSummary(UserHandle.myUserId()); + return summary != null ? summary : ""; + } + /** Notifies Safety Center of a change in lock screen settings. */ public static void onLockScreenChange(Context context) { setSafetySourceData( diff --git a/src/com/android/settings/security/ScreenLockPreferenceDetailsUtils.java b/src/com/android/settings/security/ScreenLockPreferenceDetailsUtils.java index bc38feb20dc..b1685685bb4 100644 --- a/src/com/android/settings/security/ScreenLockPreferenceDetailsUtils.java +++ b/src/com/android/settings/security/ScreenLockPreferenceDetailsUtils.java @@ -23,6 +23,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageManager; +import androidx.annotation.Nullable; import androidx.annotation.StringRes; import com.android.internal.app.UnlaunchableAppActivity; @@ -43,6 +44,7 @@ public class ScreenLockPreferenceDetailsUtils { private final int mUserId = UserHandle.myUserId(); private final Context mContext; + @Nullable private final LockPatternUtils mLockPatternUtils; private final int mProfileChallengeUserId; private final UserManager mUm; @@ -85,7 +87,7 @@ public class ScreenLockPreferenceDetailsUtils { * Returns whether the lock pattern is secure. */ public boolean isLockPatternSecure() { - return mLockPatternUtils.isSecure(mUserId); + return mLockPatternUtils != null && mLockPatternUtils.isSecure(mUserId); } /** @@ -148,6 +150,7 @@ public class ScreenLockPreferenceDetailsUtils { // profile with unified challenge on FBE-enabled devices. Otherwise, vold would not be // able to complete the operation due to the lack of (old) encryption key. if (mProfileChallengeUserId != UserHandle.USER_NULL + && mLockPatternUtils != null && !mLockPatternUtils.isSeparateProfileChallengeEnabled(mProfileChallengeUserId) && StorageManager.isFileEncrypted()) { if (mUm.isQuietModeEnabled(UserHandle.of(mProfileChallengeUserId))) { @@ -166,8 +169,12 @@ public class ScreenLockPreferenceDetailsUtils { .toIntent(); } + @Nullable @StringRes private Integer getSummaryResId(int userId) { + if (mLockPatternUtils == null) { + return null; + } if (!mLockPatternUtils.isSecure(userId)) { if (userId == mProfileChallengeUserId || mLockPatternUtils.isLockScreenDisabled(userId)) { diff --git a/tests/unit/src/com/android/settings/security/ScreenLockPreferenceDetailsUtilsTest.java b/tests/unit/src/com/android/settings/security/ScreenLockPreferenceDetailsUtilsTest.java index abc982db33c..25358708043 100644 --- a/tests/unit/src/com/android/settings/security/ScreenLockPreferenceDetailsUtilsTest.java +++ b/tests/unit/src/com/android/settings/security/ScreenLockPreferenceDetailsUtilsTest.java @@ -78,6 +78,7 @@ public class ScreenLockPreferenceDetailsUtilsTest { private StorageManager mStorageManager; private Context mContext; + private FakeFeatureFactory mFeatureFactory; private ScreenLockPreferenceDetailsUtils mScreenLockPreferenceDetailsUtils; @@ -95,8 +96,8 @@ public class ScreenLockPreferenceDetailsUtilsTest { doNothing().when(mContext).startActivity(any()); when(mUserManager.getProfileIdsWithDisabled(anyInt())).thenReturn(new int[]{}); - final FakeFeatureFactory featureFactory = FakeFeatureFactory.setupForTest(); - when(featureFactory.securityFeatureProvider.getLockPatternUtils(mContext)) + mFeatureFactory = FakeFeatureFactory.setupForTest(); + when(mFeatureFactory.securityFeatureProvider.getLockPatternUtils(mContext)) .thenReturn(mLockPatternUtils); mScreenLockPreferenceDetailsUtils = new ScreenLockPreferenceDetailsUtils(mContext); @@ -230,6 +231,15 @@ public class ScreenLockPreferenceDetailsUtilsTest { assertNull(mScreenLockPreferenceDetailsUtils.getSummary(USER_ID)); } + @Test + public void getSummary_noLockPatternUtils_shouldReturnNull() { + when(mFeatureFactory.securityFeatureProvider.getLockPatternUtils(mContext)) + .thenReturn(null); + mScreenLockPreferenceDetailsUtils = new ScreenLockPreferenceDetailsUtils(mContext); + + assertNull(mScreenLockPreferenceDetailsUtils.getSummary(USER_ID)); + } + @Test public void isPasswordQualityManaged_withoutAdmin_shouldReturnFalse() { final RestrictedLockUtils.EnforcedAdmin admin = null; @@ -274,6 +284,15 @@ public class ScreenLockPreferenceDetailsUtilsTest { assertThat(mScreenLockPreferenceDetailsUtils.isLockPatternSecure()).isFalse(); } + @Test + public void isLockPatternSecure_noLockPatterUtils_shouldReturnFalse() { + when(mFeatureFactory.securityFeatureProvider.getLockPatternUtils(mContext)) + .thenReturn(null); + mScreenLockPreferenceDetailsUtils = new ScreenLockPreferenceDetailsUtils(mContext); + + assertThat(mScreenLockPreferenceDetailsUtils.isLockPatternSecure()).isFalse(); + } + @Test @RequiresFlagsEnabled(Flags.FLAG_BIOMETRIC_ONBOARDING_EDUCATION) public void shouldShowGearMenu_patternIsSecure_flagOn_shouldReturnFalse() { @@ -341,6 +360,20 @@ public class ScreenLockPreferenceDetailsUtilsTest { ChooseLockGeneric.ChooseLockGenericFragment.class.getName()); } + @Test + public void getLaunchChooseLockGenericFragmentIntent_noLockPatternUtils_returnsIntent() { + when(mFeatureFactory.securityFeatureProvider.getLockPatternUtils(mContext)) + .thenReturn(null); + mScreenLockPreferenceDetailsUtils = new ScreenLockPreferenceDetailsUtils(mContext); + when(mUserManager.isQuietModeEnabled(any())).thenReturn(false); + + final Intent intent = mScreenLockPreferenceDetailsUtils + .getLaunchChooseLockGenericFragmentIntent(SOURCE_METRICS_CATEGORY); + + assertFragmentLaunchIntent(intent, + ChooseLockGeneric.ChooseLockGenericFragment.class.getName()); + } + private void whenConfigShowUnlockSetOrChangeIsEnabled(boolean enabled) { final int resId = ResourcesUtils.getResourcesId( ApplicationProvider.getApplicationContext(), "bool", From 11debff2be2f195f8bdadc8faf3cbdcad046d6e7 Mon Sep 17 00:00:00 2001 From: Angela Wang Date: Wed, 5 Feb 2025 15:19:31 +0000 Subject: [PATCH 11/23] Hide ambient control when there's no valid control point The preference will be accidentally shown if the device support HAP and VCP. Make sure to hide the preference when the device doesn't have any valid ambient control points. Flag: EXEMPT bugfix Test: manual test with real device Test: atest BluetoothDetailsAmbientVolumePreferenceControllerTest Bug: 388156028 Change-Id: I8162ce8e878baff414e4665c97aaf1c21aa51229 --- ...ailsAmbientVolumePreferenceController.java | 7 ++- ...AmbientVolumePreferenceControllerTest.java | 49 ++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java b/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java index defa7e9326d..8dbfedf2f10 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java @@ -129,8 +129,11 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends Bluetooth @Override public boolean isAvailable() { - return mCachedDevice.isHearingDevice() && mCachedDevice.getProfiles().stream().anyMatch( - profile -> profile instanceof VolumeControlProfile); + return mCachedDevice.isHearingDevice() + && mCachedDevice.getProfiles().stream().anyMatch( + profile -> profile instanceof VolumeControlProfile) + && mAmbientUiController != null + && mAmbientUiController.isAmbientControlAvailable(); } @Nullable diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java index 4c20a8888bc..a7523efcf96 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java @@ -98,30 +98,44 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends @Test public void isAvailable_notHearingDevice_returnFalse() { when(mCachedDevice.isHearingDevice()).thenReturn(false); + when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); + when(mUiController.isAmbientControlAvailable()).thenReturn(true); assertThat(mController.isAvailable()).isFalse(); } @Test - public void isAvailable_isHearingDeviceAndNotSupportVcp_returnFalse() { + public void isAvailable_notSupportVcp_returnFalse() { when(mCachedDevice.isHearingDevice()).thenReturn(true); when(mCachedDevice.getProfiles()).thenReturn(List.of()); + when(mUiController.isAmbientControlAvailable()).thenReturn(true); assertThat(mController.isAvailable()).isFalse(); } @Test - public void isAvailable_isHearingDeviceAndSupportVcp_returnTrue() { + public void isAvailable_noValidAmbientControlPoint_returnFalse() { when(mCachedDevice.isHearingDevice()).thenReturn(true); when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); + when(mUiController.isAmbientControlAvailable()).thenReturn(false); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_isHearingDevice_supportVcp_validAmbientControl_returnTrue() { + when(mCachedDevice.isHearingDevice()).thenReturn(true); + when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); + when(mUiController.isAmbientControlAvailable()).thenReturn(true); assertThat(mController.isAvailable()).isTrue(); } @Test - public void refresh_isHearingDeviceAndNotSupportVcp_verifyUiControllerNoRefresh() { - when(mCachedDevice.isHearingDevice()).thenReturn(true); - when(mCachedDevice.getProfiles()).thenReturn(List.of()); + public void refresh_notHearingDevice_verifyUiControllerNotRefresh() { + when(mCachedDevice.isHearingDevice()).thenReturn(false); + when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); + when(mUiController.isAmbientControlAvailable()).thenReturn(true); mController.refresh(); @@ -129,9 +143,32 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends } @Test - public void refresh_isHearingDeviceAndSupportVcp_verifyUiControllerRefresh() { + public void refresh_notSupportVcp_verifyUiControllerNotRefresh() { + when(mCachedDevice.isHearingDevice()).thenReturn(true); + when(mCachedDevice.getProfiles()).thenReturn(List.of()); + when(mUiController.isAmbientControlAvailable()).thenReturn(true); + + mController.refresh(); + + verify(mUiController, never()).refresh(); + } + + @Test + public void refresh_noValidAmbientControl_verifyUiControllerNotRefresh() { when(mCachedDevice.isHearingDevice()).thenReturn(true); when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); + when(mUiController.isAmbientControlAvailable()).thenReturn(false); + + mController.refresh(); + + verify(mUiController, never()).refresh(); + } + + @Test + public void refresh_isHearingDevice_supportVcp_validAmbientControl_verifyUiControllerRefresh() { + when(mCachedDevice.isHearingDevice()).thenReturn(true); + when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); + when(mUiController.isAmbientControlAvailable()).thenReturn(true); mController.refresh(); From b197bfe8f2cd10ccddb4755a366d58aeb3e91183 Mon Sep 17 00:00:00 2001 From: Liefu Liu Date: Wed, 5 Feb 2025 08:49:17 -0800 Subject: [PATCH 12/23] Updated the Contacts Storage Settings' bottom line text. Bug: 394335832 Test: unit test and manual test Flag: EXEMPTED modified: res/values/strings.xml modified: tests/robotests/src/com/android/settings/applications/contacts/ContactsStorageSettingsTest.java Change-Id: Ibde23352a6c64ff7ea546e2fd5ef9468398829f7 --- res/values/strings.xml | 2 +- .../applications/contacts/ContactsStorageSettingsTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 483eb130658..4727c40d1ff 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -14080,7 +14080,7 @@ Device only - New contacts won\'t be synced with an account + Contacts may not sync or be available on your other devices Contacts will be saved to your device and synced to your account by default diff --git a/tests/robotests/src/com/android/settings/applications/contacts/ContactsStorageSettingsTest.java b/tests/robotests/src/com/android/settings/applications/contacts/ContactsStorageSettingsTest.java index d99dd30d419..0abacec1d19 100644 --- a/tests/robotests/src/com/android/settings/applications/contacts/ContactsStorageSettingsTest.java +++ b/tests/robotests/src/com/android/settings/applications/contacts/ContactsStorageSettingsTest.java @@ -155,7 +155,7 @@ public class ContactsStorageSettingsTest { assertThat(deviceOnlyPreference.getTitle()).isEqualTo("Device only"); assertThat(deviceOnlyPreference.getSummary()).isEqualTo( - "New contacts won't be synced with an account"); + "Contacts may not sync or be available on your other devices"); assertThat(deviceOnlyPreference.getOrder()).isEqualTo(999); assertThat(mContactsStorageSettings.findPreference( PREF_KEY_ACCOUNT_CATEGORY).getTitle()).isEqualTo("Where to save contacts"); From b136fcfe184150ba1f2cd5aa963e81aa4aa83c8c Mon Sep 17 00:00:00 2001 From: Vlad Popa Date: Wed, 5 Feb 2025 16:04:50 -0800 Subject: [PATCH 13/23] Remove rolled out flag automatic_bt_device_type This flag has been rolled out in 24Q3 and can be removed. Test: atest BluetoothDetailsAudioDeviceTypeControllerTest Bug: 302323921 Flag: EXEMPT removing com.android.media.automatic_bt_device_type Change-Id: I9997ef60f195b36298c36078eadf6c0a82bc662d --- ...toothDetailsAudioDeviceTypeController.java | 30 ++++--------------- ...hDetailsAudioDeviceTypeControllerTest.java | 17 ++--------- 2 files changed, 9 insertions(+), 38 deletions(-) diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeController.java b/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeController.java index e7c4c19a3f0..39305808096 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeController.java @@ -16,14 +16,12 @@ package com.android.settings.bluetooth; -import static android.bluetooth.BluetoothDevice.DEVICE_TYPE_LE; import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_CARKIT; import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES; import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEARING_AID; import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_OTHER; import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER; import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN; -import static android.media.audio.Flags.automaticBtDeviceType; import android.content.Context; import android.media.AudioManager; @@ -108,15 +106,8 @@ public class BluetoothDetailsAudioDeviceTypeController extends BluetoothDetailsC final int index = pref.findIndexOfValue(value); if (index >= 0) { pref.setSummary(pref.getEntries()[index]); - if (automaticBtDeviceType()) { - mAudioManager.setBluetoothAudioDeviceCategory( - mCachedDevice.getAddress(), Integer.parseInt(value)); - } else { - mAudioManager.setBluetoothAudioDeviceCategory_legacy( - mCachedDevice.getAddress(), - mCachedDevice.getDevice().getType() == DEVICE_TYPE_LE, - Integer.parseInt(value)); - } + mAudioManager.setBluetoothAudioDeviceCategory( + mCachedDevice.getAddress(), Integer.parseInt(value)); mCachedDevice.onAudioDeviceCategoryChanged(); } } @@ -174,15 +165,8 @@ public class BluetoothDetailsAudioDeviceTypeController extends BluetoothDetailsC Integer.toString(AUDIO_DEVICE_CATEGORY_OTHER), }); - @AudioDeviceCategory int deviceCategory; - if (automaticBtDeviceType()) { - deviceCategory = mAudioManager.getBluetoothAudioDeviceCategory( - mCachedDevice.getAddress()); - } else { - deviceCategory = mAudioManager.getBluetoothAudioDeviceCategory_legacy( - mCachedDevice.getAddress(), - mCachedDevice.getDevice().getType() == DEVICE_TYPE_LE); - } + @AudioDeviceCategory int deviceCategory = mAudioManager.getBluetoothAudioDeviceCategory( + mCachedDevice.getAddress()); if (DEBUG) { Log.v(TAG, "getBluetoothAudioDeviceCategory() device: " + mCachedDevice.getDevice().getAnonymizedAddress() @@ -190,10 +174,8 @@ public class BluetoothDetailsAudioDeviceTypeController extends BluetoothDetailsC } mAudioDeviceTypePreference.setValue(Integer.toString(deviceCategory)); - if (automaticBtDeviceType()) { - if (mAudioManager.isBluetoothAudioDeviceCategoryFixed(mCachedDevice.getAddress())) { - mAudioDeviceTypePreference.setEnabled(false); - } + if (mAudioManager.isBluetoothAudioDeviceCategoryFixed(mCachedDevice.getAddress())) { + mAudioDeviceTypePreference.setEnabled(false); } mAudioDeviceTypePreference.setSummary(mAudioDeviceTypePreference.getEntry()); diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeControllerTest.java index 20105022ef1..3cdb302d673 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeControllerTest.java @@ -18,7 +18,6 @@ package com.android.settings.bluetooth; import static android.bluetooth.BluetoothDevice.DEVICE_TYPE_LE; import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER; -import static android.media.audio.Flags.automaticBtDeviceType; import static com.google.common.truth.Truth.assertThat; @@ -98,12 +97,7 @@ public class BluetoothDetailsAudioDeviceTypeControllerTest extends @Test public void createAudioDeviceTypePreference_btDeviceIsCategorized_checkSelection() { int deviceType = AUDIO_DEVICE_CATEGORY_SPEAKER; - if (automaticBtDeviceType()) { - when(mAudioManager.getBluetoothAudioDeviceCategory(MAC_ADDRESS)).thenReturn(deviceType); - } else { - when(mAudioManager.getBluetoothAudioDeviceCategory_legacy(MAC_ADDRESS, /*isBle=*/ - true)).thenReturn(deviceType); - } + when(mAudioManager.getBluetoothAudioDeviceCategory(MAC_ADDRESS)).thenReturn(deviceType); mController.createAudioDeviceTypePreference(mContext); mAudioDeviceTypePref = mController.getAudioDeviceTypePreference(); @@ -118,12 +112,7 @@ public class BluetoothDetailsAudioDeviceTypeControllerTest extends mController.onPreferenceChange(mAudioDeviceTypePref, Integer.toString(deviceType)); - if (automaticBtDeviceType()) { - verify(mAudioManager).setBluetoothAudioDeviceCategory(eq(MAC_ADDRESS), - eq(AUDIO_DEVICE_CATEGORY_SPEAKER)); - } else { - verify(mAudioManager).setBluetoothAudioDeviceCategory_legacy(eq(MAC_ADDRESS), eq(true), - eq(AUDIO_DEVICE_CATEGORY_SPEAKER)); - } + verify(mAudioManager).setBluetoothAudioDeviceCategory(eq(MAC_ADDRESS), + eq(AUDIO_DEVICE_CATEGORY_SPEAKER)); } } From 069f926bbaf662f4429f7902915d2b9a1f438388 Mon Sep 17 00:00:00 2001 From: Liefu Liu Date: Wed, 5 Feb 2025 15:25:50 -0800 Subject: [PATCH 14/23] Update the sub title of SIM preference in Contacts Storage Setting, to be consistent with "Device-only" preference. Bug: 394335832 Test: atest ContactsStorageSettingsTest and manually test Flag: EXEMPTED (minor fix) modified: src/com/android/settings/applications/contacts/ContactsStorageSettings.java modified: tests/robotests/src/com/android/settings/applications/contacts/ContactsStorageSettingsTest.java Change-Id: Ibf52d237da468d951211e9afa323d61fa72a1206 --- .../applications/contacts/ContactsStorageSettings.java | 2 +- .../applications/contacts/ContactsStorageSettingsTest.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/applications/contacts/ContactsStorageSettings.java b/src/com/android/settings/applications/contacts/ContactsStorageSettings.java index 4b70d465fb1..1eb6ab0aafb 100644 --- a/src/com/android/settings/applications/contacts/ContactsStorageSettings.java +++ b/src/com/android/settings/applications/contacts/ContactsStorageSettings.java @@ -232,7 +232,7 @@ public class ContactsStorageSettings extends DashboardFragment getPrefContext()); preference.setTitle(R.string.sim_card_label); preference.setIcon(R.drawable.ic_sim_card); - preference.setSummary(R.string.sim_card_label); + preference.setSummary(R.string.contacts_storage_device_only_preference_summary); preference.setKey(preferenceKey); preference.setOnClickListener(this); mAccountMap.put(preferenceKey, currentDefaultAccountAndState); diff --git a/tests/robotests/src/com/android/settings/applications/contacts/ContactsStorageSettingsTest.java b/tests/robotests/src/com/android/settings/applications/contacts/ContactsStorageSettingsTest.java index 0abacec1d19..e1bf87408bd 100644 --- a/tests/robotests/src/com/android/settings/applications/contacts/ContactsStorageSettingsTest.java +++ b/tests/robotests/src/com/android/settings/applications/contacts/ContactsStorageSettingsTest.java @@ -345,7 +345,8 @@ public class ContactsStorageSettingsTest { SelectorWithWidgetPreference simPreference = accountCategory.findPreference( String.valueOf(SIM_ACCOUNT.hashCode())); assertThat(simPreference.getTitle()).isEqualTo("SIM"); - assertThat(simPreference.getSummary()).isEqualTo("SIM"); + assertThat(simPreference.getSummary()).isEqualTo( + "Contacts may not sync or be available on your other devices"); assertThat(simPreference.getIcon()).isNotNull(); assertThat(simPreference.isChecked()).isTrue(); } From a66f9dc3629586c3543952566f05f1b720dfa50c Mon Sep 17 00:00:00 2001 From: tom hsu Date: Wed, 5 Feb 2025 10:04:28 +0000 Subject: [PATCH 15/23] Fix crash due to over limited length. Flag: EXEMPT bug fix Bug: b/388404115 Test: Manual test Change-Id: I5c72c3c9ca9cb2e4058959abc5a0b4fdd60907bf --- res/layout/dialog_mobile_network_rename.xml | 2 +- res/values/integers.xml | 2 ++ .../RenameMobileNetworkDialogFragment.java | 14 +++++++------- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/res/layout/dialog_mobile_network_rename.xml b/res/layout/dialog_mobile_network_rename.xml index 87f34bc1828..7a409f7cfd0 100644 --- a/res/layout/dialog_mobile_network_rename.xml +++ b/res/layout/dialog_mobile_network_rename.xml @@ -46,7 +46,7 @@ android:layout_height="wrap_content" android:inputType="text" android:paddingTop="@dimen/sim_label_padding" - android:maxLength="50" + android:maxLength="@integer/sim_label_max_length" android:singleLine="true"/> 3 2147483647 + + 50 diff --git a/src/com/android/settings/network/telephony/RenameMobileNetworkDialogFragment.java b/src/com/android/settings/network/telephony/RenameMobileNetworkDialogFragment.java index ebc5575146e..73f80f688c7 100644 --- a/src/com/android/settings/network/telephony/RenameMobileNetworkDialogFragment.java +++ b/src/com/android/settings/network/telephony/RenameMobileNetworkDialogFragment.java @@ -165,9 +165,9 @@ public class RenameMobileNetworkDialogFragment extends InstrumentedDialogFragmen } final CharSequence displayName = SubscriptionUtil.getUniqueSubscriptionDisplayName( info, getContext()); - mNameView.setText(displayName); if (!TextUtils.isEmpty(displayName)) { - mNameView.setSelection(displayName.length()); + mNameView.setSelection(Math.min(displayName.length(), + getResources().getInteger(R.integer.sim_label_max_length))); } mColorSpinner = view.findViewById(R.id.color_spinner); @@ -176,7 +176,7 @@ public class RenameMobileNetworkDialogFragment extends InstrumentedDialogFragmen mColorSpinner.setAdapter(adapter); mColorSpinner.setSelection(getSimColorIndex(info.getIconTint())); - if(Flags.isDualSimOnboardingEnabled()){ + if (Flags.isDualSimOnboardingEnabled()) { return; } @@ -293,10 +293,10 @@ public class RenameMobileNetworkDialogFragment extends InstrumentedDialogFragmen } /* - * Get the color index from previous color that defined in Android OS - * (frameworks/base/core/res/res/values/arrays.xml). If can't find the color, continue to look - * for it in the new color plattee. If not, give it the first index. - */ + * Get the color index from previous color that defined in Android OS + * (frameworks/base/core/res/res/values/arrays.xml). If can't find the color, continue to look + * for it in the new color plattee. If not, give it the first index. + */ private int getSimColorIndex(int color) { int index = -1; From 4f2e409a3ea575155fa8ba2b8b894c6eca69233e Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Thu, 6 Feb 2025 03:08:44 +0000 Subject: [PATCH 16/23] [Catalyst] Update the permissions NO_IFTTT=Catalyst only Bug: 392691854 Bug: 392691083 Test: devtool Flag: EXEMPT bugfix Change-Id: Id7809ab699745fff4ef040d4a7141498a58e04e8 --- .../settings/datausage/DataSaverMainSwitchPreference.kt | 7 ++----- .../network/AdaptiveConnectivityTogglePreference.kt | 4 +--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/com/android/settings/datausage/DataSaverMainSwitchPreference.kt b/src/com/android/settings/datausage/DataSaverMainSwitchPreference.kt index b46fe99c254..33d756ee811 100644 --- a/src/com/android/settings/datausage/DataSaverMainSwitchPreference.kt +++ b/src/com/android/settings/datausage/DataSaverMainSwitchPreference.kt @@ -16,7 +16,6 @@ package com.android.settings.datausage -import android.Manifest import android.app.settings.SettingsEnums.ACTION_DATA_SAVER_MODE import android.content.Context import com.android.settings.PreferenceActionMetricsProvider @@ -50,11 +49,9 @@ class DataSaverMainSwitchPreference : override fun storage(context: Context) = createDataStore(context) - override fun getReadPermissions(context: Context) = - Permissions.allOf(Manifest.permission.MANAGE_NETWORK_POLICY) + override fun getReadPermissions(context: Context) = Permissions.EMPTY - override fun getWritePermissions(context: Context) = - Permissions.allOf(Manifest.permission.MANAGE_NETWORK_POLICY) + override fun getWritePermissions(context: Context) = Permissions.EMPTY override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) = ReadWritePermit.ALLOW diff --git a/src/com/android/settings/network/AdaptiveConnectivityTogglePreference.kt b/src/com/android/settings/network/AdaptiveConnectivityTogglePreference.kt index ec5061ee0fa..c9ba7141b49 100644 --- a/src/com/android/settings/network/AdaptiveConnectivityTogglePreference.kt +++ b/src/com/android/settings/network/AdaptiveConnectivityTogglePreference.kt @@ -16,7 +16,6 @@ package com.android.settings.network -import android.Manifest import android.content.Context import android.net.wifi.WifiManager import android.provider.Settings.Secure.ADAPTIVE_CONNECTIVITY_ENABLED @@ -39,8 +38,7 @@ class AdaptiveConnectivityTogglePreference : override fun getReadPermissions(context: Context) = SettingsSecureStore.getReadPermissions() - override fun getWritePermissions(context: Context) = - SettingsSecureStore.getWritePermissions() and Manifest.permission.NETWORK_SETTINGS + override fun getWritePermissions(context: Context) = SettingsSecureStore.getWritePermissions() override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) = ReadWritePermit.ALLOW From e7ccbf436e97ab6da611ee6f9b2f07e5fbbed55b Mon Sep 17 00:00:00 2001 From: chelseahao Date: Fri, 24 Jan 2025 13:12:24 +0800 Subject: [PATCH 17/23] Update audio sharing dialog text. Also add a radius to the qr code image. Test: atest Bug: 381775542 Flag: com.android.settingslib.flags.enable_le_audio_sharing Change-Id: I6b06b02f63bc46ec20b7a9e0aa236f2547a5f612 --- .../bluetooth_audio_streams_qr_code.xml | 4 +-- .../bluetooth_audio_streams_qr_code.xml | 6 ++-- .../dialog_custom_body_audio_sharing.xml | 9 +++++ res/values/dimens.xml | 1 + res/values/strings.xml | 4 +++ .../AudioSharingDialogFactory.java | 23 +++++++++--- .../AudioSharingDialogFragment.java | 23 ++++++++---- .../AudioStreamsQrCodeFragment.java | 36 ++++++++++++------- .../AudioSharingDialogFragmentTest.java | 13 +++++++ 9 files changed, 90 insertions(+), 29 deletions(-) diff --git a/res/layout-land/bluetooth_audio_streams_qr_code.xml b/res/layout-land/bluetooth_audio_streams_qr_code.xml index 432d75e69d6..918b3ac60b9 100644 --- a/res/layout-land/bluetooth_audio_streams_qr_code.xml +++ b/res/layout-land/bluetooth_audio_streams_qr_code.xml @@ -45,8 +45,8 @@ diff --git a/res/layout/bluetooth_audio_streams_qr_code.xml b/res/layout/bluetooth_audio_streams_qr_code.xml index ab61f50e836..7c023551ab1 100644 --- a/res/layout/bluetooth_audio_streams_qr_code.xml +++ b/res/layout/bluetooth_audio_streams_qr_code.xml @@ -38,11 +38,11 @@ + android:layout_marginTop="70dp"/> + + 16dp 264dp + 1.8dp 300dp 30dp 16dp diff --git a/res/values/strings.xml b/res/values/strings.xml index 9930c3685c0..4470161da78 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -13873,6 +13873,10 @@ Close Connect another pair of compatible headphones, or share your stream\'s name and password with the other person + + Let others scan this code and listen to your audio\n\nStream name: %1$s\nPassword: %2$s + + or pair another set of compatible headphones Pair another set of compatible headphones, or share your audio stream QR code with the other person diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFactory.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFactory.java index b16bceb7c10..3d46361f983 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFactory.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFactory.java @@ -18,7 +18,7 @@ package com.android.settings.connecteddevice.audiosharing; import android.content.Context; import android.content.DialogInterface; -import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.widget.Button; @@ -162,13 +162,13 @@ public class AudioSharingDialogFactory { /** * Sets the custom image of the dialog custom body. * - * @param bitmap The bitmap to be used for the image. + * @param drawable The drawable to be used for the image. * @return This builder. */ @NonNull - public AudioSharingDialogFactory.DialogBuilder setCustomImage(Bitmap bitmap) { + public AudioSharingDialogFactory.DialogBuilder setCustomImage(Drawable drawable) { ImageView image = mCustomBody.findViewById(R.id.description_image); - image.setImageBitmap(bitmap); + image.setImageDrawable(drawable); image.setVisibility(View.VISIBLE); return this; } @@ -202,6 +202,21 @@ public class AudioSharingDialogFactory { return this; } + /** + * Sets the custom message below image. + * + * @param messageRes Resource ID of the string to be used for the message body. + * @return This builder. + */ + @NonNull + public AudioSharingDialogFactory.DialogBuilder setCustomMessage2( + @StringRes int messageRes) { + TextView subTitle = mCustomBody.findViewById(R.id.description_text_2); + subTitle.setText(messageRes); + subTitle.setVisibility(View.VISIBLE); + return this; + } + /** * Sets the custom device actions of the dialog custom body. * diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java index c121f550687..cf71d5f4685 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java @@ -17,13 +17,13 @@ package com.android.settings.connecteddevice.audiosharing; import static com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment.SHARE_THEN_PAIR_REQUEST_CODE; -import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeFragment.getQrCodeBitmap; +import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeFragment.getQrCodeDrawable; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PAIR_AND_JOIN_SHARING; import android.app.Dialog; import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothLeBroadcastMetadata; -import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.util.Log; import android.util.Pair; @@ -45,6 +45,7 @@ import com.android.settingslib.bluetooth.BluetoothUtils; import com.google.common.collect.Iterables; +import java.nio.charset.StandardCharsets; import java.util.List; public class AudioSharingDialogFragment extends InstrumentedDialogFragment { @@ -159,7 +160,6 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { } if (deviceItems.isEmpty()) { builder.setTitle(R.string.audio_sharing_share_dialog_title) - .setCustomMessage(R.string.audio_sharing_dialog_connect_device_content) .setCustomPositiveButton( R.string.audio_sharing_pair_button_label, v -> { @@ -183,14 +183,23 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { }); BluetoothLeBroadcastMetadata metadata = arguments.getParcelable( BUNDLE_KEY_BROADCAST_METADATA, BluetoothLeBroadcastMetadata.class); - Bitmap qrCodeBitmap = metadata == null ? null : getQrCodeBitmap(metadata, + Drawable qrCodeDrawable = metadata == null ? null : getQrCodeDrawable(metadata, getContext()).orElse(null); - if (qrCodeBitmap != null) { - builder.setCustomImage(qrCodeBitmap) - .setCustomNegativeButton(com.android.settings.R.string.cancel, + if (qrCodeDrawable != null) { + builder.setCustomImage(qrCodeDrawable) + .setCustomMessage( + getString( + R.string.audio_sharing_dialog_qr_code_content, + metadata.getBroadcastName(), + new String( + metadata.getBroadcastCode(), + StandardCharsets.UTF_8))) + .setCustomMessage2(R.string.audio_sharing_dialog_pair_new_device_content) + .setCustomNegativeButton(R.string.audio_streams_dialog_close, v -> onCancelClick()); } else { builder.setCustomImage(R.drawable.audio_sharing_guidance) + .setCustomMessage(R.string.audio_sharing_dialog_connect_device_content) .setCustomNegativeButton( R.string.audio_sharing_qrcode_button_label, v -> { diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java index 656694d1b2b..daa7a2e1c23 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java @@ -19,7 +19,9 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.content.Context; +import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; @@ -30,6 +32,8 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.graphics.drawable.RoundedBitmapDrawable; +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; import com.android.settings.R; import com.android.settings.bluetooth.Utils; @@ -70,15 +74,16 @@ public class AudioStreamsQrCodeFragment extends InstrumentedFragment { if (broadcastMetadata == null) { return; } - Bitmap bm = getQrCodeBitmap(broadcastMetadata, getActivity()).orElse(null); - if (bm == null) { + Drawable drawable = getQrCodeDrawable(broadcastMetadata, getActivity()).orElse( + null); + if (drawable == null) { return; } ThreadUtils.postOnMainThread( () -> { ((ImageView) view.requireViewById(R.id.qrcode_view)) - .setImageBitmap(bm); + .setImageDrawable(drawable); if (broadcastMetadata.getBroadcastCode() != null) { String password = new String( @@ -101,28 +106,33 @@ public class AudioStreamsQrCodeFragment extends InstrumentedFragment { }); } - /** Gets an optional bitmap from metadata. */ - public static Optional getQrCodeBitmap(@Nullable BluetoothLeBroadcastMetadata metadata, + /** Gets an optional drawable from metadata. */ + public static Optional getQrCodeDrawable( + @Nullable BluetoothLeBroadcastMetadata metadata, Context context) { if (metadata == null) { - Log.d(TAG, "getQrCodeBitmap: broadcastMetadata is empty!"); + Log.d(TAG, "getQrCodeDrawable: broadcastMetadata is empty!"); return Optional.empty(); } String metadataStr = BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata); if (metadataStr.isEmpty()) { - Log.d(TAG, "getQrCodeBitmap: metadataStr is empty!"); + Log.d(TAG, "getQrCodeDrawable: metadataStr is empty!"); return Optional.empty(); } - Log.d(TAG, "getQrCodeBitmap: metadata : " + metadata); + Log.d(TAG, "getQrCodeDrawable: metadata : " + metadata); try { - int qrcodeSize = - context.getResources().getDimensionPixelSize(R.dimen.audio_streams_qrcode_size); - Bitmap bitmap = QrCodeGenerator.encodeQrCode(metadataStr, qrcodeSize); - return Optional.of(bitmap); + Resources resources = context.getResources(); + int qrcodeSize = resources.getDimensionPixelSize(R.dimen.audio_streams_qrcode_size); + int margin = resources.getDimensionPixelSize(R.dimen.audio_streams_qrcode_margin); + Bitmap bitmap = QrCodeGenerator.encodeQrCode(metadataStr, qrcodeSize, margin); + RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(resources, bitmap); + drawable.setCornerRadius(resources.getDimensionPixelSize( + R.dimen.audio_streams_qrcode_preview_radius)); + return Optional.of(drawable); } catch (WriterException e) { Log.d( TAG, - "getQrCodeBitmap: broadcastMetadata " + "getQrCodeDrawable: broadcastMetadata " + metadata + " qrCode generation exception " + e); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java index b4c0a2047be..145a5c7a549 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java @@ -60,6 +60,7 @@ import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.androidx.fragment.FragmentController; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; @@ -263,6 +264,18 @@ public class AudioSharingDialogFragmentTest { assertThat(dialog).isNotNull(); ImageView image = dialog.findViewById(R.id.description_image); assertThat(image).isNotNull(); + TextView text = dialog.findViewById(R.id.description_text); + assertThat(text).isNotNull(); + assertThat(METADATA).isNotNull(); + assertThat(text.getText().toString()).isEqualTo( + mParent.getString(R.string.audio_sharing_dialog_qr_code_content, + METADATA.getBroadcastName(), new String( + METADATA.getBroadcastCode(), + StandardCharsets.UTF_8))); + TextView textBottom = dialog.findViewById(R.id.description_text_2); + assertThat(textBottom).isNotNull(); + assertThat(textBottom.getText().toString()).isEqualTo( + mParent.getString(R.string.audio_sharing_dialog_pair_new_device_content)); Button cancelBtn = dialog.findViewById(R.id.negative_btn); assertThat(cancelBtn).isNotNull(); cancelBtn.performClick(); From 650f865def74de95fd675fe0760fcb3677f49f4c Mon Sep 17 00:00:00 2001 From: MiltonWu Date: Wed, 5 Feb 2025 21:21:56 +0800 Subject: [PATCH 18/23] Support disable extend Fingerprint Settings 1. Disable extend fingerprint settings when all fingerprints removed 2. Refine FingerprintUnlockCategoryController logic to better determine available status Bug: 394550555 Flag: EXEMPT support interface, flag only used in impl part Test: atest FingerprintSettingsFragmentTest Change-Id: I8904e5f6d8f475ad63540eaef913407d7c0cf4ad --- .../fingerprint/FingerprintSettings.java | 20 ++++++++++++++++++ .../FingerprintUnlockCategoryController.java | 21 ++++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java index 80c6112ceab..9531f73ebd0 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java @@ -332,6 +332,8 @@ public class FingerprintSettings extends SubSettings { private boolean mIsEnrolling; /** SaveInstance key if we are waiting activity result from a extension preference */ @NonNull private String mLaunchedExtPrefKey = ""; + /** key list for changing visibility */ + @NonNull private final ArrayList mExtPrefKeys = new ArrayList<>(); private long mChallenge; @@ -474,6 +476,7 @@ public class FingerprintSettings extends SubSettings { if (preference instanceof PrimarySwitchIntentPreference) { preference.setOnPreferenceClickListener(this::onExtIntentPreferenceClick); } + mExtPrefKeys.add(preference.getKey()); mFingerprintUnlockCategory.addPreference(preference); } } @@ -826,8 +829,17 @@ public class FingerprintSettings extends SubSettings { updateAddPreference(); } + /** + * Lambda function for setCategoryHasChildrenSupplier + */ + private boolean fingerprintUnlockCategoryHasChild() { + return mFingerprintUnlockCategory.getPreferenceCount() > 0; + } + private void addFingerprintUnlockCategory() { mFingerprintUnlockCategory = findPreference(KEY_FINGERPRINT_UNLOCK_CATEGORY); + mFingerprintUnlockCategoryPreferenceController.setCategoryHasChildrenSupplier( + this::fingerprintUnlockCategoryHasChild); if (isSfps()) { // For both SFPS "screen on to auth" and "rest to unlock" final Preference restToUnlockPreference = FeatureFactory.getFeatureFactory() @@ -867,6 +879,14 @@ public class FingerprintSettings extends SubSettings { mScreenOffUnlockUdfpsPreferenceController.getAvailabilityStatus(); updatePreferenceVisibility(status, mScreenOffUnlockUdfpsPreference); } + if (!mExtPrefKeys.isEmpty()) { + for (String key: mExtPrefKeys) { + Preference preference = mFingerprintUnlockCategory.findPreference(key); + if (preference != null) { + updatePreferenceVisibility(categoryStatus, preference); + } + } + } } private void updatePreferenceVisibility(int availabilityStatus, Preference preference) { diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintUnlockCategoryController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintUnlockCategoryController.java index c949d3da4d8..2febce12cf5 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintUnlockCategoryController.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintUnlockCategoryController.java @@ -16,8 +16,7 @@ package com.android.settings.biometrics.fingerprint; -import static android.hardware.biometrics.Flags.screenOffUnlockUdfps; - +import android.annotation.Nullable; import android.content.Context; import android.hardware.fingerprint.FingerprintManager; @@ -25,6 +24,8 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.settings.Utils; import com.android.settings.core.BasePreferenceController; +import java.util.function.Supplier; + /** * Preference controller that controls the fingerprint unlock features to be shown / be hidden. */ @@ -34,17 +35,31 @@ public class FingerprintUnlockCategoryController extends BasePreferenceControlle private int mUserId; @VisibleForTesting protected FingerprintManager mFingerprintManager; + @Nullable + private Supplier mCategoryHasChildSupplier = null; public FingerprintUnlockCategoryController(Context context, String key) { super(context, key); mFingerprintManager = Utils.getFingerprintManagerOrNull(context); } + public void setCategoryHasChildrenSupplier( + @Nullable Supplier categoryHasChildSupplier + ) { + mCategoryHasChildSupplier = categoryHasChildSupplier; + } + @Override public int getAvailabilityStatus() { + Supplier categoryHasChildSupplier = mCategoryHasChildSupplier; + boolean hasChild = false; + if (categoryHasChildSupplier != null) { + hasChild = categoryHasChildSupplier.get(); + } + if (mFingerprintManager != null && mFingerprintManager.isHardwareDetected() - && (mFingerprintManager.isPowerbuttonFps() || screenOffUnlockUdfps())) { + && hasChild) { return mFingerprintManager.hasEnrolledTemplates(getUserId()) ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; } else { From d9b78ebc973909dd11006da61f41819289132007 Mon Sep 17 00:00:00 2001 From: tom hsu Date: Wed, 5 Feb 2025 23:54:17 -0800 Subject: [PATCH 19/23] Fix crash due to over limited length. Flag: EXEMPT bug fix Fix: b/388404115 Test: Manual test (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:a66f9dc3629586c3543952566f05f1b720dfa50c) Merged-In: I5c72c3c9ca9cb2e4058959abc5a0b4fdd60907bf Change-Id: I5c72c3c9ca9cb2e4058959abc5a0b4fdd60907bf --- res/layout/dialog_mobile_network_rename.xml | 2 +- res/values/integers.xml | 2 ++ .../RenameMobileNetworkDialogFragment.java | 14 +++++++------- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/res/layout/dialog_mobile_network_rename.xml b/res/layout/dialog_mobile_network_rename.xml index 2c583d3bc9e..41ac001eed4 100644 --- a/res/layout/dialog_mobile_network_rename.xml +++ b/res/layout/dialog_mobile_network_rename.xml @@ -45,7 +45,7 @@ android:layout_height="wrap_content" android:inputType="text" android:paddingTop="@dimen/sim_label_padding" - android:maxLength="50" + android:maxLength="@integer/sim_label_max_length" android:singleLine="true"/> 3 2147483647 + + 50 diff --git a/src/com/android/settings/network/telephony/RenameMobileNetworkDialogFragment.java b/src/com/android/settings/network/telephony/RenameMobileNetworkDialogFragment.java index ebc5575146e..73f80f688c7 100644 --- a/src/com/android/settings/network/telephony/RenameMobileNetworkDialogFragment.java +++ b/src/com/android/settings/network/telephony/RenameMobileNetworkDialogFragment.java @@ -165,9 +165,9 @@ public class RenameMobileNetworkDialogFragment extends InstrumentedDialogFragmen } final CharSequence displayName = SubscriptionUtil.getUniqueSubscriptionDisplayName( info, getContext()); - mNameView.setText(displayName); if (!TextUtils.isEmpty(displayName)) { - mNameView.setSelection(displayName.length()); + mNameView.setSelection(Math.min(displayName.length(), + getResources().getInteger(R.integer.sim_label_max_length))); } mColorSpinner = view.findViewById(R.id.color_spinner); @@ -176,7 +176,7 @@ public class RenameMobileNetworkDialogFragment extends InstrumentedDialogFragmen mColorSpinner.setAdapter(adapter); mColorSpinner.setSelection(getSimColorIndex(info.getIconTint())); - if(Flags.isDualSimOnboardingEnabled()){ + if (Flags.isDualSimOnboardingEnabled()) { return; } @@ -293,10 +293,10 @@ public class RenameMobileNetworkDialogFragment extends InstrumentedDialogFragmen } /* - * Get the color index from previous color that defined in Android OS - * (frameworks/base/core/res/res/values/arrays.xml). If can't find the color, continue to look - * for it in the new color plattee. If not, give it the first index. - */ + * Get the color index from previous color that defined in Android OS + * (frameworks/base/core/res/res/values/arrays.xml). If can't find the color, continue to look + * for it in the new color plattee. If not, give it the first index. + */ private int getSimColorIndex(int color) { int index = -1; From 45ac3f280df1ad822ea2975c22de1ca9081a42a7 Mon Sep 17 00:00:00 2001 From: mxyyiyi Date: Thu, 6 Feb 2025 12:17:21 +0800 Subject: [PATCH 20/23] [Expressive Battery] Update screen on time text preference style. For expressive style: - Update the enlarge font size - Remove the background Bug: 349652542 Test: visual Flag: com.android.settingslib.widget.theme.flags.is_expressive_design_enabled Change-Id: If85d2903eec6d1f7c3b58e71daef2409325ac83d --- res/layout/preference_text_view.xml | 4 ++-- .../fuelgauge/batteryusage/ScreenOnTimeController.java | 4 +++- .../settings/fuelgauge/batteryusage/TextViewPreference.java | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/res/layout/preference_text_view.xml b/res/layout/preference_text_view.xml index 3d0b2a1d9c7..9d8fafddf36 100644 --- a/res/layout/preference_text_view.xml +++ b/res/layout/preference_text_view.xml @@ -18,8 +18,8 @@ android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="24dp" - android:layout_marginTop="8dp" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" android:textAlignment="viewStart" android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="?android:attr/textColorPrimary" /> \ No newline at end of file diff --git a/src/com/android/settings/fuelgauge/batteryusage/ScreenOnTimeController.java b/src/com/android/settings/fuelgauge/batteryusage/ScreenOnTimeController.java index 379cef3a22a..9c56c178392 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/ScreenOnTimeController.java +++ b/src/com/android/settings/fuelgauge/batteryusage/ScreenOnTimeController.java @@ -30,6 +30,7 @@ import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.BasePreferenceController; import com.android.settings.fuelgauge.BatteryUtils; +import com.android.settingslib.widget.SettingsThemeHelper; import java.util.Locale; import java.util.regex.Matcher; @@ -119,10 +120,11 @@ public class ScreenOnTimeController extends BasePreferenceController { } final SpannableString spannableText = new SpannableString(text); + final int enlargeFontSizeDp = SettingsThemeHelper.isExpressiveTheme(context) ? 64 : 36; final Matcher matcher = NUMBER_PATTERN.matcher(text); while (matcher.find()) { spannableText.setSpan( - new AbsoluteSizeSpan(36, true /* dip */), + new AbsoluteSizeSpan(enlargeFontSizeDp, true /* dip */), matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); diff --git a/src/com/android/settings/fuelgauge/batteryusage/TextViewPreference.java b/src/com/android/settings/fuelgauge/batteryusage/TextViewPreference.java index b20ef39f160..c9047385e33 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/TextViewPreference.java +++ b/src/com/android/settings/fuelgauge/batteryusage/TextViewPreference.java @@ -25,9 +25,10 @@ import androidx.preference.PreferenceViewHolder; import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; +import com.android.settingslib.widget.GroupSectionDividerMixin; /** A preference for a single text view. */ -public class TextViewPreference extends Preference { +public class TextViewPreference extends Preference implements GroupSectionDividerMixin { private static final String TAG = "TextViewPreference"; @VisibleForTesting CharSequence mText; From 68bb5a04c90adac7a1cf757bd2eb965e60080c2d Mon Sep 17 00:00:00 2001 From: Jacky Wang Date: Thu, 6 Feb 2025 12:22:51 +0800 Subject: [PATCH 21/23] [Catalyst] Enforce WRITE_SYSTEM_PREFERENCES permission Bug: 374115149 Bug: 394744563 Flag: com.android.settingslib.flags.settings_catalyst Test: manual Change-Id: I63b84aea001033e81b1d811a2de983d8d107015c --- AndroidManifest.xml | 6 +++--- src/com/android/settings/SettingsService.kt | 14 ++++++-------- .../android/settings/service/PreferenceService.kt | 10 +++++++++- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 177e750c892..de54b83a220 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -5515,12 +5515,12 @@ android:exported="true" android:permission="android.permission.BLUETOOTH_PRIVILEGED" /> - - + + android:featureFlag="com.android.settings.flags.catalyst_service" + android:permission="android.permission.READ_SYSTEM_PREFERENCES"> diff --git a/src/com/android/settings/SettingsService.kt b/src/com/android/settings/SettingsService.kt index b7c6220bd23..8eaa1e973e3 100644 --- a/src/com/android/settings/SettingsService.kt +++ b/src/com/android/settings/SettingsService.kt @@ -16,21 +16,19 @@ package com.android.settings -import android.content.Intent -import com.android.settings.flags.Flags +import android.Manifest.permission.WRITE_SYSTEM_PREFERENCES +import android.app.AppOpsManager.OP_WRITE_SYSTEM_PREFERENCES import com.android.settings.metrics.SettingsRemoteOpMetricsLogger import com.android.settingslib.ipc.ApiPermissionChecker +import com.android.settingslib.ipc.AppOpApiPermissionChecker import com.android.settingslib.service.PreferenceService /** Service to expose settings APIs. */ class SettingsService : PreferenceService( graphPermissionChecker = ApiPermissionChecker.alwaysAllow(), - setterPermissionChecker = ApiPermissionChecker.alwaysAllow(), + setterPermissionChecker = + AppOpApiPermissionChecker(OP_WRITE_SYSTEM_PREFERENCES, WRITE_SYSTEM_PREFERENCES), getterPermissionChecker = ApiPermissionChecker.alwaysAllow(), metricsLogger = SettingsRemoteOpMetricsLogger(), - ) { - - override fun onBind(intent: Intent) = - if (Flags.catalystService()) super.onBind(intent) else null -} + ) diff --git a/src/com/android/settings/service/PreferenceService.kt b/src/com/android/settings/service/PreferenceService.kt index 9843847304e..6710f927515 100644 --- a/src/com/android/settings/service/PreferenceService.kt +++ b/src/com/android/settings/service/PreferenceService.kt @@ -16,6 +16,8 @@ package com.android.settings.service +import android.Manifest.permission.WRITE_SYSTEM_PREFERENCES +import android.app.AppOpsManager.OP_WRITE_SYSTEM_PREFERENCES import android.os.Binder import android.os.OutcomeReceiver import android.service.settings.preferences.GetValueRequest @@ -32,6 +34,7 @@ import com.android.settingslib.graph.PreferenceGetterApiHandler import com.android.settingslib.graph.PreferenceGetterFlags import com.android.settingslib.graph.PreferenceSetterApiHandler import com.android.settingslib.ipc.ApiPermissionChecker +import com.android.settingslib.ipc.AppOpApiPermissionChecker import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -47,10 +50,15 @@ class PreferenceService : SettingsPreferenceService() { init { val metricsLogger = SettingsRemoteOpMetricsLogger() + // PreferenceService specifies READ_SYSTEM_PREFERENCES permission in AndroidManifest.xml getApiHandler = PreferenceGetterApiHandler(1, ApiPermissionChecker.alwaysAllow(), metricsLogger) setApiHandler = - PreferenceSetterApiHandler(2, ApiPermissionChecker.alwaysAllow(), metricsLogger) + PreferenceSetterApiHandler( + 2, + AppOpApiPermissionChecker(OP_WRITE_SYSTEM_PREFERENCES, WRITE_SYSTEM_PREFERENCES), + metricsLogger, + ) graphApi = GetPreferenceGraphApiHandler(3, ApiPermissionChecker.alwaysAllow(), metricsLogger) } From 538cc35aed8cc22879e7673d6c63d3eddf739c2f Mon Sep 17 00:00:00 2001 From: shaoweishen Date: Tue, 21 Jan 2025 08:10:13 +0000 Subject: [PATCH 22/23] [Physical Keyboard] Fix bugs for input keys dialog set content description on custom radio button and seekbar. fix typo Bug: 374229004 Bug: 374229189 Bug: 374229597 Bug: 389973787 Flag: com.android.settings.keyboard.keyboard_and_touchpad_a11y_new_page_enabled Test: atest packages/apps/Settings/tests/robotests/src/com/android/settings/inputmethod/ Change-Id: I3df4b1558c2bad73d9411a0160725d7a35590cc3 --- ...ialog_keyboard_a11y_input_setting_keys.xml | 1 + res/values/strings.xml | 21 ++++++++++++------- ...AccessibilityBounceKeysDialogFragment.java | 2 ++ ...yboardAccessibilityKeysDialogFragment.java | 19 +++++++++++++---- ...rdAccessibilitySlowKeysDialogFragment.java | 2 ++ 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/res/layout/dialog_keyboard_a11y_input_setting_keys.xml b/res/layout/dialog_keyboard_a11y_input_setting_keys.xml index d826fee96fd..01f247cdf66 100644 --- a/res/layout/dialog_keyboard_a11y_input_setting_keys.xml +++ b/res/layout/dialog_keyboard_a11y_input_setting_keys.xml @@ -88,6 +88,7 @@ android:layout_width="wrap_content" android:layout_height="48dp" android:layout_gravity="center_vertical" + android:contentDescription="@string/input_setting_keys_custom_title" android:background="@null"/> Custom custom value + + Bounce key threshold time + + Slow key threshold time + Slow keys @@ -4748,17 +4753,17 @@ Mouse keys for %s - Use the \“%s\” keys to move the mouse pointer + Use the \"%s\" keys to move the mouse pointer - Use the \“%s\” key to click the primary mouse button + Use the \"%s\" key to click the primary mouse button - Use the \“%s\” key to press & hold the primary mouse button + Use the \"%s\" key to press & hold the primary mouse button - Use the \“%s\” key to release the primary mouse button + Use the \"%s\" key to release the primary mouse button - Use the \“%1$s\” key to toggle scroll mode. This will make the \“%2$s\” keys scroll the view top, down, left or right + Use the \"%1$s\" key to toggle scroll mode. This will make the \"%2$s\" keys scroll the view top, down, left or right - Use the \“%s\” key to click the secondary mouse button + Use the \"%s\" key to click the secondary mouse button View keyboard shortcuts @@ -4778,7 +4783,7 @@ Mouse - Pointer speed, swap buttons, button customisation + Pointer speed, swap buttons, button customization Pointer speed, gestures @@ -4803,7 +4808,7 @@ Cursor speed - Customise 3-finger tap + Customize 3-finger tap Touchpad acceleration diff --git a/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysDialogFragment.java b/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysDialogFragment.java index 2dc90de84b1..5e5bd0661ed 100644 --- a/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysDialogFragment.java +++ b/src/com/android/settings/inputmethod/KeyboardAccessibilityBounceKeysDialogFragment.java @@ -32,6 +32,8 @@ public class KeyboardAccessibilityBounceKeysDialogFragment extends Bundle bundle = new Bundle(); bundle.putInt(EXTRA_TITLE_RES, R.string.bounce_keys_dialog_title); bundle.putInt(EXTRA_SUBTITLE_RES, R.string.bounce_keys_dialog_subtitle); + bundle.putInt(EXTRA_SEEKBAR_CONTENT_DESCRIPTION, + R.string.input_setting_bounce_keys_seekbar_desc); result.setArguments(bundle); return result; } diff --git a/src/com/android/settings/inputmethod/KeyboardAccessibilityKeysDialogFragment.java b/src/com/android/settings/inputmethod/KeyboardAccessibilityKeysDialogFragment.java index bba47a72fd7..252ce54768a 100644 --- a/src/com/android/settings/inputmethod/KeyboardAccessibilityKeysDialogFragment.java +++ b/src/com/android/settings/inputmethod/KeyboardAccessibilityKeysDialogFragment.java @@ -38,6 +38,7 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import org.jspecify.annotations.Nullable; +import java.util.Locale; import java.util.concurrent.TimeUnit; public abstract class KeyboardAccessibilityKeysDialogFragment extends DialogFragment { @@ -45,6 +46,8 @@ public abstract class KeyboardAccessibilityKeysDialogFragment extends DialogFrag private static final long MILLISECOND_IN_SECONDS = TimeUnit.SECONDS.toMillis(1); protected static final String EXTRA_TITLE_RES = "extra_title_res"; protected static final String EXTRA_SUBTITLE_RES = "extra_subtitle_res"; + protected static final String EXTRA_SEEKBAR_CONTENT_DESCRIPTION = + "extra_seekbar_content_description_res"; protected final MetricsFeatureProvider mMetricsFeatureProvider; @@ -67,6 +70,7 @@ public abstract class KeyboardAccessibilityKeysDialogFragment extends DialogFrag super.onCreateDialog(savedInstanceState); int titleRes = getArguments().getInt(EXTRA_TITLE_RES); int subtitleRes = getArguments().getInt(EXTRA_SUBTITLE_RES); + int seekbarContentDescriptionRes = getArguments().getInt(EXTRA_SEEKBAR_CONTENT_DESCRIPTION); Activity activity = getActivity(); View dialoglayout = @@ -121,6 +125,10 @@ public abstract class KeyboardAccessibilityKeysDialogFragment extends DialogFrag titleTextView.setText(titleRes); subTitleTextView.setText(subtitleRes); + if (seekbarContentDescriptionRes != 0) { + customProgressBar.setContentDescription( + getContext().getString(seekbarContentDescriptionRes)); + } customProgressBar.incrementProgressBy(CUSTOM_PROGRESS_INTERVAL); customProgressBar.setProgress(1); View customValueView = accessibilityKeyDialog.findViewById( @@ -141,7 +149,9 @@ public abstract class KeyboardAccessibilityKeysDialogFragment extends DialogFrag customProgressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - customValueTextView.setText(progressToThresholdInSecond(progress)); + String threshold = progressToThresholdInSecond(progress); + customValueTextView.setText(threshold); + customProgressBar.setContentDescription(threshold); } @Override @@ -162,9 +172,10 @@ public abstract class KeyboardAccessibilityKeysDialogFragment extends DialogFrag return accessibilityKeyDialog; } - private static String progressToThresholdInSecond(int progress) { - return String.valueOf((double) progress * CUSTOM_PROGRESS_INTERVAL - / MILLISECOND_IN_SECONDS); + private String progressToThresholdInSecond(int progress) { + return (double) progress * CUSTOM_PROGRESS_INTERVAL + / MILLISECOND_IN_SECONDS + " " + TimeUnit.SECONDS.name().toLowerCase( + Locale.getDefault()); } private void initStateBasedOnThreshold(RadioGroup cannedValueRadioGroup, diff --git a/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysDialogFragment.java b/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysDialogFragment.java index fa3b6858cd5..e411d7ab5b6 100644 --- a/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysDialogFragment.java +++ b/src/com/android/settings/inputmethod/KeyboardAccessibilitySlowKeysDialogFragment.java @@ -32,6 +32,8 @@ public class KeyboardAccessibilitySlowKeysDialogFragment extends Bundle bundle = new Bundle(); bundle.putInt(EXTRA_TITLE_RES, R.string.slow_keys); bundle.putInt(EXTRA_SUBTITLE_RES, R.string.slow_keys_summary); + bundle.putInt(EXTRA_SEEKBAR_CONTENT_DESCRIPTION, + R.string.input_setting_slow_keys_seekbar_desc); result.setArguments(bundle); return result; } From a9e22be8a57bccb522bc1f4f2a9f704f45c7b4dc Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Thu, 6 Feb 2025 14:44:39 +0000 Subject: [PATCH 23/23] Temp disable test that contains custom shadow within it We need proper support or Exception throw for this scenario Test: atest SettingsRoboTests Bug: 394813533 Change-Id: I8b552f7b2bff166643bdaa492dceff23679116ff --- .../src/com/android/settings/wifi/slice/WifiSliceTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java b/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java index c9cf5a2a852..c6bdebdcc14 100644 --- a/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java +++ b/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java @@ -56,6 +56,7 @@ import com.android.wifitrackerlib.WifiEntry; import com.android.wifitrackerlib.WifiEntry.ConnectedState; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -72,6 +73,7 @@ import org.robolectric.shadows.ShadowBinder; import java.util.ArrayList; import java.util.List; +@Ignore("b/394813533") @Deprecated(forRemoval = true) @RunWith(RobolectricTestRunner.class) @Config(shadows = {