Note: This is special-cased to the structure of these fragments: + * one column, N rows (one per preference, including category titles and header+footer + * preferences), <=N 'important' rows (image prefs without content descriptions). This + * is not intended for use with generic {@link RecyclerView}s. + */ + public static RecyclerView addCollectionInfoToAccessibilityDelegate(RecyclerView recyclerView) { + if (!Flags.toggleFeatureFragmentCollectionInfo()) { + return recyclerView; + } + final RecyclerViewAccessibilityDelegate delegate = + recyclerView.getCompatAccessibilityDelegate(); + if (delegate == null) { + // No delegate, so do nothing. This should not occur for real RecyclerViews. + return recyclerView; + } + recyclerView.setAccessibilityDelegateCompat( + new RvAccessibilityDelegateWrapper(recyclerView, delegate) { + @Override + public void onInitializeAccessibilityNodeInfo(@NonNull View host, + @NonNull AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + if (!(recyclerView.getAdapter() + instanceof final PreferenceGroupAdapter preferenceGroupAdapter)) { + return; + } + final int visibleCount = preferenceGroupAdapter.getItemCount(); + int importantCount = 0; + for (int i = 0; i < visibleCount; i++) { + if (isPreferenceImportantToA11y(preferenceGroupAdapter.getItem(i))) { + importantCount++; + } + } + info.unwrap().setCollectionInfo( + new AccessibilityNodeInfo.CollectionInfo( + /*rowCount=*/visibleCount, + /*columnCount=*/1, + /*hierarchical=*/false, + AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_SINGLE, + /*itemCount=*/visibleCount, + /*importantForAccessibilityItemCount=*/importantCount)); + } + }); + return recyclerView; + } + + /** + * Returns whether the preference will be marked as important to accessibility for the sake + * of calculating {@link AccessibilityNodeInfo.CollectionInfo} counts. + * + *
The accessibility service itself knows this information for an individual preference
+ * on the screen, but it expects the preference's {@link RecyclerView} to also provide the
+ * same information for its entire set of adapter items.
+ */
+ @VisibleForTesting
+ static boolean isPreferenceImportantToA11y(Preference pref) {
+ if ((pref instanceof IllustrationPreference illustrationPref
+ && TextUtils.isEmpty(illustrationPref.getContentDescription()))
+ || pref instanceof PaletteListPreference) {
+ // Illustration preference that is visible but unannounced by accessibility services.
+ return false;
+ }
+ // All other preferences from the PreferenceGroupAdapter are important.
+ return true;
+ }
+
+ /**
+ * Wrapper around a {@link RecyclerViewAccessibilityDelegate} that allows customizing
+ * a subset of methods and while also deferring to the original. All overridden methods
+ * in instantiations of this class should call {@code super}.
+ */
+ private static class RvAccessibilityDelegateWrapper extends RecyclerViewAccessibilityDelegate {
+ private final RecyclerViewAccessibilityDelegate mOriginal;
+
+ RvAccessibilityDelegateWrapper(RecyclerView recyclerView,
+ RecyclerViewAccessibilityDelegate original) {
+ super(recyclerView);
+ mOriginal = original;
+ }
+
+ @Override
+ public boolean performAccessibilityAction(@NonNull View host, int action, Bundle args) {
+ return mOriginal.performAccessibilityAction(host, action, args);
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(@NonNull View host,
+ @NonNull AccessibilityNodeInfoCompat info) {
+ mOriginal.onInitializeAccessibilityNodeInfo(host, info);
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(@NonNull View host,
+ @NonNull AccessibilityEvent event) {
+ mOriginal.onInitializeAccessibilityEvent(host, event);
+ }
+
+ @Override
+ @NonNull
+ public AccessibilityDelegateCompat getItemDelegate() {
+ if (mOriginal == null) {
+ // Needed for super constructor which calls getItemDelegate before mOriginal is
+ // defined, but unused by actual clients of this RecyclerViewAccessibilityDelegate
+ // which invoke getItemDelegate() after the constructor finishes.
+ return new ItemDelegate(this);
+ }
+ return mOriginal.getItemDelegate();
+ }
+ }
+}
diff --git a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java
index 0ac29bc6ba5..9c61e5c3305 100644
--- a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java
+++ b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java
@@ -56,6 +56,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
+import androidx.recyclerview.widget.RecyclerView;
import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
@@ -871,4 +872,12 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
return PreferredShortcuts.retrieveUserShortcutType(
getPrefContext(), mComponentName.flattenToString(), getDefaultShortcutTypes());
}
+
+ @Override
+ public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent,
+ Bundle savedInstanceState) {
+ RecyclerView recyclerView =
+ super.onCreateRecyclerView(inflater, parent, savedInstanceState);
+ return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(recyclerView);
+ }
}
diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java
index 97405d24e9f..52d75c19ed4 100644
--- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java
+++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java
@@ -79,7 +79,8 @@ public class ToggleScreenMagnificationPreferenceFragmentForSetupWizard
Bundle savedInstanceState) {
if (parent instanceof GlifPreferenceLayout) {
final GlifPreferenceLayout layout = (GlifPreferenceLayout) parent;
- return layout.onCreateRecyclerView(inflater, parent, savedInstanceState);
+ return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(
+ layout.onCreateRecyclerView(inflater, parent, savedInstanceState));
}
return super.onCreateRecyclerView(inflater, parent, savedInstanceState);
}
diff --git a/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java
index 4309b1d9038..10813a7e262 100644
--- a/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java
+++ b/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java
@@ -68,7 +68,8 @@ public class ToggleScreenReaderPreferenceFragmentForSetupWizard
Bundle savedInstanceState) {
if (parent instanceof GlifPreferenceLayout) {
final GlifPreferenceLayout layout = (GlifPreferenceLayout) parent;
- return layout.onCreateRecyclerView(inflater, parent, savedInstanceState);
+ return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(
+ layout.onCreateRecyclerView(inflater, parent, savedInstanceState));
}
return super.onCreateRecyclerView(inflater, parent, savedInstanceState);
}
diff --git a/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java
index 8d26785d021..10796b5d218 100644
--- a/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java
+++ b/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java
@@ -68,7 +68,8 @@ public class ToggleSelectToSpeakPreferenceFragmentForSetupWizard
Bundle savedInstanceState) {
if (parent instanceof GlifPreferenceLayout) {
final GlifPreferenceLayout layout = (GlifPreferenceLayout) parent;
- return layout.onCreateRecyclerView(inflater, parent, savedInstanceState);
+ return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(
+ layout.onCreateRecyclerView(inflater, parent, savedInstanceState));
}
return super.onCreateRecyclerView(inflater, parent, savedInstanceState);
}
diff --git a/src/com/android/settings/bluetooth/BluetoothKeyMissingDialog.java b/src/com/android/settings/bluetooth/BluetoothKeyMissingDialog.java
new file mode 100644
index 00000000000..46975f77726
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothKeyMissingDialog.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 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.bluetooth;
+
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+
+/** A dialog to ask the user to forget a bluetooth device when the key is missing. */
+public class BluetoothKeyMissingDialog extends FragmentActivity {
+ public static final String FRAGMENT_TAG = "BtKeyMissingFrg";
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+ Intent intent = getIntent();
+ BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ if (device == null) {
+ finish();
+ return;
+ }
+ BluetoothKeyMissingDialogFragment fragment = new BluetoothKeyMissingDialogFragment(device);
+ fragment.show(getSupportFragmentManager(), FRAGMENT_TAG);
+ closeSystemDialogs();
+ }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java b/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java
new file mode 100644
index 00000000000..a8e3aae175a
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 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.bluetooth;
+
+import android.app.Dialog;
+import android.app.settings.SettingsEnums;
+import android.bluetooth.BluetoothDevice;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+/**
+ * A dialogFragment used by {@link BluetoothKeyMissingDialog} to create a dialog for the
+ * bluetooth device.
+ */
+public class BluetoothKeyMissingDialogFragment extends InstrumentedDialogFragment
+ implements OnClickListener {
+
+ private static final String TAG = "BTKeyMissingDialogFragment";
+
+ private BluetoothDevice mBluetoothDevice;
+
+ public BluetoothKeyMissingDialogFragment(@NonNull BluetoothDevice bluetoothDevice) {
+ mBluetoothDevice = bluetoothDevice;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ View view = getActivity().getLayoutInflater().inflate(R.layout.bluetooth_key_missing, null);
+ TextView keyMissingTitle = view.findViewById(R.id.bluetooth_key_missing_title);
+ keyMissingTitle.setText(
+ getString(R.string.bluetooth_key_missing_title, mBluetoothDevice.getName()));
+ builder.setView(view);
+ builder.setPositiveButton(getString(R.string.bluetooth_key_missing_forget), this);
+ builder.setNegativeButton(getString(R.string.bluetooth_key_missing_cancel), this);
+ AlertDialog dialog = builder.create();
+ dialog.setCanceledOnTouchOutside(false);
+ return dialog;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (!getActivity().isFinishing()) {
+ getActivity().finish();
+ }
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ Log.i(
+ TAG,
+ "Positive button clicked, remove bond for "
+ + mBluetoothDevice.getAnonymizedAddress());
+ mBluetoothDevice.removeBond();
+ } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+ Log.i(TAG, "Negative button clicked for " + mBluetoothDevice.getAnonymizedAddress());
+ }
+ if (!getActivity().isFinishing()) {
+ getActivity().finish();
+ }
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return SettingsEnums.BLUETOOTH_KEY_MISSING_DIALOG_FRAGMENT;
+ }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java b/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java
new file mode 100644
index 00000000000..d7a5343d694
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2024 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.bluetooth;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.PowerManager;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.core.app.NotificationCompat;
+
+import com.android.settings.R;
+import com.android.settings.flags.Flags;
+
+/**
+ * BluetoothKeyMissingReceiver is a receiver for Bluetooth key missing error when reconnecting to a
+ * bonded bluetooth device.
+ */
+public final class BluetoothKeyMissingReceiver extends BroadcastReceiver {
+ private static final String TAG = "BtKeyMissingReceiver";
+ private static final String CHANNEL_ID = "bluetooth_notification_channel";
+ private static final int NOTIFICATION_ID = android.R.drawable.stat_sys_data_bluetooth;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!Flags.enableBluetoothKeyMissingDialog()) {
+ return;
+ }
+ String action = intent.getAction();
+ if (action == null) {
+ return;
+ }
+
+ BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ PowerManager powerManager = context.getSystemService(PowerManager.class);
+ if (TextUtils.equals(action, BluetoothDevice.ACTION_KEY_MISSING)) {
+ Log.d(TAG, "Receive ACTION_KEY_MISSING");
+ if (shouldShowDialog(context, device, powerManager)) {
+ Intent pairingIntent = getKeyMissingDialogIntent(context, device);
+ Log.d(TAG, "Show key missing dialog:" + device);
+ context.startActivityAsUser(pairingIntent, UserHandle.CURRENT);
+ } else {
+ Log.d(TAG, "Show key missing notification: " + device);
+ showNotification(context, device);
+ }
+ }
+ }
+
+ private Intent getKeyMissingDialogIntent(Context context, BluetoothDevice device) {
+ Intent pairingIntent = new Intent();
+ pairingIntent.setClass(context, BluetoothKeyMissingDialog.class);
+ pairingIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ pairingIntent.setAction(BluetoothDevice.ACTION_KEY_MISSING);
+ pairingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ return pairingIntent;
+ }
+
+ private boolean shouldShowDialog(
+ Context context, BluetoothDevice device, PowerManager powerManager) {
+ return LocalBluetoothPreferences.shouldShowDialogInForeground(context, device)
+ && powerManager.isInteractive();
+ }
+
+ private void showNotification(Context context, BluetoothDevice bluetoothDevice) {
+ NotificationManager nm = context.getSystemService(NotificationManager.class);
+ NotificationChannel notificationChannel =
+ new NotificationChannel(
+ CHANNEL_ID,
+ context.getString(R.string.bluetooth),
+ NotificationManager.IMPORTANCE_HIGH);
+ nm.createNotificationChannel(notificationChannel);
+
+ PendingIntent pairIntent =
+ PendingIntent.getActivity(
+ context,
+ 0,
+ getKeyMissingDialogIntent(context, bluetoothDevice),
+ PendingIntent.FLAG_ONE_SHOT
+ | PendingIntent.FLAG_UPDATE_CURRENT
+ | PendingIntent.FLAG_IMMUTABLE);
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context,
+ CHANNEL_ID)
+ .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
+ .setTicker(context.getString(R.string.bluetooth_notif_ticker))
+ .setLocalOnly(true);
+ builder.setContentTitle(
+ context.getString(
+ R.string.bluetooth_key_missing_title, bluetoothDevice.getName()))
+ .setContentText(context.getString(R.string.bluetooth_key_missing_message))
+ .setContentIntent(pairIntent)
+ .setAutoCancel(true)
+ .setDefaults(Notification.DEFAULT_SOUND)
+ .setColor(
+ context.getColor(
+ com.android.internal.R.color.system_notification_accent_color));
+
+ nm.notify(NOTIFICATION_ID, builder.build());
+ }
+}
diff --git a/src/com/android/settings/gestures/OneHandedSettings.java b/src/com/android/settings/gestures/OneHandedSettings.java
index c84b9ea6934..0a1ab64360c 100644
--- a/src/com/android/settings/gestures/OneHandedSettings.java
+++ b/src/com/android/settings/gestures/OneHandedSettings.java
@@ -23,9 +23,14 @@ import android.content.Context;
import android.os.Bundle;
import android.os.UserHandle;
import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.RecyclerView;
import com.android.internal.accessibility.AccessibilityShortcutController;
import com.android.settings.R;
+import com.android.settings.accessibility.AccessibilityFragmentUtils;
import com.android.settings.accessibility.AccessibilityShortcutPreferenceFragment;
import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType;
import com.android.settings.search.BaseSearchIndexProvider;
@@ -176,4 +181,12 @@ public class OneHandedSettings extends AccessibilityShortcutPreferenceFragment {
return OneHandedSettingsUtils.isSupportOneHandedMode();
}
};
+
+ @Override
+ public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent,
+ Bundle savedInstanceState) {
+ RecyclerView recyclerView =
+ super.onCreateRecyclerView(inflater, parent, savedInstanceState);
+ return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(recyclerView);
+ }
}
diff --git a/src/com/android/settings/network/NetworkProviderSettings.java b/src/com/android/settings/network/NetworkProviderSettings.java
index e2406826320..69183ff25c0 100644
--- a/src/com/android/settings/network/NetworkProviderSettings.java
+++ b/src/com/android/settings/network/NetworkProviderSettings.java
@@ -19,6 +19,8 @@ package com.android.settings.network;
import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLED;
import static android.os.UserManager.DISALLOW_CONFIG_WIFI;
+import static com.android.wifitrackerlib.WifiEntry.CONNECTED_STATE_CONNECTED;
+
import android.app.Activity;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
@@ -669,7 +671,7 @@ public class NetworkProviderSettings extends RestrictedSettingsFragment
@VisibleForTesting
void addModifyMenuIfSuitable(ContextMenu menu, WifiEntry wifiEntry) {
if (mIsAdmin && wifiEntry.isSaved()
- && wifiEntry.getConnectedState() != WifiEntry.CONNECTED_STATE_CONNECTED) {
+ && wifiEntry.getConnectedState() != CONNECTED_STATE_CONNECTED) {
menu.add(Menu.NONE, MENU_ID_MODIFY, 0 /* order */, R.string.wifi_modify);
}
}
@@ -765,7 +767,7 @@ public class NetworkProviderSettings extends RestrictedSettingsFragment
private void showDialog(WifiEntry wifiEntry, int dialogMode) {
if (WifiUtils.isNetworkLockedDown(getActivity(), wifiEntry.getWifiConfiguration())
- && wifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED) {
+ && wifiEntry.getConnectedState() == CONNECTED_STATE_CONNECTED) {
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getActivity(),
RestrictedLockUtilsInternal.getDeviceOwner(getActivity()));
return;
@@ -1068,8 +1070,8 @@ public class NetworkProviderSettings extends RestrictedSettingsFragment
@VisibleForTesting
void launchNetworkDetailsFragment(LongPressWifiEntryPreference pref) {
final WifiEntry wifiEntry = pref.getWifiEntry();
- if (!wifiEntry.isSaved()) {
- Log.w(TAG, "launchNetworkDetailsFragment: Don't launch because WifiEntry isn't saved!");
+ if (!wifiEntry.isSaved() && wifiEntry.getConnectedState() != CONNECTED_STATE_CONNECTED) {
+ Log.w(TAG, "Don't launch Wi-Fi details because WifiEntry is not saved or connected!");
return;
}
final Context context = requireContext();
diff --git a/src/com/android/settings/network/SimOnboardingActivity.kt b/src/com/android/settings/network/SimOnboardingActivity.kt
index a5d4ade6992..25afb661e8b 100644
--- a/src/com/android/settings/network/SimOnboardingActivity.kt
+++ b/src/com/android/settings/network/SimOnboardingActivity.kt
@@ -221,6 +221,7 @@ class SimOnboardingActivity : SpaBaseDialogActivity() {
"showRestartDialog:${showRestartDialog.value}")
showStartingDialog.value = false
} else if (onboardingService.activeSubInfoList.isNotEmpty()) {
+ Log.d(TAG, "status: showStartingDialog.value:${showStartingDialog.value}")
showStartingDialog.value = true
}
}
@@ -468,11 +469,11 @@ class SimOnboardingActivity : SpaBaseDialogActivity() {
}
fun handleEnableMultiSimSidecarStateChange() {
- showDsdsProgressDialog.value = false
when (enableMultiSimSidecar!!.state) {
SidecarFragment.State.SUCCESS -> {
enableMultiSimSidecar!!.reset()
Log.i(TAG, "Successfully switched to DSDS without reboot.")
+ showDsdsProgressDialog.value = false
// refresh data
initServiceData(this, onboardingService.targetSubId, callbackListener)
startSimOnboardingProvider()
@@ -480,6 +481,7 @@ class SimOnboardingActivity : SpaBaseDialogActivity() {
SidecarFragment.State.ERROR -> {
enableMultiSimSidecar!!.reset()
+ showDsdsProgressDialog.value = false
Log.i(TAG, "Failed to switch to DSDS without rebooting.")
showError.value = ErrorType.ERROR_ENABLE_DSDS
callbackListener(CallbackType.CALLBACK_ERROR)
diff --git a/src/com/android/settings/users/MultiUserSwitchBarController.java b/src/com/android/settings/users/MultiUserSwitchBarController.java
index 07c03d716c3..1d641418714 100644
--- a/src/com/android/settings/users/MultiUserSwitchBarController.java
+++ b/src/com/android/settings/users/MultiUserSwitchBarController.java
@@ -53,6 +53,12 @@ public class MultiUserSwitchBarController implements SwitchWidgetController.OnSw
mSwitchBar = switchBar;
mListener = listener;
mUserCapabilities = UserCapabilities.create(context);
+ updateState();
+ mSwitchBar.setListener(this);
+ }
+
+ void updateState() {
+ mUserCapabilities.updateAddUserCapabilities(mContext);
mSwitchBar.setChecked(mUserCapabilities.mUserSwitcherEnabled);
if (Flags.fixDisablingOfMuToggleWhenRestrictionApplied()) {
@@ -74,7 +80,6 @@ public class MultiUserSwitchBarController implements SwitchWidgetController.OnSw
mSwitchBar.setEnabled(mUserCapabilities.mIsMain);
}
}
- mSwitchBar.setListener(this);
}
@Override
@@ -92,7 +97,7 @@ public class MultiUserSwitchBarController implements SwitchWidgetController.OnSw
Log.d(TAG, "Toggling multi-user feature enabled state to: " + isChecked);
final boolean success = Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.USER_SWITCHER_ENABLED, isChecked ? 1 : 0);
- if (success && mListener != null) {
+ if (success && mListener != null && !Flags.newMultiuserSettingsUx()) {
mListener.onMultiUserSwitchChanged(isChecked);
}
return success;
diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java
index c387d9e461d..a0137df728f 100644
--- a/src/com/android/settings/users/UserSettings.java
+++ b/src/com/android/settings/users/UserSettings.java
@@ -419,6 +419,7 @@ public class UserSettings extends SettingsPreferenceFragment
mTimeoutToDockUserPreferenceController.getPreferenceKey()));
mRemoveGuestOnExitPreferenceController.updateState(screen.findPreference(
mRemoveGuestOnExitPreferenceController.getPreferenceKey()));
+ mSwitchBarController.updateState();
if (mShouldUpdateUserList) {
updateUI();
}
diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilityFragmentUtilsTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilityFragmentUtilsTest.java
new file mode 100644
index 00000000000..cd4ee89aaaf
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilityFragmentUtilsTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.accessibility;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.preference.Preference;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settingslib.widget.IllustrationPreference;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link AccessibilityFragmentUtils} */
+@RunWith(RobolectricTestRunner.class)
+public class AccessibilityFragmentUtilsTest {
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+
+ @Test
+ public void isPreferenceImportantToA11y_basicPreference_isImportant() {
+ final Preference pref = new ShortcutPreference(mContext, /* attrs= */ null);
+
+ assertThat(AccessibilityFragmentUtils.isPreferenceImportantToA11y(pref)).isTrue();
+ }
+
+ @Test
+ public void isPreferenceImportantToA11y_illustrationPreference_hasContentDesc_isImportant() {
+ final IllustrationPreference pref =
+ new IllustrationPreference(mContext, /* attrs= */ null);
+ pref.setContentDescription("content desc");
+
+ assertThat(AccessibilityFragmentUtils.isPreferenceImportantToA11y(pref)).isTrue();
+ }
+
+ @Test
+ public void isPreferenceImportantToA11y_illustrationPreference_noContentDesc_notImportant() {
+ final IllustrationPreference pref =
+ new IllustrationPreference(mContext, /* attrs= */ null);
+ pref.setContentDescription(null);
+
+ assertThat(AccessibilityFragmentUtils.isPreferenceImportantToA11y(pref)).isFalse();
+ }
+
+ @Test
+ public void isPreferenceImportantToA11y_paletteListPreference_notImportant() {
+ final PaletteListPreference pref =
+ new PaletteListPreference(mContext, /* attrs= */ null);
+
+ assertThat(AccessibilityFragmentUtils.isPreferenceImportantToA11y(pref)).isFalse();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java
index 22bb2669bb5..038672fc198 100644
--- a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java
@@ -53,9 +53,12 @@ import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.preference.Preference;
import androidx.preference.TwoStatePreference;
+import androidx.recyclerview.widget.RecyclerView;
import androidx.test.core.app.ApplicationProvider;
import com.android.server.accessibility.Flags;
@@ -1000,6 +1003,28 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
assertThat(summary).isEqualTo(expected);
}
+ @Test
+ @EnableFlags(
+ com.android.settings.accessibility.Flags.FLAG_TOGGLE_FEATURE_FRAGMENT_COLLECTION_INFO)
+ public void fragmentRecyclerView_getCollectionInfo_hasCorrectCounts() {
+ ToggleScreenMagnificationPreferenceFragment fragment =
+ mFragController.create(R.id.main_content, /* bundle= */
+ null).start().resume().get();
+ RecyclerView rv = fragment.getListView();
+
+ AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain();
+ rv.getCompatAccessibilityDelegate().onInitializeAccessibilityNodeInfo(rv, node);
+ AccessibilityNodeInfo.CollectionInfo collectionInfo = node.unwrap().getCollectionInfo();
+
+ // Asserting against specific item counts will be brittle to changes to the preferences
+ // included on this page, so instead just check some properties of these counts.
+ assertThat(collectionInfo.getColumnCount()).isEqualTo(1);
+ assertThat(collectionInfo.getRowCount()).isEqualTo(collectionInfo.getItemCount());
+ assertThat(collectionInfo.getItemCount())
+ // One unimportant item: the illustration preference
+ .isEqualTo(collectionInfo.getImportantForAccessibilityItemCount() + 1);
+ }
+
private void putStringIntoSettings(String key, String componentName) {
Settings.Secure.putString(mContext.getContentResolver(), key, componentName);
}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogTest.java
new file mode 100644
index 00000000000..a47101e7b79
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 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.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+
+import android.bluetooth.BluetoothDevice;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = ShadowAlertDialogCompat.class)
+public class BluetoothKeyMissingDialogTest {
+ @Mock private BluetoothDevice mBluetoothDevice;
+
+ private BluetoothKeyMissingDialogFragment mFragment = null;
+ private FragmentActivity mActivity = null;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mActivity = Robolectric.setupActivity(FragmentActivity.class);
+ mFragment = new BluetoothKeyMissingDialogFragment(mBluetoothDevice);
+ mActivity
+ .getSupportFragmentManager()
+ .beginTransaction()
+ .add(mFragment, null)
+ .commit();
+ shadowMainLooper().idle();
+ }
+
+ @Test
+ public void clickForgetDevice_removeBond() {
+ mFragment.onClick(mFragment.getDialog(), AlertDialog.BUTTON_POSITIVE);
+
+ verify(mBluetoothDevice).removeBond();
+ assertThat(mActivity.isFinishing()).isTrue();
+ }
+
+ @Test
+ public void clickCancel_notRemoveBond() {
+ mFragment.onClick(mFragment.getDialog(), AlertDialog.BUTTON_NEGATIVE);
+
+ verify(mBluetoothDevice, never()).removeBond();
+ assertThat(mActivity.isFinishing()).isTrue();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiverTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiverTest.java
new file mode 100644
index 00000000000..c764ed6cd97
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiverTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 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.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import com.android.settings.flags.Flags;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowApplication;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowBluetoothAdapter.class, ShadowBluetoothUtils.class})
+public class BluetoothKeyMissingReceiverTest {
+ @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private Context mContext;
+ private ShadowApplication mShadowApplication;
+ private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+ @Mock private LocalBluetoothManager mLocalBtManager;
+ @Mock private NotificationManager mNm;
+ @Mock private BluetoothDevice mBluetoothDevice;
+
+ @Before
+ public void setUp() {
+ mContext = spy(RuntimeEnvironment.getApplication());
+ mShadowApplication = Shadow.extract(mContext);
+ mShadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm);
+ mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ mShadowBluetoothAdapter.setEnabled(true);
+ ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
+ }
+
+ @After
+ public void tearDown() {
+ ShadowBluetoothUtils.reset();
+ }
+
+ @Test
+ public void broadcastReceiver_isRegistered() {
+ List