Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a8ea8e0e authored by Haijie Hong's avatar Haijie Hong Committed by Android (Google) Code Review
Browse files

Merge "Add bluetooth profile toggle visibility checker" into main

parents 55fc6463 82688cc1
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -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
+33 −0
Original line number Diff line number Diff line
@@ -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<Set<String>> 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<String> 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
+11 −0
Original line number Diff line number Diff line
@@ -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<Preference> 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<String> getInvisibleProfilePreferenceKeys(
            Context context, BluetoothDevice bluetoothDevice);
}
+8 −0
Original line number Diff line number Diff line
@@ -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<String> getInvisibleProfilePreferenceKeys(
            Context context, BluetoothDevice bluetoothDevice) {
        return ImmutableSet.of();
    }
}
+98 −29
Original line number Diff line number Diff line
@@ -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<LocalBluetoothProfile> 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<SwitchPreference> getProfileSwitches(boolean expectOnlyMConnectable) {
    private List<SwitchPreferenceCompat> getProfileSwitches(boolean expectOnlyMConnectable) {
        if (expectOnlyMConnectable) {
            assertThat(mConnectableProfiles).isNotEmpty();
            assertThat(mProfiles.getPreferenceCount() - 1).isEqualTo(mConnectableProfiles.size());
        }
        List<SwitchPreference> result = new ArrayList<>();
        List<SwitchPreferenceCompat> 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<SwitchPreference> switches) {
    private void verifyProfileSwitchTitles(List<SwitchPreferenceCompat> 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<SwitchPreference> switches = getProfileSwitches(true);
        List<SwitchPreferenceCompat> 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<SwitchPreference> switches = getProfileSwitches(true);
        SwitchPreference pref = switches.get(0);
        List<SwitchPreferenceCompat> 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<SwitchPreference> switches = getProfileSwitches(false);
        List<SwitchPreferenceCompat> 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<SwitchPreference> switches = getProfileSwitches(false);
        List<SwitchPreferenceCompat> 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<SwitchPreference> switches = getProfileSwitches(false);
        List<SwitchPreferenceCompat> 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<SwitchPreferenceCompat> 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<SwitchPreferenceCompat> switches = getProfileSwitches(false);
        assertThat(switches.get(0).isVisible()).isTrue();
    }
}