diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 80b481f16b3..138758059c3 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -33,6 +33,7 @@ + diff --git a/res/values/strings.xml b/res/values/strings.xml index 04901fb05dd..9a4812f1194 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -11999,6 +11999,19 @@ Audio changes as you move your head to sound more natural + + Audio Device Type + + Unknown + + Speaker + + Headphones + + Car Kit + + Other + Network download rate limit diff --git a/res/xml/bluetooth_device_details_fragment.xml b/res/xml/bluetooth_device_details_fragment.xml index 35359f761d3..8f309a487be 100644 --- a/res/xml/bluetooth_device_details_fragment.xml +++ b/res/xml/bluetooth_device_details_fragment.xml @@ -71,6 +71,9 @@ + + diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeController.java b/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeController.java new file mode 100644 index 00000000000..ba5f465005e --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeController.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2022 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.bluetooth.BluetoothDevice.DEVICE_TYPE_LE; +import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_CARKIT; +import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES; +import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_OTHER; +import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER; +import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN; + +import android.content.Context; +import android.media.AudioManager; +import android.media.AudioManager.AudioDeviceCategory; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settingslib.bluetooth.A2dpProfile; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LeAudioProfile; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.core.lifecycle.Lifecycle; + +/** + * Controller responsible for the bluetooth audio device type selection + */ +public class BluetoothDetailsAudioDeviceTypeController extends BluetoothDetailsController + implements Preference.OnPreferenceChangeListener { + private static final String TAG = "BluetoothDetailsAudioDeviceTypeController"; + + private static final boolean DEBUG = false; + + private static final String KEY_BT_AUDIO_DEVICE_TYPE_GROUP = + "bluetooth_audio_device_type_group"; + private static final String KEY_BT_AUDIO_DEVICE_TYPE = "bluetooth_audio_device_type"; + + private final AudioManager mAudioManager; + + private ListPreference mAudioDeviceTypePreference; + + private final LocalBluetoothProfileManager mProfileManager; + + @VisibleForTesting + PreferenceCategory mProfilesContainer; + + public BluetoothDetailsAudioDeviceTypeController( + Context context, + PreferenceFragmentCompat fragment, + LocalBluetoothManager manager, + CachedBluetoothDevice device, + Lifecycle lifecycle) { + super(context, fragment, device, lifecycle); + mAudioManager = context.getSystemService(AudioManager.class); + mProfileManager = manager.getProfileManager(); + } + + @Override + public boolean isAvailable() { + // Available only for A2DP and BLE devices. + A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); + boolean a2dpProfileEnabled = false; + if (a2dpProfile != null) { + a2dpProfileEnabled = a2dpProfile.isEnabled(mCachedDevice.getDevice()); + } + + LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile(); + boolean leAudioProfileEnabled = false; + if (leAudioProfile != null) { + leAudioProfileEnabled = leAudioProfile.isEnabled(mCachedDevice.getDevice()); + } + + return a2dpProfileEnabled || leAudioProfileEnabled; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference instanceof ListPreference) { + final ListPreference pref = (ListPreference) preference; + final String key = pref.getKey(); + if (key.equals(KEY_BT_AUDIO_DEVICE_TYPE)) { + if (newValue instanceof String) { + final String value = (String) newValue; + final int index = pref.findIndexOfValue(value); + if (index >= 0) { + pref.setSummary(pref.getEntries()[index]); + mAudioManager.setBluetoothAudioDeviceCategory(mCachedDevice.getAddress(), + mCachedDevice.getDevice().getType() == DEVICE_TYPE_LE, + Integer.parseInt(value)); + } + } + return true; + } + } + + return false; + } + + @Override + public String getPreferenceKey() { + return KEY_BT_AUDIO_DEVICE_TYPE_GROUP; + } + + @Override + protected void init(PreferenceScreen screen) { + mProfilesContainer = screen.findPreference(getPreferenceKey()); + refresh(); + } + + @Override + protected void refresh() { + mAudioDeviceTypePreference = mProfilesContainer.findPreference( + KEY_BT_AUDIO_DEVICE_TYPE); + if (mAudioDeviceTypePreference == null) { + createAudioDeviceTypePreference(mProfilesContainer.getContext()); + mProfilesContainer.addPreference(mAudioDeviceTypePreference); + } + } + + @VisibleForTesting + void createAudioDeviceTypePreference(Context context) { + mAudioDeviceTypePreference = new ListPreference(context); + mAudioDeviceTypePreference.setKey(KEY_BT_AUDIO_DEVICE_TYPE); + mAudioDeviceTypePreference.setTitle( + mContext.getString(R.string.bluetooth_details_audio_device_types_title)); + mAudioDeviceTypePreference.setEntries(new CharSequence[]{ + mContext.getString(R.string.bluetooth_details_audio_device_type_unknown), + mContext.getString(R.string.bluetooth_details_audio_device_type_speaker), + mContext.getString(R.string.bluetooth_details_audio_device_type_headphones), + mContext.getString(R.string.bluetooth_details_audio_device_type_carkit), + mContext.getString(R.string.bluetooth_details_audio_device_type_other), + }); + mAudioDeviceTypePreference.setEntryValues(new CharSequence[]{ + Integer.toString(AUDIO_DEVICE_CATEGORY_UNKNOWN), + Integer.toString(AUDIO_DEVICE_CATEGORY_SPEAKER), + Integer.toString(AUDIO_DEVICE_CATEGORY_HEADPHONES), + Integer.toString(AUDIO_DEVICE_CATEGORY_CARKIT), + Integer.toString(AUDIO_DEVICE_CATEGORY_OTHER), + }); + + @AudioDeviceCategory final int deviceCategory = + mAudioManager.getBluetoothAudioDeviceCategory(mCachedDevice.getAddress(), + mCachedDevice.getDevice().getType() == DEVICE_TYPE_LE); + if (DEBUG) { + Log.v(TAG, "getBluetoothAudioDeviceCategory() device: " + + mCachedDevice.getDevice().getAnonymizedAddress() + + ", has audio device category: " + deviceCategory); + } + mAudioDeviceTypePreference.setValue(Integer.toString(deviceCategory)); + + mAudioDeviceTypePreference.setSummary(mAudioDeviceTypePreference.getEntry()); + mAudioDeviceTypePreference.setOnPreferenceChangeListener(this); + } + + @VisibleForTesting + ListPreference getAudioDeviceTypePreference() { + return mAudioDeviceTypePreference; + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index 99f3e3187cb..c48494b25af 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -300,6 +300,8 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment lifecycle)); controllers.add(new BluetoothDetailsCompanionAppsController(context, this, mCachedDevice, lifecycle)); + controllers.add(new BluetoothDetailsAudioDeviceTypeController(context, this, mManager, + mCachedDevice, lifecycle)); controllers.add(new BluetoothDetailsSpatialAudioController(context, this, mCachedDevice, lifecycle)); controllers.add(new BluetoothDetailsProfilesController(context, this, mManager, diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeControllerTest.java new file mode 100644 index 00000000000..0fc06476ca8 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeControllerTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2022 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.bluetooth.BluetoothDevice.DEVICE_TYPE_LE; +import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothDevice; +import android.media.AudioManager; + +import androidx.preference.ListPreference; +import androidx.preference.PreferenceCategory; + +import com.android.settingslib.bluetooth.LeAudioProfile; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class BluetoothDetailsAudioDeviceTypeControllerTest extends + BluetoothDetailsControllerTestBase { + + private static final String MAC_ADDRESS = "04:52:C7:0B:D8:3C"; + private static final String KEY_BT_AUDIO_DEVICE_TYPE = "bluetooth_audio_device_type"; + + @Mock + private AudioManager mAudioManager; + @Mock + private Lifecycle mAudioDeviceTypeLifecycle; + @Mock + private PreferenceCategory mProfilesContainer; + @Mock + private BluetoothDevice mBluetoothDevice; + @Mock + private LocalBluetoothManager mManager; + @Mock + private LocalBluetoothProfileManager mProfileManager; + @Mock + private LeAudioProfile mLeAudioProfile; + private BluetoothDetailsAudioDeviceTypeController mController; + private ListPreference mAudioDeviceTypePref; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = spy(RuntimeEnvironment.application); + when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager); + when(mCachedDevice.getAddress()).thenReturn(MAC_ADDRESS); + when(mCachedDevice.getDevice()).thenReturn(mBluetoothDevice); + when(mBluetoothDevice.getAnonymizedAddress()).thenReturn(MAC_ADDRESS); + when(mBluetoothDevice.getType()).thenReturn(DEVICE_TYPE_LE); + when(mManager.getProfileManager()).thenReturn(mProfileManager); + when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile); + when(mLeAudioProfile.isEnabled(mCachedDevice.getDevice())).thenReturn(true); + + mController = new BluetoothDetailsAudioDeviceTypeController(mContext, mFragment, mManager, + mCachedDevice, mAudioDeviceTypeLifecycle); + mController.mProfilesContainer = mProfilesContainer; + + mController.createAudioDeviceTypePreference(mContext); + mAudioDeviceTypePref = mController.getAudioDeviceTypePreference(); + + when(mProfilesContainer.findPreference(KEY_BT_AUDIO_DEVICE_TYPE)).thenReturn( + mAudioDeviceTypePref); + } + + @Test + public void createAudioDeviceTypePreference_btDeviceIsCategorized_checkSelection() { + int deviceType = AUDIO_DEVICE_CATEGORY_SPEAKER; + when(mAudioManager.getBluetoothAudioDeviceCategory(MAC_ADDRESS, /*isBle=*/true)).thenReturn( + deviceType); + + mController.createAudioDeviceTypePreference(mContext); + mAudioDeviceTypePref = mController.getAudioDeviceTypePreference(); + + assertThat(mAudioDeviceTypePref.getValue()).isEqualTo(Integer.toString(deviceType)); + } + + @Test + public void selectDeviceTypeSpeaker_invokeSetBluetoothAudioDeviceType() { + int deviceType = AUDIO_DEVICE_CATEGORY_SPEAKER; + mAudioDeviceTypePref.setValue(Integer.toString(deviceType)); + + mController.onPreferenceChange(mAudioDeviceTypePref, Integer.toString(deviceType)); + + verify(mAudioManager).setBluetoothAudioDeviceCategory(eq(MAC_ADDRESS), eq(true), + eq(AUDIO_DEVICE_CATEGORY_SPEAKER)); + } +}