From f1d544f9454affdaae585c2a3e268316dc1a69e5 Mon Sep 17 00:00:00 2001 From: Antony Sargent Date: Thu, 20 Apr 2017 12:35:32 -0700 Subject: [PATCH] Add a "Use high quality audio" option to Bluetooth A2DP device settings This change adds a checkbox reading "Use high quality audio: " to the details dialog for A2DP audio bluetooth devices that support codecs other than the mandatory SBC (eg AAC, LDAC, aptX, etc.). Bug: 37441685 Test: make RunSettingsRoboTests Change-Id: I6e5423db11a0cd7fe0b1141dd998e7c936c240f0 --- .../bluetooth/DeviceProfilesSettings.java | 35 +++ .../bluetooth/BluetoothCodecConfig.java | 23 ++ .../bluetooth/DeviceProfilesSettingsTest.java | 206 ++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 tests/robotests/src/android/bluetooth/BluetoothCodecConfig.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/DeviceProfilesSettingsTest.java diff --git a/src/com/android/settings/bluetooth/DeviceProfilesSettings.java b/src/com/android/settings/bluetooth/DeviceProfilesSettings.java index a6dfa9b4b48..a76ed460585 100755 --- a/src/com/android/settings/bluetooth/DeviceProfilesSettings.java +++ b/src/com/android/settings/bluetooth/DeviceProfilesSettings.java @@ -23,6 +23,7 @@ import android.bluetooth.BluetoothProfile; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; +import android.support.annotation.VisibleForTesting; import android.text.Html; import android.text.TextUtils; import android.util.Log; @@ -37,6 +38,7 @@ import android.widget.TextView; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settingslib.bluetooth.A2dpProfile; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothManager; @@ -55,6 +57,8 @@ public final class DeviceProfilesSettings extends InstrumentedDialogFragment imp private static final String KEY_PROFILE_CONTAINER = "profile_container"; private static final String KEY_UNPAIR = "unpair"; private static final String KEY_PBAP_SERVER = "PBAP Server"; + @VisibleForTesting + static final String HIGH_QUALITY_AUDIO_PREF_TAG = "A2dpProfileHighQualityAudio"; private CachedBluetoothDevice mCachedDevice; private LocalBluetoothManager mManager; @@ -169,6 +173,21 @@ public final class DeviceProfilesSettings extends InstrumentedDialogFragment imp for (LocalBluetoothProfile profile : mCachedDevice.getConnectableProfiles()) { CheckBox pref = createProfilePreference(profile); mProfileContainer.addView(pref); + + if (profile instanceof A2dpProfile) { + BluetoothDevice device = mCachedDevice.getDevice(); + A2dpProfile a2dpProfile = (A2dpProfile) profile; + if (a2dpProfile.supportsHighQualityAudio(device)) { + CheckBox highQualityPref = new CheckBox(getActivity()); + highQualityPref.setTag(HIGH_QUALITY_AUDIO_PREF_TAG); + highQualityPref.setOnClickListener(v -> { + a2dpProfile.setHighQualityAudioEnabled(device, highQualityPref.isChecked()); + }); + highQualityPref.setVisibility(View.GONE); + mProfileContainer.addView(highQualityPref); + } + refreshProfilePreference(pref, profile); + } } final int pbapPermission = mCachedDevice.getPhonebookPermissionChoice(); @@ -356,6 +375,22 @@ public final class DeviceProfilesSettings extends InstrumentedDialogFragment imp } else { profilePref.setChecked(profile.isPreferred(device)); } + if (profile instanceof A2dpProfile) { + A2dpProfile a2dpProfile = (A2dpProfile) profile; + View v = mProfileContainer.findViewWithTag(HIGH_QUALITY_AUDIO_PREF_TAG); + if (v instanceof CheckBox) { + CheckBox highQualityPref = (CheckBox) v; + highQualityPref.setText(a2dpProfile.getHighQualityAudioOptionLabel(device)); + highQualityPref.setChecked(a2dpProfile.isHighQualityAudioEnabled(device)); + + if (a2dpProfile.isPreferred(device)) { + v.setVisibility(View.VISIBLE); + v.setEnabled(!mCachedDevice.isBusy()); + } else { + v.setVisibility(View.GONE); + } + } + } } private LocalBluetoothProfile getProfileOf(View v) { diff --git a/tests/robotests/src/android/bluetooth/BluetoothCodecConfig.java b/tests/robotests/src/android/bluetooth/BluetoothCodecConfig.java new file mode 100644 index 00000000000..40b76df759c --- /dev/null +++ b/tests/robotests/src/android/bluetooth/BluetoothCodecConfig.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 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 android.bluetooth; + +/** + * A placeholder class to prevent ClassNotFound exceptions caused by lack of visibility. + */ +public class BluetoothCodecConfig { +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/DeviceProfilesSettingsTest.java b/tests/robotests/src/com/android/settings/bluetooth/DeviceProfilesSettingsTest.java new file mode 100644 index 00000000000..c61823cb8ee --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/DeviceProfilesSettingsTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2017 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.Activity; +import android.content.Context; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; + +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.testutils.shadow.ShadowEventLogWriter; +import com.android.settingslib.R; +import com.android.settingslib.bluetooth.A2dpProfile; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.LocalBluetoothAdapter; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfile; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; +import org.robolectric.util.FragmentTestUtil; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.RuntimeEnvironment; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + +import java.util.ArrayList; + + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION, shadows = { + ShadowEventLogWriter.class +}) +public class DeviceProfilesSettingsTest { + Context mContext; + @Mock Activity mActivity; + @Mock LocalBluetoothManager mManager; + @Mock LocalBluetoothAdapter mAdapter; + @Mock LocalBluetoothProfileManager mProfileManager; + @Mock CachedBluetoothDeviceManager mDeviceManager; + @Mock CachedBluetoothDevice mCachedDevice; + @Mock A2dpProfile mProfile; + + ArrayList mProfiles; + DeviceProfilesSettings mFragment; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + + when(mProfile.getNameResource(any())).thenReturn(R.string.bluetooth_profile_a2dp); + mProfiles = new ArrayList<>(); + mProfiles.add(mProfile); + when(mCachedDevice.getConnectableProfiles()).thenReturn(mProfiles); + + mFragment = new DeviceProfilesSettings(); + mFragment.setArguments(new Bundle()); + + ReflectionHelpers.setStaticField(LocalBluetoothManager.class, "sInstance", mManager); + when(mManager.getCachedDeviceManager()).thenReturn(mDeviceManager); + when(mManager.getBluetoothAdapter()).thenReturn(mAdapter); + when(mManager.getProfileManager()).thenReturn(mProfileManager); + when(mProfileManager.getMapProfile()).thenReturn(null); + when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice); + } + + @Test + public void deviceHasHighQualityAudio() { + when(mProfile.supportsHighQualityAudio(any())).thenReturn(true); + when(mProfile.isHighQualityAudioEnabled(any())).thenReturn(true); + when(mProfile.isPreferred(any())).thenReturn(true); + FragmentTestUtil.startFragment(mFragment); + + ViewGroup profilesGroup = mFragment.getDialog().findViewById(R.id.profiles_section); + CheckBox box = (CheckBox) profilesGroup.findViewWithTag( + DeviceProfilesSettings.HIGH_QUALITY_AUDIO_PREF_TAG); + assertThat(box).isNotNull(); + assertThat(box.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(box.isEnabled()).isTrue(); + assertThat(box.isChecked()).isTrue(); + + box.performClick(); + verify(mProfile).setHighQualityAudioEnabled(any(), eq(false)); + box.performClick(); + verify(mProfile).setHighQualityAudioEnabled(any(), eq(true)); + } + + @Test + public void busyDeviceDisablesControl() { + when(mProfile.supportsHighQualityAudio(any())).thenReturn(true); + when(mProfile.isHighQualityAudioEnabled(any())).thenReturn(true); + when(mProfile.isPreferred(any())).thenReturn(true); + when(mCachedDevice.isBusy()).thenReturn(true); + FragmentTestUtil.startFragment(mFragment); + + // Make sure that the high quality audio option is present but disabled when the device + // is busy. + ViewGroup profilesGroup = mFragment.getDialog().findViewById(R.id.profiles_section); + CheckBox box = (CheckBox) profilesGroup.findViewWithTag( + DeviceProfilesSettings.HIGH_QUALITY_AUDIO_PREF_TAG); + assertThat(box).isNotNull(); + assertThat(box.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(box.isEnabled()).isFalse(); + } + + @Test + public void mediaAudioGetsDisabledAndReEnabled() { + when(mProfile.supportsHighQualityAudio(any())).thenReturn(true); + when(mProfile.isHighQualityAudioEnabled(any())).thenReturn(true); + when(mProfile.isPreferred(any())).thenReturn(true); + FragmentTestUtil.startFragment(mFragment); + + ViewGroup profilesGroup = mFragment.getDialog().findViewById(R.id.profiles_section); + CheckBox audioBox = profilesGroup.findViewWithTag(mProfile.toString()); + CheckBox highQualityAudioBox = profilesGroup.findViewWithTag( + DeviceProfilesSettings.HIGH_QUALITY_AUDIO_PREF_TAG); + assertThat(audioBox).isNotNull(); + assertThat(audioBox.isChecked()).isTrue(); + assertThat(highQualityAudioBox).isNotNull(); + assertThat(highQualityAudioBox.isChecked()).isTrue(); + + // Disabling media audio should cause the high quality audio box to disappear. + when(mProfile.isPreferred(any())).thenReturn(false); + mFragment.onDeviceAttributesChanged(); + audioBox = profilesGroup.findViewWithTag(mProfile.toString()); + highQualityAudioBox = profilesGroup.findViewWithTag( + DeviceProfilesSettings.HIGH_QUALITY_AUDIO_PREF_TAG); + assertThat(audioBox).isNotNull(); + assertThat(audioBox.isChecked()).isFalse(); + assertThat(highQualityAudioBox).isNotNull(); + assertThat(highQualityAudioBox.getVisibility()).isEqualTo(View.GONE); + + // And re-enabling media audio should make it reappear. + when(mProfile.isPreferred(any())).thenReturn(true); + mFragment.onDeviceAttributesChanged(); + audioBox = profilesGroup.findViewWithTag(mProfile.toString()); + highQualityAudioBox = profilesGroup.findViewWithTag( + DeviceProfilesSettings.HIGH_QUALITY_AUDIO_PREF_TAG); + assertThat(audioBox).isNotNull(); + assertThat(audioBox.isChecked()).isTrue(); + assertThat(highQualityAudioBox).isNotNull(); + assertThat(highQualityAudioBox.isChecked()).isTrue(); + } + + @Test + public void mediaAudioStartsDisabled() { + when(mProfile.supportsHighQualityAudio(any())).thenReturn(true); + when(mProfile.isHighQualityAudioEnabled(any())).thenReturn(true); + when(mProfile.isPreferred(any())).thenReturn(false); + + FragmentTestUtil.startFragment(mFragment); + ViewGroup profilesGroup = mFragment.getDialog().findViewById(R.id.profiles_section); + CheckBox audioBox = profilesGroup.findViewWithTag(mProfile.toString()); + CheckBox highQualityAudioBox = profilesGroup.findViewWithTag( + DeviceProfilesSettings.HIGH_QUALITY_AUDIO_PREF_TAG); + + assertThat(audioBox).isNotNull(); + assertThat(audioBox.isChecked()).isFalse(); + assertThat(highQualityAudioBox).isNotNull(); + assertThat(highQualityAudioBox.getVisibility()).isEqualTo(View.GONE); + } + + @Test + public void deviceDoesntHaveHighQualityAudio() { + when(mProfile.supportsHighQualityAudio(any())).thenReturn(false); + when(mProfile.isPreferred(any())).thenReturn(true); + FragmentTestUtil.startFragment(mFragment); + + // A device that doesn't support high quality audio shouldn't have the checkbox for + // high quality audio support. + ViewGroup profilesGroup = mFragment.getDialog().findViewById(R.id.profiles_section); + CheckBox box = (CheckBox) profilesGroup.findViewWithTag( + DeviceProfilesSettings.HIGH_QUALITY_AUDIO_PREF_TAG); + assertThat(box).isNull(); + } + +}