diff options
7 files changed, 457 insertions, 2 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java index 24ede164fbdd..db9f7b876789 100755 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java @@ -19,6 +19,8 @@ package com.android.settingslib.bluetooth; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; +import android.bluetooth.BluetoothCodecConfig; +import android.bluetooth.BluetoothCodecStatus; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; @@ -26,16 +28,22 @@ import android.content.Context; import android.os.ParcelUuid; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.R; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; -public final class A2dpProfile implements LocalBluetoothProfile { +public class A2dpProfile implements LocalBluetoothProfile { private static final String TAG = "A2dpProfile"; private static boolean V = false; + private Context mContext; + private BluetoothA2dp mService; + BluetoothA2dpWrapper.Factory mWrapperFactory; + private BluetoothA2dpWrapper mServiceWrapper; private boolean mIsProfileReady; private final LocalBluetoothAdapter mLocalAdapter; @@ -59,6 +67,7 @@ public final class A2dpProfile implements LocalBluetoothProfile { public void onServiceConnected(int profile, BluetoothProfile proxy) { if (V) Log.d(TAG,"Bluetooth service connected"); mService = (BluetoothA2dp) proxy; + mServiceWrapper = mWrapperFactory.getInstance(mService); // We just bound to the service, so refresh the UI for any connected A2DP devices. List<BluetoothDevice> deviceList = mService.getConnectedDevices(); while (!deviceList.isEmpty()) { @@ -88,11 +97,18 @@ public final class A2dpProfile implements LocalBluetoothProfile { A2dpProfile(Context context, LocalBluetoothAdapter adapter, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager) { + mContext = context; mLocalAdapter = adapter; mDeviceManager = deviceManager; mProfileManager = profileManager; mLocalAdapter.getProfileProxy(context, new A2dpServiceListener(), BluetoothProfile.A2DP); + mWrapperFactory = new BluetoothA2dpWrapperImpl.Factory(); + } + + @VisibleForTesting + void setWrapperFactory(BluetoothA2dpWrapper.Factory factory) { + mWrapperFactory = factory; } public boolean isConnectable() { @@ -173,6 +189,72 @@ public final class A2dpProfile implements LocalBluetoothProfile { return false; } + public boolean supportsHighQualityAudio(BluetoothDevice device) { + int support = mServiceWrapper.supportsOptionalCodecs(device); + return support == BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED; + } + + public boolean isHighQualityAudioEnabled(BluetoothDevice device) { + int enabled = mServiceWrapper.getOptionalCodecsEnabled(device); + if (enabled != BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN) { + return enabled == BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED; + } else if (getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED && + supportsHighQualityAudio(device)) { + // Since we don't have a stored preference and the device isn't connected, just return + // true since the default behavior when the device gets connected in the future would be + // to have optional codecs enabled. + return true; + } + BluetoothCodecConfig codecConfig = null; + if (mServiceWrapper.getCodecStatus() != null) { + codecConfig = mServiceWrapper.getCodecStatus().getCodecConfig(); + } + if (codecConfig != null) { + return !codecConfig.isMandatoryCodec(); + } else { + return false; + } + } + + public void setHighQualityAudioEnabled(BluetoothDevice device, boolean enabled) { + int prefValue = enabled + ? BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED + : BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED; + mServiceWrapper.setOptionalCodecsEnabled(device, prefValue); + if (getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED) { + return; + } + if (enabled) { + mService.enableOptionalCodecs(); + } else { + mService.disableOptionalCodecs(); + } + } + + public String getHighQualityAudioOptionLabel(BluetoothDevice device) { + int unknownCodecId = R.string.bluetooth_profile_a2dp_high_quality_unknown_codec; + if (!supportsHighQualityAudio(device) || + getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED) { + return mContext.getString(unknownCodecId); + } + // We want to get the highest priority codec, since that's the one that will be used with + // this device, and see if it is high-quality (ie non-mandatory). + BluetoothCodecConfig[] selectable = null; + if (mServiceWrapper.getCodecStatus() != null) { + selectable = mServiceWrapper.getCodecStatus().getCodecsSelectableCapabilities(); + // To get the highest priority, we sort in reverse. + Arrays.sort(selectable, + (a, b) -> { + return b.getCodecPriority() - a.getCodecPriority(); + }); + } + if (selectable == null || selectable.length < 1 || selectable[0].isMandatoryCodec()) { + return mContext.getString(unknownCodecId); + } + return mContext.getString(R.string.bluetooth_profile_a2dp_high_quality, + selectable[0].getCodecName()); + } + public String toString() { return NAME; } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothA2dpWrapper.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothA2dpWrapper.java new file mode 100644 index 000000000000..aa3e8356739a --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothA2dpWrapper.java @@ -0,0 +1,58 @@ +/* + * 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.settingslib.bluetooth; + +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothCodecStatus; +import android.bluetooth.BluetoothDevice; + +/** + * This interface replicates some methods of android.bluetooth.BluetoothA2dp that are new and not + * yet available in our current version of Robolectric. It provides a thin wrapper to call the real + * methods in production and a mock in tests. + */ +public interface BluetoothA2dpWrapper { + + static interface Factory { + BluetoothA2dpWrapper getInstance(BluetoothA2dp service); + } + + /** + * @return the real {@code BluetoothA2dp} object + */ + BluetoothA2dp getService(); + + /** + * Wraps {@code BluetoothA2dp.getCodecStatus} + */ + public BluetoothCodecStatus getCodecStatus(); + + /** + * Wraps {@code BluetoothA2dp.supportsOptionalCodecs} + */ + int supportsOptionalCodecs(BluetoothDevice device); + + /** + * Wraps {@code BluetoothA2dp.getOptionalCodecsEnabled} + */ + int getOptionalCodecsEnabled(BluetoothDevice device); + + /** + * Wraps {@code BluetoothA2dp.setOptionalCodecsEnabled} + */ + void setOptionalCodecsEnabled(BluetoothDevice device, int value); +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothA2dpWrapperImpl.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothA2dpWrapperImpl.java new file mode 100644 index 000000000000..14fa7966eab1 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothA2dpWrapperImpl.java @@ -0,0 +1,62 @@ +/* + * 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.settingslib.bluetooth; + +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothCodecStatus; +import android.bluetooth.BluetoothDevice; + +public class BluetoothA2dpWrapperImpl implements BluetoothA2dpWrapper { + + public static class Factory implements BluetoothA2dpWrapper.Factory { + @Override + public BluetoothA2dpWrapper getInstance(BluetoothA2dp service) { + return new BluetoothA2dpWrapperImpl(service); + } + } + + private BluetoothA2dp mService; + + public BluetoothA2dpWrapperImpl(BluetoothA2dp service) { + mService = service; + } + + @Override + public BluetoothA2dp getService() { + return mService; + } + + @Override + public BluetoothCodecStatus getCodecStatus() { + return mService.getCodecStatus(); + } + + @Override + public int supportsOptionalCodecs(BluetoothDevice device) { + return mService.supportsOptionalCodecs(device); + } + + @Override + public int getOptionalCodecsEnabled(BluetoothDevice device) { + return mService.getOptionalCodecsEnabled(device); + } + + @Override + public void setOptionalCodecsEnabled(BluetoothDevice device, int value) { + mService.setOptionalCodecsEnabled(device, value); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java index 5a9a7496fa4f..0750dc750051 100755 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java @@ -42,7 +42,7 @@ import java.util.Map; * LocalBluetoothProfileManager provides access to the LocalBluetoothProfile * objects for the available Bluetooth profiles. */ -public final class LocalBluetoothProfileManager { +public class LocalBluetoothProfileManager { private static final String TAG = "LocalBluetoothProfileManager"; private static final boolean DEBUG = Utils.D; /** Singleton instance. */ diff --git a/packages/SettingsLib/tests/robotests/src/android/bluetooth/BluetoothCodecConfig.java b/packages/SettingsLib/tests/robotests/src/android/bluetooth/BluetoothCodecConfig.java new file mode 100644 index 000000000000..656ab86e6987 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/android/bluetooth/BluetoothCodecConfig.java @@ -0,0 +1,25 @@ +/* + * 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 { + public boolean isMandatoryCodec() { return true; } + public String getCodecName() { return null;} +} diff --git a/packages/SettingsLib/tests/robotests/src/android/bluetooth/BluetoothCodecStatus.java b/packages/SettingsLib/tests/robotests/src/android/bluetooth/BluetoothCodecStatus.java new file mode 100644 index 000000000000..919ec3f3aa10 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/android/bluetooth/BluetoothCodecStatus.java @@ -0,0 +1,25 @@ +/* + * 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 BluetoothCodecStatus { + public BluetoothCodecConfig getCodecConfig() { return null; } + public BluetoothCodecConfig[] getCodecsSelectableCapabilities() { return null; } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/A2dpProfileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/A2dpProfileTest.java new file mode 100644 index 000000000000..07a0e11dc063 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/A2dpProfileTest.java @@ -0,0 +1,203 @@ +/* + * 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.settingslib.bluetooth; + +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothCodecConfig; +import android.bluetooth.BluetoothCodecStatus; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; + +import com.android.settingslib.R; +import com.android.settingslib.TestConfig; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +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.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class A2dpProfileTest { + + @Mock Context mContext; + @Mock LocalBluetoothAdapter mAdapter; + @Mock CachedBluetoothDeviceManager mDeviceManager; + @Mock LocalBluetoothProfileManager mProfileManager; + @Mock BluetoothDevice mDevice; + @Mock BluetoothA2dp mBluetoothA2dp; + @Mock BluetoothA2dpWrapper mBluetoothA2dpWrapper; + BluetoothProfile.ServiceListener mServiceListener; + + A2dpProfile mProfile; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + // Capture the A2dpServiceListener our A2dpProfile will pass during its constructor, so that + // we can call its onServiceConnected method and get it to use our mock BluetoothA2dp + // object. + doAnswer((invocation) -> { + mServiceListener = (BluetoothProfile.ServiceListener) invocation.getArguments()[1]; + return null; + }).when(mAdapter).getProfileProxy(any(Context.class), any(), eq(BluetoothProfile.A2DP)); + + mProfile = new A2dpProfile(mContext, mAdapter, mDeviceManager, mProfileManager); + mProfile.setWrapperFactory((service) -> { return mBluetoothA2dpWrapper; }); + mServiceListener.onServiceConnected(BluetoothProfile.A2DP, mBluetoothA2dp); + } + + @Test + public void supportsHighQualityAudio() { + when(mBluetoothA2dpWrapper.supportsOptionalCodecs(any())).thenReturn( + BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED); + assertThat(mProfile.supportsHighQualityAudio(mDevice)).isTrue(); + + when(mBluetoothA2dpWrapper.supportsOptionalCodecs(any())).thenReturn( + BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED); + assertThat(mProfile.supportsHighQualityAudio(mDevice)).isFalse(); + + when(mBluetoothA2dpWrapper.supportsOptionalCodecs(any())).thenReturn( + BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN); + assertThat(mProfile.supportsHighQualityAudio(mDevice)).isFalse(); + } + + @Test + public void isHighQualityAudioEnabled() { + when(mBluetoothA2dpWrapper.getOptionalCodecsEnabled(any())).thenReturn( + BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED); + assertThat(mProfile.isHighQualityAudioEnabled(mDevice)).isTrue(); + + when(mBluetoothA2dpWrapper.getOptionalCodecsEnabled(any())).thenReturn( + BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED); + assertThat(mProfile.isHighQualityAudioEnabled(mDevice)).isFalse(); + + // If we don't have a stored pref for whether optional codecs should be enabled or not, + // then isHighQualityAudioEnabled() should return true or false based on whether optional + // codecs are supported. If the device is connected then we should ask it directly, but if + // the device isn't connected then rely on the stored pref about such support. + when(mBluetoothA2dpWrapper.getOptionalCodecsEnabled(any())).thenReturn( + BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN); + when(mBluetoothA2dp.getConnectionState(any())).thenReturn( + BluetoothProfile.STATE_DISCONNECTED); + + when(mBluetoothA2dpWrapper.supportsOptionalCodecs(any())).thenReturn( + BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED); + assertThat(mProfile.isHighQualityAudioEnabled(mDevice)).isFalse(); + + when(mBluetoothA2dpWrapper.supportsOptionalCodecs(any())).thenReturn( + BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED); + assertThat(mProfile.isHighQualityAudioEnabled(mDevice)).isTrue(); + + when(mBluetoothA2dp.getConnectionState(any())).thenReturn( + BluetoothProfile.STATE_CONNECTED); + BluetoothCodecStatus status = mock(BluetoothCodecStatus.class); + when(mBluetoothA2dpWrapper.getCodecStatus()).thenReturn(status); + BluetoothCodecConfig config = mock(BluetoothCodecConfig.class); + when(status.getCodecConfig()).thenReturn(config); + when(config.isMandatoryCodec()).thenReturn(false); + assertThat(mProfile.isHighQualityAudioEnabled(mDevice)).isTrue(); + when(config.isMandatoryCodec()).thenReturn(true); + assertThat(mProfile.isHighQualityAudioEnabled(mDevice)).isFalse(); + } + + // Strings to use in fake resource lookups. + private static String KNOWN_CODEC_LABEL = "Use high quality audio: %1$s"; + private static String UNKNOWN_CODEC_LABEL = "Use high quality audio"; + + /** + * Helper for setting up several tests of getHighQualityAudioOptionLabel + */ + private void setupLabelTest() { + // SettingsLib doesn't have string resource lookup working for robotests, so fake our own + // string loading. + when(mContext.getString(eq(R.string.bluetooth_profile_a2dp_high_quality), + any(String.class))).thenAnswer((invocation) -> { + return String.format(KNOWN_CODEC_LABEL, invocation.getArguments()[1]); + }); + when(mContext.getString(eq(R.string.bluetooth_profile_a2dp_high_quality_unknown_codec))) + .thenReturn(UNKNOWN_CODEC_LABEL); + + // Most tests want to simulate optional codecs being supported by the device, so do that + // by default here. + when(mBluetoothA2dpWrapper.supportsOptionalCodecs(any())).thenReturn( + BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED); + } + + @Test + public void getLableCodecsNotSupported() { + setupLabelTest(); + when(mBluetoothA2dpWrapper.supportsOptionalCodecs(any())).thenReturn( + BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED); + assertThat(mProfile.getHighQualityAudioOptionLabel(mDevice)).isEqualTo(UNKNOWN_CODEC_LABEL); + } + + @Test + public void getLabelDeviceDisconnected() { + setupLabelTest(); + when(mBluetoothA2dp.getConnectionState(any())).thenReturn( + BluetoothProfile.STATE_DISCONNECTED); + assertThat(mProfile.getHighQualityAudioOptionLabel(mDevice)).isEqualTo(UNKNOWN_CODEC_LABEL); + } + + @Test + public void getLabelDeviceConnectedButNotHighQualityCodec() { + setupLabelTest(); + when(mBluetoothA2dp.getConnectionState(any())).thenReturn( + BluetoothProfile.STATE_CONNECTED); + BluetoothCodecStatus status = mock(BluetoothCodecStatus.class); + BluetoothCodecConfig config = mock(BluetoothCodecConfig.class); + BluetoothCodecConfig[] configs = {config}; + when(mBluetoothA2dpWrapper.getCodecStatus()).thenReturn(status); + when(status.getCodecsSelectableCapabilities()).thenReturn(configs); + + when(config.isMandatoryCodec()).thenReturn(true); + assertThat(mProfile.getHighQualityAudioOptionLabel(mDevice)).isEqualTo(UNKNOWN_CODEC_LABEL); + } + + @Test + public void getLabelDeviceConnectedWithHighQualityCodec() { + setupLabelTest(); + when(mBluetoothA2dp.getConnectionState(any())).thenReturn( + BluetoothProfile.STATE_CONNECTED); + BluetoothCodecStatus status = mock(BluetoothCodecStatus.class); + BluetoothCodecConfig config = mock(BluetoothCodecConfig.class); + BluetoothCodecConfig[] configs = {config}; + when(mBluetoothA2dpWrapper.getCodecStatus()).thenReturn(status); + when(status.getCodecsSelectableCapabilities()).thenReturn(configs); + + when(config.isMandatoryCodec()).thenReturn(false); + when(config.getCodecName()).thenReturn("PiedPiper"); + assertThat(mProfile.getHighQualityAudioOptionLabel(mDevice)).isEqualTo( + String.format(KNOWN_CODEC_LABEL, config.getCodecName())); + } +} |