diff --git a/aconfig/settings_bluetooth_declarations.aconfig b/aconfig/settings_bluetooth_declarations.aconfig index 841430907d3..58ddd2569cd 100644 --- a/aconfig/settings_bluetooth_declarations.aconfig +++ b/aconfig/settings_bluetooth_declarations.aconfig @@ -6,3 +6,10 @@ flag { description: "Gates whether to offload bluetooth operations to background thread" bug: "305636727" } + +flag { + name: "enable_bluetooth_profile_toggle_visibility_checker" + namespace: "pixel_cross_device_control" + description: "Gates whether to enable checker for bluetooth profile toggle visibility" + bug: "321178209" +} \ No newline at end of file diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java index 3b162b6c9da..943d99bb4ee 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java @@ -37,6 +37,8 @@ import androidx.preference.TwoStatePreference; import com.android.settings.R; import com.android.settings.core.SettingsUIDeviceConfig; +import com.android.settings.flags.Flags; +import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.bluetooth.A2dpProfile; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; @@ -49,11 +51,14 @@ import com.android.settingslib.bluetooth.MapProfile; import com.android.settingslib.bluetooth.PanProfile; import com.android.settingslib.bluetooth.PbapServerProfile; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.utils.ThreadUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; /** * This class adds switches for toggling the individual profiles that a Bluetooth device @@ -79,6 +84,8 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll private static final String LE_AUDIO_TOGGLE_VISIBLE_PROPERTY = "persist.bluetooth.leaudio.toggle_visible"; + private final AtomicReference> mInvisiblePreferenceKey = new AtomicReference<>(); + private LocalBluetoothManager mManager; private LocalBluetoothProfileManager mProfileManager; private CachedBluetoothDevice mCachedDevice; @@ -547,6 +554,22 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll */ @Override protected void refresh() { + if (Flags.enableBluetoothProfileToggleVisibilityChecker()) { + ThreadUtils.postOnBackgroundThread( + () -> { + mInvisiblePreferenceKey.set( + FeatureFactory.getFeatureFactory() + .getBluetoothFeatureProvider() + .getInvisibleProfilePreferenceKeys( + mContext, mCachedDevice.getDevice())); + ThreadUtils.postOnMainThread(this::refreshUi); + }); + } else { + refreshUi(); + } + } + + private void refreshUi() { for (LocalBluetoothProfile profile : getProfiles()) { if (profile == null || !profile.isProfileReady()) { continue; @@ -577,6 +600,16 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll preference.setSelectable(false); mProfilesContainer.addPreference(preference); } + + if (Flags.enableBluetoothProfileToggleVisibilityChecker()) { + Set invisibleKeys = mInvisiblePreferenceKey.get(); + if (invisibleKeys != null) { + for (int i = 0; i < mProfilesContainer.getPreferenceCount(); ++i) { + Preference pref = mProfilesContainer.getPreference(i); + pref.setVisible(pref.isVisible() && !invisibleKeys.contains(pref.getKey())); + } + } + } } @Override diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java b/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java index d44e4b24872..1751082a45f 100644 --- a/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java +++ b/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java @@ -27,6 +27,7 @@ import androidx.preference.Preference; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import java.util.List; +import java.util.Set; /** * Provider for bluetooth related features. @@ -73,4 +74,14 @@ public interface BluetoothFeatureProvider { * @return the extra bluetooth preference list */ List getBluetoothExtraOptions(Context context, CachedBluetoothDevice device); + + /** + * Gets the bluetooth profile preference keys which should be hidden in the device details page. + * + * @param context Context + * @param bluetoothDevice the bluetooth device + * @return the profiles which should be hidden + */ + Set getInvisibleProfilePreferenceKeys( + Context context, BluetoothDevice bluetoothDevice); } diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java index 014a0f3f99a..2d4ac496d49 100644 --- a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java +++ b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java @@ -29,8 +29,10 @@ import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import java.util.List; +import java.util.Set; /** * Impl of {@link BluetoothFeatureProvider} @@ -65,4 +67,10 @@ public class BluetoothFeatureProviderImpl implements BluetoothFeatureProvider { CachedBluetoothDevice device) { return ImmutableList.of(); } + + @Override + public Set getInvisibleProfilePreferenceKeys( + Context context, BluetoothDevice bluetoothDevice) { + return ImmutableSet.of(); + } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsProfilesControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsProfilesControllerTest.java index ad7c9843ef6..9b1466b4fcb 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsProfilesControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsProfilesControllerTest.java @@ -18,6 +18,7 @@ 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.mock; import static org.mockito.Mockito.spy; @@ -28,23 +29,33 @@ import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.flag.junit.SetFlagsRule; import android.sysprop.BluetoothProperties; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; -import androidx.preference.SwitchPreference; +import androidx.preference.SwitchPreferenceCompat; +import com.android.settings.flags.Flags; +import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowBluetoothDevice; +import com.android.settingslib.R; import com.android.settingslib.bluetooth.A2dpProfile; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.LeAudioProfile; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfile; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.bluetooth.MapProfile; import com.android.settingslib.bluetooth.PbapServerProfile; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; -import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -59,30 +70,41 @@ import java.util.Map; import java.util.Set; @RunWith(RobolectricTestRunner.class) -@Ignore @Config(shadows = ShadowBluetoothDevice.class) public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsControllerTestBase { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private static final String LE_DEVICE_MODEL = "le_audio_headset"; private static final String NON_LE_DEVICE_MODEL = "non_le_audio_headset"; private BluetoothDetailsProfilesController mController; private List mConnectableProfiles; private PreferenceCategory mProfiles; + private BluetoothFeatureProvider mFeatureProvider; @Mock private LocalBluetoothManager mLocalManager; @Mock private LocalBluetoothProfileManager mProfileManager; + @Mock + private CachedBluetoothDeviceManager mCachedBluetoothDeviceManager; @Override public void setUp() { super.setUp(); + FakeFeatureFactory fakeFeatureFactory = FakeFeatureFactory.setupForTest(); + mFeatureProvider = fakeFeatureFactory.getBluetoothFeatureProvider(); + mProfiles = spy(new PreferenceCategory(mContext)); when(mProfiles.getPreferenceManager()).thenReturn(mPreferenceManager); mConnectableProfiles = new ArrayList<>(); when(mLocalManager.getProfileManager()).thenReturn(mProfileManager); + when(mLocalManager.getCachedDeviceManager()).thenReturn(mCachedBluetoothDeviceManager); + when(mCachedBluetoothDeviceManager.getCachedDevicesCopy()) + .thenReturn(ImmutableList.of(mCachedDevice)); when(mCachedDevice.getConnectableProfiles()).thenAnswer(invocation -> new ArrayList<>(mConnectableProfiles) ); @@ -196,25 +218,26 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont return profile; } - /** Returns the list of SwitchPreference objects added to the screen - there should be one per - * Bluetooth profile. + /** + * Returns the list of SwitchPreferenceCompat objects added to the screen - there should be one + * per Bluetooth profile. */ - private List getProfileSwitches(boolean expectOnlyMConnectable) { + private List getProfileSwitches(boolean expectOnlyMConnectable) { if (expectOnlyMConnectable) { assertThat(mConnectableProfiles).isNotEmpty(); assertThat(mProfiles.getPreferenceCount() - 1).isEqualTo(mConnectableProfiles.size()); } - List result = new ArrayList<>(); + List result = new ArrayList<>(); for (int i = 0; i < mProfiles.getPreferenceCount(); i++) { final Preference preference = mProfiles.getPreference(i); - if (preference instanceof SwitchPreference) { - result.add((SwitchPreference) preference); + if (preference instanceof SwitchPreferenceCompat) { + result.add((SwitchPreferenceCompat) preference); } } return result; } - private void verifyProfileSwitchTitles(List switches) { + private void verifyProfileSwitchTitles(List switches) { for (int i = 0; i < switches.size(); i++) { String expectedTitle = mContext.getString(mConnectableProfiles.get(i).getNameResource(mDevice)); @@ -234,7 +257,7 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont addFakeProfile(com.android.settingslib.R.string.bluetooth_profile_a2dp, true); addFakeProfile(com.android.settingslib.R.string.bluetooth_profile_headset, false); showScreen(mController); - List switches = getProfileSwitches(true); + List switches = getProfileSwitches(true); verifyProfileSwitchTitles(switches); assertThat(switches.get(0).isChecked()).isTrue(); assertThat(switches.get(1).isChecked()).isFalse(); @@ -260,8 +283,8 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont addFakeProfile(com.android.settingslib.R.string.bluetooth_profile_a2dp, true); addFakeProfile(com.android.settingslib.R.string.bluetooth_profile_headset, true); showScreen(mController); - List switches = getProfileSwitches(true); - SwitchPreference pref = switches.get(0); + List switches = getProfileSwitches(true); + SwitchPreferenceCompat pref = switches.get(0); // Clicking the pref should cause the profile to become not-preferred. assertThat(pref.isChecked()).isTrue(); @@ -296,14 +319,16 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont PbapServerProfile psp = mock(PbapServerProfile.class); when(psp.getNameResource(mDevice)) .thenReturn(com.android.settingslib.R.string.bluetooth_profile_pbap); + when(psp.getSummaryResourceForDevice(mDevice)) + .thenReturn(R.string.bluetooth_profile_pbap_summary); when(psp.toString()).thenReturn(PbapServerProfile.NAME); when(psp.isProfileReady()).thenReturn(true); when(mProfileManager.getPbapProfile()).thenReturn(psp); showScreen(mController); - List switches = getProfileSwitches(false); + List switches = getProfileSwitches(false); assertThat(switches.size()).isEqualTo(1); - SwitchPreference pref = switches.get(0); + SwitchPreferenceCompat pref = switches.get(0); assertThat(pref.getTitle()).isEqualTo( mContext.getString(com.android.settingslib.R.string.bluetooth_profile_pbap)); assertThat(pref.isChecked()).isTrue(); @@ -321,14 +346,16 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont PbapServerProfile psp = mock(PbapServerProfile.class); when(psp.getNameResource(mDevice)) .thenReturn(com.android.settingslib.R.string.bluetooth_profile_pbap); + when(psp.getSummaryResourceForDevice(mDevice)) + .thenReturn(R.string.bluetooth_profile_pbap_summary); when(psp.toString()).thenReturn(PbapServerProfile.NAME); when(psp.isProfileReady()).thenReturn(true); when(mProfileManager.getPbapProfile()).thenReturn(psp); showScreen(mController); - List switches = getProfileSwitches(false); + List switches = getProfileSwitches(false); assertThat(switches.size()).isEqualTo(1); - SwitchPreference pref = switches.get(0); + SwitchPreferenceCompat pref = switches.get(0); assertThat(pref.getTitle()).isEqualTo( mContext.getString(com.android.settingslib.R.string.bluetooth_profile_pbap)); assertThat(pref.isChecked()).isFalse(); @@ -350,9 +377,9 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont when(mProfileManager.getProfileByName(eq(mapProfile.toString()))).thenReturn(mapProfile); mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); showScreen(mController); - List switches = getProfileSwitches(false); + List switches = getProfileSwitches(false); assertThat(switches.size()).isEqualTo(1); - SwitchPreference pref = switches.get(0); + SwitchPreferenceCompat pref = switches.get(0); assertThat(pref.getTitle()).isEqualTo( mContext.getString(com.android.settingslib.R.string.bluetooth_profile_map)); assertThat(pref.isChecked()).isFalse(); @@ -379,8 +406,8 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont return profile; } - private SwitchPreference getHighQualityAudioPref() { - return (SwitchPreference) mScreen.findPreference( + private SwitchPreferenceCompat getHighQualityAudioPref() { + return (SwitchPreferenceCompat) mScreen.findPreference( BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG); } @@ -389,7 +416,7 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont setupDevice(makeDefaultDeviceConfig()); addMockA2dpProfile(true, true, true); showScreen(mController); - SwitchPreference pref = getHighQualityAudioPref(); + SwitchPreferenceCompat pref = getHighQualityAudioPref(); assertThat(pref.getKey()).isEqualTo( BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG); @@ -407,7 +434,7 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont addMockA2dpProfile(true, false, false); showScreen(mController); assertThat(mProfiles.getPreferenceCount()).isEqualTo(2); - SwitchPreference pref = (SwitchPreference) mProfiles.getPreference(0); + SwitchPreferenceCompat pref = (SwitchPreferenceCompat) mProfiles.getPreference(0); assertThat(pref.getKey()) .isNotEqualTo(BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG); assertThat(pref.getTitle()).isEqualTo( @@ -420,7 +447,7 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont addMockA2dpProfile(true, true, true); when(mCachedDevice.isBusy()).thenReturn(true); showScreen(mController); - SwitchPreference pref = getHighQualityAudioPref(); + SwitchPreferenceCompat pref = getHighQualityAudioPref(); assertThat(pref.isEnabled()).isFalse(); } @@ -433,14 +460,14 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont // Disabling media audio should cause the high quality audio switch to disappear, but not // the regular audio one. - SwitchPreference audioPref = - (SwitchPreference) mScreen.findPreference(audioProfile.toString()); + SwitchPreferenceCompat audioPref = + (SwitchPreferenceCompat) mScreen.findPreference(audioProfile.toString()); audioPref.performClick(); verify(audioProfile).setEnabled(mDevice, false); when(audioProfile.isEnabled(mDevice)).thenReturn(false); mController.onDeviceAttributesChanged(); assertThat(audioPref.isVisible()).isTrue(); - SwitchPreference highQualityAudioPref = getHighQualityAudioPref(); + SwitchPreferenceCompat highQualityAudioPref = getHighQualityAudioPref(); assertThat(highQualityAudioPref.isVisible()).isFalse(); // And re-enabling media audio should make high quality switch to reappear. @@ -457,8 +484,8 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont setupDevice(makeDefaultDeviceConfig()); A2dpProfile audioProfile = addMockA2dpProfile(false, true, true); showScreen(mController); - SwitchPreference audioPref = mScreen.findPreference(audioProfile.toString()); - SwitchPreference highQualityAudioPref = getHighQualityAudioPref(); + SwitchPreferenceCompat audioPref = mScreen.findPreference(audioProfile.toString()); + SwitchPreferenceCompat highQualityAudioPref = getHighQualityAudioPref(); assertThat(audioPref).isNotNull(); assertThat(audioPref.isChecked()).isFalse(); assertThat(highQualityAudioPref).isNotNull(); @@ -489,4 +516,46 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont assertThat(mController.isModelNameInAllowList(null)).isFalse(); assertThat(mController.isModelNameInAllowList(NON_LE_DEVICE_MODEL)).isFalse(); } + + @Test + public void prefKeyInBlockingList_hideToggle() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_BLUETOOTH_PROFILE_TOGGLE_VISIBILITY_CHECKER); + setupDevice(makeDefaultDeviceConfig()); + + LeAudioProfile leAudioProfile = mock(LeAudioProfile.class); + when(leAudioProfile.getNameResource(mDevice)) + .thenReturn(com.android.settingslib.R.string.bluetooth_profile_le_audio); + when(leAudioProfile.isProfileReady()).thenReturn(true); + when(leAudioProfile.toString()).thenReturn("LE_AUDIO"); + when(mProfileManager.getLeAudioProfile()).thenReturn(leAudioProfile); + when(mFeatureProvider.getInvisibleProfilePreferenceKeys(any(), any())) + .thenReturn(ImmutableSet.of("LE_AUDIO")); + mConnectableProfiles.add(leAudioProfile); + + showScreen(mController); + + List switches = getProfileSwitches(false); + assertThat(switches.get(0).isVisible()).isFalse(); + } + + @Test + public void prefKeyNotInBlockingList_showToggle() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_BLUETOOTH_PROFILE_TOGGLE_VISIBILITY_CHECKER); + setupDevice(makeDefaultDeviceConfig()); + + LeAudioProfile leAudioProfile = mock(LeAudioProfile.class); + when(leAudioProfile.getNameResource(mDevice)) + .thenReturn(com.android.settingslib.R.string.bluetooth_profile_le_audio); + when(leAudioProfile.isProfileReady()).thenReturn(true); + when(leAudioProfile.toString()).thenReturn("LE_AUDIO"); + when(mProfileManager.getLeAudioProfile()).thenReturn(leAudioProfile); + when(mFeatureProvider.getInvisibleProfilePreferenceKeys(any(), any())) + .thenReturn(ImmutableSet.of("A2DP")); + mConnectableProfiles.add(leAudioProfile); + + showScreen(mController); + + List switches = getProfileSwitches(false); + assertThat(switches.get(0).isVisible()).isTrue(); + } }