From 1890c16f90ee7731d8493acebc10fb6c464fcfb2 Mon Sep 17 00:00:00 2001 From: Daniel Norman Date: Mon, 26 Aug 2024 19:45:27 +0000 Subject: [PATCH 1/6] Populates collection info count for A11y toggle feature pages. This helps an accessibility service like TalkBack inform the user that there are items that are skipped when navigating the list because they are unimportant to accessibility. Bug: 318607873 Test: atest AccessibilityFragmentUtilsTest Test: atest ToggleScreenMagnificationPreferenceFragmentTest Test: Enable TalkBack that supports the collection info count feature, open any of the pages from the bug, observe the item count and (un)important count are correct. Flag: com.android.settings.accessibility.toggle_feature_fragment_collection_info Change-Id: If64c89f2eb2f8301076baa79b9530124c850d2fc --- .../accessibility/accessibility_flags.aconfig | 16 +- .../AccessibilityFragmentUtils.java | 152 ++++++++++++++++++ .../ToggleFeaturePreferenceFragment.java | 9 ++ ...ationPreferenceFragmentForSetupWizard.java | 3 +- ...eaderPreferenceFragmentForSetupWizard.java | 3 +- ...SpeakPreferenceFragmentForSetupWizard.java | 3 +- .../settings/gestures/OneHandedSettings.java | 13 ++ .../AccessibilityFragmentUtilsTest.java | 70 ++++++++ ...enMagnificationPreferenceFragmentTest.java | 25 +++ 9 files changed, 288 insertions(+), 6 deletions(-) create mode 100644 src/com/android/settings/accessibility/AccessibilityFragmentUtils.java create mode 100644 tests/robotests/src/com/android/settings/accessibility/AccessibilityFragmentUtilsTest.java diff --git a/aconfig/accessibility/accessibility_flags.aconfig b/aconfig/accessibility/accessibility_flags.aconfig index 5c81cc9a671..1871172fc5a 100644 --- a/aconfig/accessibility/accessibility_flags.aconfig +++ b/aconfig/accessibility/accessibility_flags.aconfig @@ -37,6 +37,13 @@ flag { bug: "300302098" } +flag { + name: "enable_color_contrast_control" + namespace: "accessibility" + description: "Allows users to control color contrast in the Accessibility settings page." + bug: "246577325" +} + flag { name: "enable_hearing_aid_preset_control" namespace: "accessibility" @@ -89,8 +96,11 @@ flag { } flag { - name: "enable_color_contrast_control" + name: "toggle_feature_fragment_collection_info" namespace: "accessibility" - description: "Allows users to control color contrast in the Accessibility settings page." - bug: "246577325" + description: "Provides custom CollectionInfo for ToggleFeaturePreferenceFragment" + bug: "318607873" + metadata { + purpose: PURPOSE_BUGFIX + } } diff --git a/src/com/android/settings/accessibility/AccessibilityFragmentUtils.java b/src/com/android/settings/accessibility/AccessibilityFragmentUtils.java new file mode 100644 index 00000000000..34e17c01335 --- /dev/null +++ b/src/com/android/settings/accessibility/AccessibilityFragmentUtils.java @@ -0,0 +1,152 @@ +/* + * 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 android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.core.view.AccessibilityDelegateCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroupAdapter; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; + +import com.android.settingslib.widget.IllustrationPreference; + +/** Utilities for {@code Settings > Accessibility} fragments. */ +public class AccessibilityFragmentUtils { + // TODO: b/350782252 - Replace with an official library-provided solution when available. + /** + * Modifies the existing {@link RecyclerViewAccessibilityDelegate} of the provided + * {@link RecyclerView} for this fragment to report the number of visible and important + * items on this page via the RecyclerView's {@link AccessibilityNodeInfo}. + * + *

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/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/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); } From 9ba90e5b3bc4114b80cb8f7d3aeb3a7adf4f1550 Mon Sep 17 00:00:00 2001 From: Tetiana Meronyk Date: Fri, 30 Aug 2024 17:08:49 +0000 Subject: [PATCH 2/6] Update main toggle state in User Settings onResume() The toggle state used to be set on creation and only changed onSwitchToggled(). But the state can also change when restrictions are applied from outside of settings, or when user is added with toggle being off by default. This change updates the state during execution of onResume() in Users Settings, which also aligns with the update strategy of other Toggles on this Settings page. Bug: 362706097 Test: atest MultiUserSwitchBarControllerTest Flag: EXEMPT bugfix Change-Id: I8a994b2e0ddb672362e69653374b87f85ae1548c --- .../settings/users/MultiUserSwitchBarController.java | 9 +++++++-- src/com/android/settings/users/UserSettings.java | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) 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(); } From f93aaadad0337b60d67204ad7d78f049815b06b9 Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Wed, 4 Sep 2024 16:57:14 +0800 Subject: [PATCH 3/6] Show a dialog if bluetooth key is missing when reconnecting Previous change is reverted due to test failure in b/362901443. BUG: 360031750 Test: atest BluetoothKeyMissingDialogTest Flag: com.android.settings.flags.enable_bluetooth_key_missing_dialog Change-Id: I05b940e8aac26c14f93baa19c224ad98c291b891 --- AndroidManifest.xml | 20 +++ .../settings_bluetooth_declarations.aconfig | 10 ++ res/layout/bluetooth_key_missing.xml | 58 +++++++ res/values/strings.xml | 9 + .../bluetooth/BluetoothKeyMissingDialog.java | 47 +++++ .../BluetoothKeyMissingDialogFragment.java | 94 ++++++++++ .../BluetoothKeyMissingReceiver.java | 122 +++++++++++++ .../BluetoothKeyMissingDialogTest.java | 76 +++++++++ .../BluetoothKeyMissingReceiverTest.java | 160 ++++++++++++++++++ 9 files changed, 596 insertions(+) create mode 100644 res/layout/bluetooth_key_missing.xml create mode 100644 src/com/android/settings/bluetooth/BluetoothKeyMissingDialog.java create mode 100644 src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java create mode 100644 src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogTest.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiverTest.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 7b79611ca65..13aafc9dbb7 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3230,6 +3230,19 @@ + + + + + + + + + + + + + diff --git a/aconfig/settings_bluetooth_declarations.aconfig b/aconfig/settings_bluetooth_declarations.aconfig index 3d14288fcd2..0c423b5a1b7 100644 --- a/aconfig/settings_bluetooth_declarations.aconfig +++ b/aconfig/settings_bluetooth_declarations.aconfig @@ -34,3 +34,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_bluetooth_key_missing_dialog" + namespace: "cross_device_experiences" + description: "Show a dialog if the bluetooth key is missing when reconnecting" + bug: "360031750" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/res/layout/bluetooth_key_missing.xml b/res/layout/bluetooth_key_missing.xml new file mode 100644 index 00000000000..b9f8d866bd3 --- /dev/null +++ b/res/layout/bluetooth_key_missing.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 0fcb0d68502..17a9a08f48d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1860,6 +1860,15 @@ Change + + %1$s not connected + + For your security, forget this device, then pair it again + + Forget device + + Cancel + Device details 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/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 registeredReceivers = + mShadowApplication.getRegisteredReceivers(); + + int matchedCount = + registeredReceivers.stream() + .filter( + receiver -> + BluetoothKeyMissingReceiver.class + .getSimpleName() + .equals( + receiver.broadcastReceiver + .getClass() + .getSimpleName())) + .collect(Collectors.toList()) + .size(); + assertThat(matchedCount).isEqualTo(1); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_BLUETOOTH_KEY_MISSING_DIALOG) + public void broadcastReceiver_receiveKeyMissingIntentFlagOff_doNothing() { + Intent intent = spy(new Intent(BluetoothDevice.ACTION_KEY_MISSING)); + when(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)).thenReturn(mBluetoothDevice); + BluetoothKeyMissingReceiver bluetoothKeyMissingReceiver = getReceiver(intent); + bluetoothKeyMissingReceiver.onReceive(mContext, intent); + + verifyNoInteractions(mNm); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_BLUETOOTH_KEY_MISSING_DIALOG) + public void broadcastReceiver_background_showNotification() { + Intent intent = spy(new Intent(BluetoothDevice.ACTION_KEY_MISSING)); + when(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)).thenReturn(mBluetoothDevice); + BluetoothKeyMissingReceiver bluetoothKeyMissingReceiver = getReceiver(intent); + bluetoothKeyMissingReceiver.onReceive(mContext, intent); + + verify(mNm).notify(eq(android.R.drawable.stat_sys_data_bluetooth), any(Notification.class)); + verify(mContext, never()).startActivityAsUser(any(), any()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_BLUETOOTH_KEY_MISSING_DIALOG) + public void broadcastReceiver_foreground_receiveKeyMissingIntent_showDialog() { + when(mLocalBtManager.isForegroundActivity()).thenReturn(true); + Intent intent = spy(new Intent(BluetoothDevice.ACTION_KEY_MISSING)); + when(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)).thenReturn(mBluetoothDevice); + BluetoothKeyMissingReceiver bluetoothKeyMissingReceiver = getReceiver(intent); + bluetoothKeyMissingReceiver.onReceive(mContext, intent); + + verifyNoInteractions(mNm); + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mContext).startActivityAsUser(captor.capture(), eq(UserHandle.CURRENT)); + assertThat(captor.getValue().getComponent().getClassName()) + .isEqualTo(BluetoothKeyMissingDialog.class.getName()); + } + + private BluetoothKeyMissingReceiver getReceiver(Intent intent) { + assertThat(mShadowApplication.hasReceiverForIntent(intent)).isTrue(); + List receiversForIntent = + mShadowApplication.getReceiversForIntent(intent); + assertThat(receiversForIntent).hasSize(1); + BroadcastReceiver broadcastReceiver = receiversForIntent.get(0); + assertThat(broadcastReceiver).isInstanceOf(BluetoothKeyMissingReceiver.class); + return (BluetoothKeyMissingReceiver) broadcastReceiver; + } +} From 74c7f9b65f0c725a6ac801eb108403940297661c Mon Sep 17 00:00:00 2001 From: tomhsu Date: Thu, 5 Sep 2024 14:00:56 +0000 Subject: [PATCH 4/6] Fix no content change if activity start from onNewIntent - Mobile network page can not change content to another subscription if this is started from onNewIntent. This is because intent is not udpated so current fragment still get the information from old Intent. Flag: EXEMPT bug fix Bug: 270416514 Test: Manual test passed - adb shell am start -a android.settings.NETWORK_OPERATOR_SETTINGS --ei android.provider.extra.SUB_ID x Change-Id: Ib6aa9d359232ff602551e121b52894a27a84e4e1 --- src/com/android/settings/Settings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index 493a27b6f26..7678338976a 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -447,7 +447,7 @@ public class Settings extends SettingsActivity { super.onNewIntent(intent); Log.d(TAG, "Starting onNewIntent"); - + setIntent(intent); createUiFromIntent(null /* savedState */, convertIntent(intent)); } From 74a76a806747d22592e507a30d97b1e63bfe9b78 Mon Sep 17 00:00:00 2001 From: songferngwang Date: Thu, 5 Sep 2024 18:35:27 +0000 Subject: [PATCH 5/6] Fix the DSDS dialog status To avoid the dsds dialog status to reset. Bug: 364668360 Test: verify enabling DSDS mode Flag: EXEMPT bugfix Change-Id: I80a8abd896856740fad1f98de50c79034a4cc602 --- src/com/android/settings/network/SimOnboardingActivity.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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) From 99cd495d5b83ed114bc4eab70314ce8c94b0feea Mon Sep 17 00:00:00 2001 From: Weng Su Date: Fri, 6 Sep 2024 07:42:15 +0800 Subject: [PATCH 6/6] Launch Wi-Fi details for connected Wi-Fi network - Since the posspoint Wi-Fi entry can be connected even if the entry is not saved - Allow to launch Wi-Fi details when the entry is connected but not saved Bug: 363151879 Flag: EXEMPT bugfix Test: Manual testing atest -c NetworkProviderSettingsTest Change-Id: I004b8a33bdd07cb92e167039949919f255c5303e --- .../network/NetworkProviderSettings.java | 10 ++-- .../network/NetworkProviderSettingsTest.java | 51 +++++++++++++++++-- 2 files changed, 52 insertions(+), 9 deletions(-) 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/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java b/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java index 1bed8a82172..400f73f7f56 100644 --- a/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java +++ b/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java @@ -22,6 +22,7 @@ import static com.android.settings.network.NetworkProviderSettings.MENU_ID_MODIF import static com.android.settings.network.NetworkProviderSettings.MENU_ID_SHARE; import static com.android.settings.wifi.WifiConfigUiBase2.MODE_CONNECT; import static com.android.settings.wifi.WifiConfigUiBase2.MODE_MODIFY; +import static com.android.wifitrackerlib.WifiEntry.CONNECTED_STATE_CONNECTED; import static com.android.wifitrackerlib.WifiEntry.CONNECTED_STATE_DISCONNECTED; import static com.android.wifitrackerlib.WifiPickerTracker.WIFI_ENTRIES_CHANGED_REASON_GENERAL; @@ -343,7 +344,7 @@ public class NetworkProviderSettingsTest { when(mWifiEntry.canDisconnect()).thenReturn(true); when(mWifiEntry.canForget()).thenReturn(true); when(mWifiEntry.isSaved()).thenReturn(true); - when(mWifiEntry.getConnectedState()).thenReturn(WifiEntry.CONNECTED_STATE_CONNECTED); + when(mWifiEntry.getConnectedState()).thenReturn(CONNECTED_STATE_CONNECTED); final LongPressWifiEntryPreference connectedWifiEntryPreference = mNetworkProviderSettings.createLongPressWifiEntryPreference(mWifiEntry); @@ -366,7 +367,7 @@ public class NetworkProviderSettingsTest { when(mWifiEntry.canShare()).thenReturn(true); when(mWifiEntry.canForget()).thenReturn(true); when(mWifiEntry.isSaved()).thenReturn(true); - when(mWifiEntry.getConnectedState()).thenReturn(WifiEntry.CONNECTED_STATE_CONNECTED); + when(mWifiEntry.getConnectedState()).thenReturn(CONNECTED_STATE_CONNECTED); final LongPressWifiEntryPreference connectedWifiEntryPreference = mNetworkProviderSettings.createLongPressWifiEntryPreference(mWifiEntry); @@ -388,7 +389,7 @@ public class NetworkProviderSettingsTest { when(mWifiEntry.canShare()).thenReturn(false); when(mWifiEntry.canForget()).thenReturn(true); when(mWifiEntry.isSaved()).thenReturn(true); - when(mWifiEntry.getConnectedState()).thenReturn(WifiEntry.CONNECTED_STATE_CONNECTED); + when(mWifiEntry.getConnectedState()).thenReturn(CONNECTED_STATE_CONNECTED); final LongPressWifiEntryPreference connectedWifiEntryPreference = mNetworkProviderSettings.createLongPressWifiEntryPreference(mWifiEntry); @@ -872,14 +873,54 @@ public class NetworkProviderSettingsTest { } @Test - public void launchNetworkDetailsFragment_wifiEntryIsNotSaved_ignoreWifiEntry() { + public void launchNetworkDetailsFragment_entryDisconnectedNotSaved_ignore() { + when(mWifiEntry.getConnectedState()).thenReturn(CONNECTED_STATE_DISCONNECTED); when(mWifiEntry.isSaved()).thenReturn(false); LongPressWifiEntryPreference preference = mNetworkProviderSettings.createLongPressWifiEntryPreference(mWifiEntry); mNetworkProviderSettings.launchNetworkDetailsFragment(preference); - verify(mWifiEntry, never()).getKey(); + verify(mContext, never()).startActivity(any()); + } + + @Test + public void launchNetworkDetailsFragment_entryConnectedNotSaved_launch() { + doNothing().when(mContext).startActivity(any()); + when(mWifiEntry.getConnectedState()).thenReturn(CONNECTED_STATE_CONNECTED); + when(mWifiEntry.isSaved()).thenReturn(false); + LongPressWifiEntryPreference preference = + mNetworkProviderSettings.createLongPressWifiEntryPreference(mWifiEntry); + + mNetworkProviderSettings.launchNetworkDetailsFragment(preference); + + verify(mContext).startActivity(any()); + } + + @Test + public void launchNetworkDetailsFragment_entryDisconnectedSaved_launch() { + doNothing().when(mContext).startActivity(any()); + when(mWifiEntry.getConnectedState()).thenReturn(CONNECTED_STATE_DISCONNECTED); + when(mWifiEntry.isSaved()).thenReturn(true); + LongPressWifiEntryPreference preference = + mNetworkProviderSettings.createLongPressWifiEntryPreference(mWifiEntry); + + mNetworkProviderSettings.launchNetworkDetailsFragment(preference); + + verify(mContext).startActivity(any()); + } + + @Test + public void launchNetworkDetailsFragment_entryConnectedSaved_launch() { + doNothing().when(mContext).startActivity(any()); + when(mWifiEntry.getConnectedState()).thenReturn(CONNECTED_STATE_CONNECTED); + when(mWifiEntry.isSaved()).thenReturn(true); + LongPressWifiEntryPreference preference = + mNetworkProviderSettings.createLongPressWifiEntryPreference(mWifiEntry); + + mNetworkProviderSettings.launchNetworkDetailsFragment(preference); + + verify(mContext).startActivity(any()); } @Implements(PreferenceFragmentCompat.class)