android:order="-180"
settings:controller="com.android.settings.notification.MediaVolumePreferenceController"/>
+ <!-- Media output switcher -->
+ <ListPreference
+ android:key="media_output"
+ android:title="@string/media_output_title"
+ android:dialogTitle="@string/media_output_title"
+ android:order="-175"
+ settings:controller="com.android.settings.sound.MediaOutputPreferenceController"/>
+
<!-- Ring volume -->
<com.android.settings.notification.VolumeSeekBarPreference
android:key="ring_volume"
android:title="@string/vibrate_when_ringing_title"
android:order="-160"/>
+ <!-- Hands free profile output switcher -->
+ <ListPreference
+ android:key="take_call_on_output"
+ android:title="@string/take_call_on_title"
+ android:dialogTitle="@string/take_call_on_title"
+ android:order="-155"
+ settings:controller="com.android.settings.sound.HandsFreeProfileOutputPreferenceController"/>
+
<!-- Alarm volume -->
<com.android.settings.notification.VolumeSeekBarPreference
android:key="alarm_volume"
public static final String ABOUT_PHONE_V2 = "settings_about_phone_v2";
public static final String BLUETOOTH_WHILE_DRIVING = "settings_bluetooth_while_driving";
public static final String DATA_USAGE_SETTINGS_V2 = "settings_data_usage_v2";
+ public static final String AUDIO_SWITCHER_SETTINGS = "settings_audio_switcher";
}
--- /dev/null
+/*
+ * Copyright (C) 2018 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.sound;
+
+
+import static android.media.AudioManager.STREAM_DEVICES_CHANGED_ACTION;
+import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioDeviceCallback;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.MediaRouter;
+import android.media.MediaRouter.Callback;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.v7.preference.ListPreference;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
+import android.text.TextUtils;
+import android.util.FeatureFlagUtils;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.core.FeatureFlags;
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnStart;
+import com.android.settingslib.core.lifecycle.events.OnStop;
+
+import java.util.List;
+
+/**
+ * Abstract class for audio switcher controller to notify subclass
+ * updating the current status of switcher entry. Subclasses must overwrite
+ * {@link #setActiveBluetoothDevice(BluetoothDevice)} to set the
+ * active device for corresponding profile.
+ */
+public abstract class AudioSwitchPreferenceController extends BasePreferenceController
+ implements Preference.OnPreferenceChangeListener, BluetoothCallback,
+ LifecycleObserver, OnStart, OnStop {
+
+ private static final int INVALID_INDEX = -1;
+
+ protected final AudioManager mAudioManager;
+ protected final MediaRouter mMediaRouter;
+ protected final LocalBluetoothProfileManager mProfileManager;
+ protected int mSelectedIndex;
+ protected Preference mPreference;
+ protected List<BluetoothDevice> mConnectedDevices;
+
+ private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback;
+ private final LocalBluetoothManager mLocalBluetoothManager;
+ private final MediaRouterCallback mMediaRouterCallback;
+ private final WiredHeadsetBroadcastReceiver mReceiver;
+ private final Handler mHandler;
+
+ public AudioSwitchPreferenceController(Context context, String preferenceKey) {
+ super(context, preferenceKey);
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mMediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
+ mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
+ mLocalBluetoothManager.setForegroundActivity(context);
+ mProfileManager = mLocalBluetoothManager.getProfileManager();
+ mHandler = new Handler(Looper.getMainLooper());
+ mAudioManagerAudioDeviceCallback = new AudioManagerAudioDeviceCallback();
+ mReceiver = new WiredHeadsetBroadcastReceiver();
+ mMediaRouterCallback = new MediaRouterCallback();
+ }
+
+ /**
+ * Make this method as final, ensure that subclass will checking
+ * the feature flag and they could mistakenly break it via overriding.
+ */
+ @Override
+ public final int getAvailabilityStatus() {
+ return FeatureFlagUtils.isEnabled(mContext, FeatureFlags.AUDIO_SWITCHER_SETTINGS)
+ ? AVAILABLE : DISABLED_UNSUPPORTED;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ final String address = (String) newValue;
+ if (!(preference instanceof ListPreference)) {
+ return false;
+ }
+
+ final ListPreference listPreference = (ListPreference) preference;
+ if (TextUtils.equals(address, mContext.getText(R.string.media_output_default_summary))) {
+ // Switch to default device which address is device name
+ mSelectedIndex = getDefaultDeviceIndex();
+ setActiveBluetoothDevice(null);
+ listPreference.setSummary(mContext.getText(R.string.media_output_default_summary));
+ } else {
+ // Switch to BT device which address is hardware address
+ final int connectedDeviceIndex = getConnectedDeviceIndex(address);
+ if (connectedDeviceIndex == INVALID_INDEX) {
+ return false;
+ }
+ final BluetoothDevice btDevice = mConnectedDevices.get(connectedDeviceIndex);
+ mSelectedIndex = connectedDeviceIndex;
+ setActiveBluetoothDevice(btDevice);
+ listPreference.setSummary(btDevice.getName());
+ }
+ return true;
+ }
+
+ public abstract void setActiveBluetoothDevice(BluetoothDevice device);
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mPreference = screen.findPreference(mPreferenceKey);
+ }
+
+ @Override
+ public void onStart() {
+ register();
+ }
+
+ @Override
+ public void onStop() {
+ unregister();
+ }
+
+ /**
+ * Only concerned about whether the local adapter is connected to any profile of any device and
+ * are not really concerned about which profile.
+ */
+ @Override
+ public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {
+ updateState(mPreference);
+ }
+
+ @Override
+ public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {
+ updateState(mPreference);
+ }
+
+ @Override
+ public void onAudioModeChanged() {
+ updateState(mPreference);
+ }
+
+ @Override
+ public void onBluetoothStateChanged(int bluetoothState) {
+ }
+
+ /**
+ * The local Bluetooth adapter has started the remote device discovery process.
+ */
+ @Override
+ public void onScanningStateChanged(boolean started) {
+ }
+
+ /**
+ * Indicates a change in the bond state of a remote
+ * device. For example, if a device is bonded (paired).
+ */
+ @Override
+ public void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
+ updateState(mPreference);
+ }
+
+ @Override
+ public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {
+ }
+
+ @Override
+ public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
+ }
+
+ protected boolean isOngoingCallStatus() {
+ int audioMode = mAudioManager.getMode();
+ return audioMode == AudioManager.MODE_RINGTONE
+ || audioMode == AudioManager.MODE_IN_CALL
+ || audioMode == AudioManager.MODE_IN_COMMUNICATION;
+ }
+
+ int getDefaultDeviceIndex() {
+ // Default device is after all connected devices.
+ return ArrayUtils.size(mConnectedDevices);
+ }
+
+ void setupPreferenceEntries(CharSequence[] mediaOutputs, CharSequence[] mediaValues,
+ BluetoothDevice activeDevice) {
+ // default to current device
+ mSelectedIndex = getDefaultDeviceIndex();
+ // default device is after all connected devices.
+ mediaOutputs[mSelectedIndex] = mContext.getText(R.string.media_output_default_summary);
+ // use default device name as address
+ mediaValues[mSelectedIndex] = mContext.getText(R.string.media_output_default_summary);
+ for (int i = 0, size = mConnectedDevices.size(); i < size; i++) {
+ final BluetoothDevice btDevice = mConnectedDevices.get(i);
+ mediaOutputs[i] = btDevice.getName();
+ mediaValues[i] = btDevice.getAddress();
+ if (btDevice.equals(activeDevice)) {
+ // select the active connected device.
+ mSelectedIndex = i;
+ }
+ }
+ }
+
+ void setPreference(CharSequence[] mediaOutputs, CharSequence[] mediaValues,
+ Preference preference) {
+ final ListPreference listPreference = (ListPreference) preference;
+ listPreference.setEntries(mediaOutputs);
+ listPreference.setEntryValues(mediaValues);
+ listPreference.setValueIndex(mSelectedIndex);
+ listPreference.setSummary(mediaOutputs[mSelectedIndex]);
+ }
+
+ private int getConnectedDeviceIndex(String hardwareAddress) {
+ if (mConnectedDevices != null) {
+ for (int i = 0, size = mConnectedDevices.size(); i < size; i++) {
+ final BluetoothDevice btDevice = mConnectedDevices.get(i);
+ if (TextUtils.equals(btDevice.getAddress(), hardwareAddress)) {
+ return i;
+ }
+ }
+ }
+ return INVALID_INDEX;
+ }
+
+ private void register() {
+ mLocalBluetoothManager.getEventManager().registerCallback(this);
+ mAudioManager.registerAudioDeviceCallback(mAudioManagerAudioDeviceCallback, mHandler);
+ mMediaRouter.addCallback(ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback);
+
+ // Register for misc other intent broadcasts.
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
+ intentFilter.addAction(STREAM_DEVICES_CHANGED_ACTION);
+ mContext.registerReceiver(mReceiver, intentFilter);
+ }
+
+ private void unregister() {
+ mLocalBluetoothManager.getEventManager().unregisterCallback(this);
+ mAudioManager.unregisterAudioDeviceCallback(mAudioManagerAudioDeviceCallback);
+ mMediaRouter.removeCallback(mMediaRouterCallback);
+ mContext.unregisterReceiver(mReceiver);
+ }
+
+ /** Callback for headset plugged and unplugged events. */
+ private class AudioManagerAudioDeviceCallback extends AudioDeviceCallback {
+ @Override
+ public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
+ updateState(mPreference);
+ }
+
+ @Override
+ public void onAudioDevicesRemoved(AudioDeviceInfo[] devices) {
+ updateState(mPreference);
+ }
+ }
+
+ /** Receiver for wired headset plugged and unplugged events. */
+ private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (AudioManager.ACTION_HEADSET_PLUG.equals(action) ||
+ AudioManager.STREAM_DEVICES_CHANGED_ACTION.equals(action)) {
+ updateState(mPreference);
+ }
+ }
+ }
+
+ /** Callback for cast device events. */
+ private class MediaRouterCallback extends Callback {
+ @Override
+ public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info) {
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouter router, int type, MediaRouter.RouteInfo info) {
+ }
+
+ @Override
+ public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+ if (info != null && !info.isDefault()) {
+ // cast mode
+ updateState(mPreference);
+ }
+ }
+
+ @Override
+ public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+ if (info != null && !info.isDefault()) {
+ // cast mode
+ updateState(mPreference);
+ }
+ }
+
+ @Override
+ public void onRouteGrouped(MediaRouter router, MediaRouter.RouteInfo info,
+ MediaRouter.RouteGroup group, int index) {
+ }
+
+ @Override
+ public void onRouteUngrouped(MediaRouter router, MediaRouter.RouteInfo info,
+ MediaRouter.RouteGroup group) {
+ }
+
+ @Override
+ public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2018 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.sound;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.support.v7.preference.Preference;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.settings.R;
+import com.android.settingslib.bluetooth.HeadsetProfile;
+
+/**
+ * This class allows switching between HFP-connected BT devices
+ * while in on-call state.
+ */
+public class HandsFreeProfileOutputPreferenceController extends
+ AudioSwitchPreferenceController {
+
+ public HandsFreeProfileOutputPreferenceController(Context context, String key) {
+ super(context, key);
+ }
+
+ @Override
+ public void updateState(Preference preference) {
+ if (preference == null) {
+ // In case UI is not ready.
+ return;
+ }
+
+ if (!isOngoingCallStatus()) {
+ // Without phone call, disable the switch entry.
+ preference.setEnabled(false);
+ preference.setSummary(mContext.getText(R.string.media_output_default_summary));
+ return;
+ }
+
+ // Ongoing call status, list all the connected devices support hands free profile.
+ // Select current active device.
+ // Disable switch entry if there is no connected device.
+ mConnectedDevices = null;
+ BluetoothDevice activeDevice = null;
+
+ final HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
+ if (headsetProfile != null) {
+ mConnectedDevices = headsetProfile.getConnectedDevices();
+ activeDevice = headsetProfile.getActiveDevice();
+ }
+
+ final int numDevices = ArrayUtils.size(mConnectedDevices);
+ if (numDevices == 0) {
+ // No connected devices, disable switch entry.
+ preference.setEnabled(false);
+ preference.setSummary(mContext.getText(R.string.media_output_default_summary));
+ return;
+ }
+
+ preference.setEnabled(true);
+ CharSequence[] mediaOutputs = new CharSequence[numDevices + 1];
+ CharSequence[] mediaValues = new CharSequence[numDevices + 1];
+
+ // Setup devices entries, select active connected device
+ setupPreferenceEntries(mediaOutputs, mediaValues, activeDevice);
+
+ if (mAudioManager.isWiredHeadsetOn() && !mAudioManager.isBluetoothScoOn()) {
+ // If wired headset is plugged in and active, select to default device.
+ mSelectedIndex = getDefaultDeviceIndex();
+ }
+
+ // Display connected devices, default device and show the active device
+ setPreference(mediaOutputs, mediaValues, preference);
+ }
+
+ @Override
+ public void setActiveBluetoothDevice(BluetoothDevice device) {
+ if (isOngoingCallStatus()) {
+ mProfileManager.getHeadsetProfile().setActiveDevice(device);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2018 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.sound;
+
+import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.MediaRouter;
+import android.support.v7.preference.Preference;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.settings.R;
+import com.android.settingslib.bluetooth.A2dpProfile;
+
+
+/**
+ * This class which allows switching between a2dp-connected BT devices.
+ * A few conditions will disable this switcher:
+ * - No available BT device(s)
+ * - Media stream captured by cast device
+ * - During a call.
+ */
+public class MediaOutputPreferenceController extends AudioSwitchPreferenceController {
+
+ public MediaOutputPreferenceController(Context context, String key) {
+ super(context, key);
+ }
+
+ @Override
+ public void updateState(Preference preference) {
+ if (preference == null) {
+ // In case UI is not ready.
+ return;
+ }
+
+ if (mAudioManager.isMusicActiveRemotely() || isCastDevice(mMediaRouter)) {
+ // TODO(76455906): Workaround for cast mode, need a solid way to identify cast mode.
+ // In cast mode, disable switch entry.
+ preference.setEnabled(false);
+ preference.setSummary(mContext.getText(R.string.media_output_summary_unavailable));
+ return;
+ }
+
+ if (isOngoingCallStatus()) {
+ // Ongoing call status, switch entry for media will be disabled.
+ preference.setEnabled(false);
+ preference.setSummary(
+ mContext.getText(R.string.media_out_summary_ongoing_call_state));
+ return;
+ }
+
+ // Otherwise, list all of the A2DP connected device and display the active device.
+ mConnectedDevices = null;
+ BluetoothDevice activeDevice = null;
+ if (mAudioManager.getMode() == AudioManager.MODE_NORMAL) {
+ final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
+ if (a2dpProfile != null) {
+ mConnectedDevices = a2dpProfile.getConnectedDevices();
+ activeDevice = a2dpProfile.getActiveDevice();
+ }
+ }
+
+ final int numDevices = ArrayUtils.size(mConnectedDevices);
+ if (numDevices == 0) {
+ // Disable switch entry if there is no connected devices.
+ preference.setEnabled(false);
+ preference.setSummary(mContext.getText(R.string.media_output_default_summary));
+ return;
+ }
+
+ preference.setEnabled(true);
+ CharSequence[] mediaOutputs = new CharSequence[numDevices + 1];
+ CharSequence[] mediaValues = new CharSequence[numDevices + 1];
+
+ // Setup devices entries, select active connected device
+ setupPreferenceEntries(mediaOutputs, mediaValues, activeDevice);
+
+ if (mAudioManager.isWiredHeadsetOn() && !mAudioManager.isBluetoothA2dpOn()) {
+ // If wired headset is plugged in and active, select to default device.
+ mSelectedIndex = getDefaultDeviceIndex();
+ }
+
+ // Display connected devices, default device and show the active device
+ setPreference(mediaOutputs, mediaValues, preference);
+ }
+
+ @Override
+ public void setActiveBluetoothDevice(BluetoothDevice device) {
+ if (mAudioManager.getMode() == AudioManager.MODE_NORMAL) {
+ mProfileManager.getA2dpProfile().setActiveDevice(device);
+ }
+ }
+
+ private static boolean isCastDevice(MediaRouter mediaRouter) {
+ final MediaRouter.RouteInfo selected = mediaRouter.getSelectedRoute(
+ ROUTE_TYPE_REMOTE_DISPLAY);
+ return selected != null && selected.getPresentationDisplay() != null
+ && selected.getPresentationDisplay().isValid();
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2018 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.sound;
+
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.DISABLED_UNSUPPORTED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.IntentFilter;
+import android.support.v7.preference.ListPreference;
+import android.support.v7.preference.PreferenceManager;
+import android.support.v7.preference.PreferenceScreen;
+import android.util.FeatureFlagUtils;
+
+import com.android.settings.R;
+import com.android.settings.core.FeatureFlags;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+import com.android.settings.testutils.shadow.ShadowAudioManager;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settings.testutils.shadow.ShadowMediaRouter;
+import com.android.settingslib.bluetooth.A2dpProfile;
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.BluetoothEventManager;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowBluetoothDevice;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(shadows = {
+ ShadowAudioManager.class,
+ ShadowMediaRouter.class,
+ ShadowBluetoothUtils.class,
+ ShadowBluetoothDevice.class}
+)
+public class AudioOutputSwitchPreferenceControllerTest {
+ private static final String TEST_KEY = "Test_Key";
+ private static final String TEST_DEVICE_NAME_1 = "Test_A2DP_BT_Device_NAME_1";
+ private static final String TEST_DEVICE_NAME_2 = "Test_A2DP_BT_Device_NAME_2";
+ private static final String TEST_DEVICE_ADDRESS_1 = "00:07:80:78:A4:69";
+ private static final String TEST_DEVICE_ADDRESS_2 = "00:00:00:00:00:00";
+
+ @Mock
+ private LocalBluetoothManager mLocalManager;
+ @Mock
+ private BluetoothEventManager mBluetoothEventManager;
+ @Mock
+ private CachedBluetoothDevice mCachedBluetoothDevice;
+ @Mock
+ private LocalBluetoothProfileManager mLocalBluetoothProfileManager;
+ @Mock
+ private A2dpProfile mA2dpProfile;
+
+ private Context mContext;
+ private PreferenceScreen mScreen;
+ private ListPreference mPreference;
+ private ShadowAudioManager mShadowAudioManager;
+ private ShadowMediaRouter mShadowMediaRouter;
+ private BluetoothManager mBluetoothManager;
+ private BluetoothAdapter mBluetoothAdapter;
+ private BluetoothDevice mBluetoothDevice;
+ private ShadowBluetoothDevice mShadowBluetoothDevice;
+ private LocalBluetoothManager mLocalBluetoothManager;
+ private AudioSwitchPreferenceController mController;
+ private List<BluetoothDevice> mConnectedDevices;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = spy(RuntimeEnvironment.application);
+
+ mShadowAudioManager = ShadowAudioManager.getShadow();
+ mShadowMediaRouter = ShadowMediaRouter.getShadow();
+
+ ShadowBluetoothUtils.sLocalBluetoothManager = mLocalManager;
+ mLocalBluetoothManager = ShadowBluetoothUtils.getLocalBtManager(mContext);
+
+ when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager);
+ when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager);
+ when(mLocalBluetoothProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile);
+
+ mBluetoothManager = new BluetoothManager(mContext);
+ mBluetoothAdapter = mBluetoothManager.getAdapter();
+
+ mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_1);
+ mShadowBluetoothDevice = Shadows.shadowOf(mBluetoothDevice);
+ mShadowBluetoothDevice.setName(TEST_DEVICE_NAME_1);
+ when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+
+ mController = new AudioSwitchPreferenceControllerTestable(mContext, TEST_KEY);
+ mScreen = spy(new PreferenceScreen(mContext, null));
+ mPreference = new ListPreference(mContext);
+ mConnectedDevices = new ArrayList<>(1);
+ mConnectedDevices.add(mBluetoothDevice);
+
+ when(mScreen.getPreferenceManager()).thenReturn(mock(PreferenceManager.class));
+ when(mScreen.getContext()).thenReturn(mContext);
+ when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
+ mScreen.addPreference(mPreference);
+ mController.displayPreference(mScreen);
+ }
+
+ @After
+ public void tearDown() {
+ mShadowAudioManager.reset();
+ mShadowMediaRouter.reset();
+ ShadowBluetoothUtils.reset();
+ }
+
+ @Test
+ public void getAvailabilityStatus_byDefault_isAvailable() {
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+ }
+
+ @Test
+ public void getAvailabilityStatus_whenNotVisible_isDisable() {
+ FeatureFlagUtils.setEnabled(mContext, FeatureFlags.AUDIO_SWITCHER_SETTINGS, false);
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(DISABLED_UNSUPPORTED);
+ }
+
+ @Test
+ public void onStart_shouldRegisterCallbackAndRegisterReceiver() {
+ mController.onStart();
+
+ verify(mLocalBluetoothManager.getEventManager()).registerCallback(
+ any(BluetoothCallback.class));
+ verify(mContext).registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class));
+ }
+
+ @Test
+ public void onStop_shouldUnregisterCallbackAndUnregisterReceiver() {
+ mController.onStart();
+ mController.onStop();
+
+ verify(mLocalBluetoothManager.getEventManager()).unregisterCallback(
+ any(BluetoothCallback.class));
+ verify(mContext).unregisterReceiver(any(BroadcastReceiver.class));
+ }
+
+ @Test
+ public void onPreferenceChange_toThisDevice_shouldSetDefaultSummary() {
+ mController.mConnectedDevices = mConnectedDevices;
+
+ mController.onPreferenceChange(mPreference,
+ mContext.getText(R.string.media_output_default_summary));
+
+ assertThat(mPreference.getSummary()).isEqualTo(
+ mContext.getText(R.string.media_output_default_summary));
+ }
+
+ /**
+ * One Bluetooth devices are available, and select the device.
+ * Preference summary should be device name.
+ */
+ @Test
+ public void onPreferenceChange_toBtDevice_shouldSetBtDeviceName() {
+ mController.mConnectedDevices = mConnectedDevices;
+
+ mController.onPreferenceChange(mPreference, TEST_DEVICE_ADDRESS_1);
+
+ assertThat(mPreference.getSummary()).isEqualTo(mBluetoothDevice.getName());
+ }
+
+ /**
+ * More than one Bluetooth devices are available, and select second device.
+ * Preference summary should be second device name.
+ */
+ @Test
+ public void onPreferenceChange_toBtDevices_shouldSetSecondBtDeviceName() {
+ ShadowBluetoothDevice shadowBluetoothDevice;
+ BluetoothDevice secondBluetoothDevice;
+ secondBluetoothDevice = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_2);
+ shadowBluetoothDevice = Shadows.shadowOf(secondBluetoothDevice);
+ shadowBluetoothDevice.setName(TEST_DEVICE_NAME_2);
+ List<BluetoothDevice> connectedDevices = new ArrayList<>(2);
+ connectedDevices.add(mBluetoothDevice);
+ connectedDevices.add(secondBluetoothDevice);
+ mController.mConnectedDevices = connectedDevices;
+
+ mController.onPreferenceChange(mPreference, TEST_DEVICE_ADDRESS_2);
+
+ assertThat(mPreference.getSummary()).isEqualTo(secondBluetoothDevice.getName());
+ }
+
+ /**
+ * mConnectedDevices is Null.
+ * onPreferenceChange should return false.
+ */
+ @Test
+ public void onPreferenceChange_connectedDeviceIsNull_shouldReturnFalse() {
+ mController.mConnectedDevices = null;
+
+ assertThat(mController.onPreferenceChange(mPreference, TEST_DEVICE_ADDRESS_1)).isFalse();
+ }
+
+ private class AudioSwitchPreferenceControllerTestable extends
+ AudioSwitchPreferenceController {
+ AudioSwitchPreferenceControllerTestable(Context context, String key) {
+ super(context, key);
+ }
+
+ @Override
+ public void setActiveBluetoothDevice(BluetoothDevice device) {
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return null;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2018 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.sound;
+
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.media.AudioManager;
+import android.support.v7.preference.ListPreference;
+import android.support.v7.preference.PreferenceManager;
+import android.support.v7.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+import com.android.settings.testutils.shadow.ShadowAudioManager;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settings.testutils.shadow.ShadowMediaRouter;
+import com.android.settingslib.bluetooth.BluetoothEventManager;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.HeadsetProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowBluetoothDevice;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(shadows = {
+ ShadowAudioManager.class,
+ ShadowMediaRouter.class,
+ ShadowBluetoothUtils.class,
+ ShadowBluetoothDevice.class}
+)
+public class HandsFreeProfileOutputPreferenceControllerTest {
+ private static final String TEST_KEY = "Test_Key";
+ private static final String TEST_DEVICE_NAME_1 = "Test_HAP_BT_Device_NAME_1";
+ private static final String TEST_DEVICE_NAME_2 = "Test_HAP_BT_Device_NAME_2";
+ private static final String TEST_DEVICE_ADDRESS_1 = "00:07:80:78:A4:69";
+ private static final String TEST_DEVICE_ADDRESS_2 = "00:00:00:00:00:00";
+
+ @Mock
+ private LocalBluetoothManager mLocalManager;
+ @Mock
+ private BluetoothEventManager mBluetoothEventManager;
+ @Mock
+ private CachedBluetoothDevice mCachedBluetoothDevice;
+ @Mock
+ private LocalBluetoothProfileManager mLocalBluetoothProfileManager;
+ @Mock
+ private HeadsetProfile mHeadsetProfile;
+
+ private Context mContext;
+ private PreferenceScreen mScreen;
+ private ListPreference mPreference;
+ private ShadowAudioManager mShadowAudioManager;
+ private ShadowMediaRouter mShadowMediaRouter;
+ private BluetoothManager mBluetoothManager;
+ private BluetoothAdapter mBluetoothAdapter;
+ private BluetoothDevice mBluetoothDevice;
+ private ShadowBluetoothDevice mShadowBluetoothDevice;
+ private LocalBluetoothManager mLocalBluetoothManager;
+ private AudioSwitchPreferenceController mController;
+ private List<BluetoothDevice> mConnectedDevices;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = spy(RuntimeEnvironment.application);
+
+ mShadowAudioManager = ShadowAudioManager.getShadow();
+ mShadowMediaRouter = ShadowMediaRouter.getShadow();
+
+ ShadowBluetoothUtils.sLocalBluetoothManager = mLocalManager;
+ mLocalBluetoothManager = ShadowBluetoothUtils.getLocalBtManager(mContext);
+
+ when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager);
+ when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager);
+ when(mLocalBluetoothProfileManager.getHeadsetProfile()).thenReturn(mHeadsetProfile);
+
+ mBluetoothManager = new BluetoothManager(mContext);
+ mBluetoothAdapter = mBluetoothManager.getAdapter();
+ mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_1);
+ mShadowBluetoothDevice = Shadows.shadowOf(mBluetoothDevice);
+ mShadowBluetoothDevice.setName(TEST_DEVICE_NAME_1);
+ when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+
+ mController = new HandsFreeProfileOutputPreferenceController(mContext, TEST_KEY);
+ mScreen = spy(new PreferenceScreen(mContext, null));
+ mPreference = new ListPreference(mContext);
+ mConnectedDevices = new ArrayList<>(1);
+ mConnectedDevices.add(mBluetoothDevice);
+
+ when(mScreen.getPreferenceManager()).thenReturn(mock(PreferenceManager.class));
+ when(mScreen.getContext()).thenReturn(mContext);
+ when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
+ mScreen.addPreference(mPreference);
+ mController.displayPreference(mScreen);
+ }
+
+ @After
+ public void tearDown() {
+ mShadowAudioManager.reset();
+ mShadowMediaRouter.reset();
+ ShadowBluetoothUtils.reset();
+ }
+
+ @Test
+ public void setActiveBluetoothDevice_duringACalling_shouldSetBtDeviceActive() {
+ mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+
+ mController.setActiveBluetoothDevice(mBluetoothDevice);
+
+ verify(mHeadsetProfile).setActiveDevice(mBluetoothDevice);
+ }
+
+ @Test
+ public void updateState_shouldSetSummary() {
+ mController.updateState(mPreference);
+
+ assertThat(mPreference.getSummary()).isEqualTo(
+ mContext.getText(R.string.media_output_default_summary));
+ }
+
+ /**
+ * One Headset Bluetooth device is available and activated
+ * Preference should be enabled
+ * Preference summary should be activate device name
+ */
+ @Test
+ public void updateState_oneHeadsetsAvailableAndActivated_shouldSetDeviceName() {
+ mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+ when(mHeadsetProfile.getConnectedDevices()).thenReturn(mConnectedDevices);
+ when(mHeadsetProfile.getActiveDevice()).thenReturn(mBluetoothDevice);
+
+ mController.updateState(mPreference);
+
+ assertThat(mPreference.isEnabled()).isTrue();
+ assertThat(mPreference.getSummary()).isEqualTo(mBluetoothDevice.getName());
+ }
+
+ /**
+ * More than one Headset Bluetooth devices are available, and second device is active.
+ * Preference should be enabled
+ * Preference summary should be activate device name
+ */
+ @Test
+ public void updateState_moreThanOneHapBtDevicesAreAvailable_shouldSetActivatedDeviceName() {
+ ShadowBluetoothDevice shadowBluetoothDevice;
+ mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+ BluetoothDevice secondBluetoothDevice;
+ secondBluetoothDevice = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_2);
+ shadowBluetoothDevice = Shadows.shadowOf(secondBluetoothDevice);
+ shadowBluetoothDevice.setName(TEST_DEVICE_NAME_2);
+ List<BluetoothDevice> connectedDevices = new ArrayList<>(2);
+ connectedDevices.add(mBluetoothDevice);
+ connectedDevices.add(secondBluetoothDevice);
+
+ when(mHeadsetProfile.getConnectedDevices()).thenReturn(connectedDevices);
+ when(mHeadsetProfile.getActiveDevice()).thenReturn(secondBluetoothDevice);
+
+ mController.updateState(mPreference);
+
+ assertThat(mPreference.isEnabled()).isTrue();
+ assertThat(mPreference.getSummary()).isEqualTo(secondBluetoothDevice.getName());
+ }
+
+ /**
+ * Hands Free Profile Bluetooth device(s) are available, but wired headset is plugged in
+ * and activated.
+ * Preference should be enabled
+ * Preference summary should be "This device"
+ */
+ @Test
+ public void hapBtDevicesAreAvailableButWiredHeadsetIsActivated_shouldSetDefaultSummary() {
+ mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+ mShadowAudioManager.setWiredHeadsetOn(true);
+ mShadowAudioManager.setBluetoothScoOn(false);
+ when(mHeadsetProfile.getConnectedDevices()).thenReturn(mConnectedDevices);
+ when(mHeadsetProfile.getActiveDevice()).thenReturn(
+ mBluetoothDevice); // BT device is still activated in this case
+
+ mController.updateState(mPreference);
+
+ assertThat(mPreference.isEnabled()).isTrue();
+ assertThat(mPreference.getSummary()).isEqualTo(
+ mContext.getText(R.string.media_output_default_summary));
+ }
+
+ /**
+ * No available Headset BT devices
+ * Preference should be disabled
+ * Preference summary should be "This device"
+ */
+ @Test
+ public void noAvailableHeadsetBtDevices_preferenceEnableIsFalse_shouldSetDefaultSummary() {
+ mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+ List<BluetoothDevice> emptyDeviceList = new ArrayList<>();
+ when(mHeadsetProfile.getConnectedDevices()).thenReturn(emptyDeviceList);
+
+ mController.updateState(mPreference);
+
+ assertThat(mPreference.isEnabled()).isFalse();
+ assertThat(mPreference.getSummary()).isEqualTo(
+ mContext.getText(R.string.media_output_default_summary));
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2018 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.sound;
+
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.media.AudioManager;
+import android.support.v7.preference.ListPreference;
+import android.support.v7.preference.PreferenceManager;
+import android.support.v7.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+import com.android.settings.testutils.shadow.ShadowAudioManager;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settings.testutils.shadow.ShadowMediaRouter;
+import com.android.settingslib.bluetooth.A2dpProfile;
+import com.android.settingslib.bluetooth.BluetoothEventManager;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowBluetoothDevice;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(shadows = {
+ ShadowAudioManager.class,
+ ShadowMediaRouter.class,
+ ShadowBluetoothUtils.class,
+ ShadowBluetoothDevice.class}
+)
+public class MediaOutputPreferenceControllerTest {
+ private static final String TEST_KEY = "Test_Key";
+ private static final String TEST_DEVICE_NAME_1 = "Test_A2DP_BT_Device_NAME_1";
+ private static final String TEST_DEVICE_NAME_2 = "Test_A2DP_BT_Device_NAME_2";
+ private static final String TEST_DEVICE_ADDRESS_1 = "00:07:80:78:A4:69";
+ private static final String TEST_DEVICE_ADDRESS_2 = "00:00:00:00:00:00";
+
+ @Mock
+ private LocalBluetoothManager mLocalManager;
+ @Mock
+ private BluetoothEventManager mBluetoothEventManager;
+ @Mock
+ private CachedBluetoothDevice mCachedBluetoothDevice;
+ @Mock
+ private LocalBluetoothProfileManager mLocalBluetoothProfileManager;
+ @Mock
+ private A2dpProfile mA2dpProfile;
+
+ private Context mContext;
+ private PreferenceScreen mScreen;
+ private ListPreference mPreference;
+ private ShadowAudioManager mShadowAudioManager;
+ private ShadowMediaRouter mShadowMediaRouter;
+ private BluetoothManager mBluetoothManager;
+ private BluetoothAdapter mBluetoothAdapter;
+ private BluetoothDevice mBluetoothDevice;
+ private ShadowBluetoothDevice mShadowBluetoothDevice;
+ private LocalBluetoothManager mLocalBluetoothManager;
+ private AudioSwitchPreferenceController mController;
+ private List<BluetoothDevice> mConnectedDevices;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = spy(RuntimeEnvironment.application);
+
+ mShadowAudioManager = ShadowAudioManager.getShadow();
+ mShadowMediaRouter = ShadowMediaRouter.getShadow();
+
+ ShadowBluetoothUtils.sLocalBluetoothManager = mLocalManager;
+ mLocalBluetoothManager = ShadowBluetoothUtils.getLocalBtManager(mContext);
+
+ when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager);
+ when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager);
+ when(mLocalBluetoothProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile);
+
+ mBluetoothManager = new BluetoothManager(mContext);
+ mBluetoothAdapter = mBluetoothManager.getAdapter();
+ mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_1);
+ mShadowBluetoothDevice = Shadows.shadowOf(mBluetoothDevice);
+ mShadowBluetoothDevice.setName(TEST_DEVICE_NAME_1);
+ when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+
+ mController = new MediaOutputPreferenceController(mContext, TEST_KEY);
+ mScreen = spy(new PreferenceScreen(mContext, null));
+ mPreference = new ListPreference(mContext);
+ mConnectedDevices = new ArrayList<>(1);
+ mConnectedDevices.add(mBluetoothDevice);
+
+ when(mScreen.getPreferenceManager()).thenReturn(mock(PreferenceManager.class));
+ when(mScreen.getContext()).thenReturn(mContext);
+ when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
+ mScreen.addPreference(mPreference);
+ mController.displayPreference(mScreen);
+ }
+
+ @After
+ public void tearDown() {
+ mShadowAudioManager.reset();
+ mShadowMediaRouter.reset();
+ ShadowBluetoothUtils.reset();
+ }
+
+ @Test
+ public void setActiveBluetoothDevice_withoutRingAndCall_shouldSetBtDeviceActive() {
+ mShadowAudioManager.setMode(AudioManager.MODE_NORMAL);
+
+ mController.setActiveBluetoothDevice(mBluetoothDevice);
+
+ verify(mA2dpProfile).setActiveDevice(mBluetoothDevice);
+ }
+
+ @Test
+ public void updateState_shouldSetSummary() {
+ mController.updateState(mPreference);
+
+ assertThat(mPreference.getSummary()).isEqualTo(
+ mContext.getText(R.string.media_output_default_summary));
+ }
+
+ /**
+ * On going call state:
+ * Preference should be disabled
+ * Default string should be "Unavailable during calls"
+ */
+ @Test
+ public void updateState_duringACall_shouldSetDefaultSummary() {
+ mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+
+ mController.updateState(mPreference);
+
+ assertThat(mPreference.isEnabled()).isFalse();
+ assertThat(mPreference.getSummary()).isEqualTo(
+ mContext.getText(R.string.media_out_summary_ongoing_call_state));
+ }
+
+ /**
+ * No available A2dp BT devices:
+ * Preference should be disabled
+ * Preference summary should be "This device"
+ */
+ @Test
+ public void updateState_noAvailableA2dpBtDevices_shouldDisableAndSetDefaultSummary() {
+ mShadowAudioManager.setMode(AudioManager.MODE_NORMAL);
+ List<BluetoothDevice> emptyDeviceList = new ArrayList<>();
+ when(mA2dpProfile.getConnectedDevices()).thenReturn(emptyDeviceList);
+
+ mController.updateState(mPreference);
+
+ assertThat(mPreference.isEnabled()).isFalse();
+ String defaultString = mContext.getString(R.string.media_output_default_summary);
+ assertThat(mPreference.getSummary()).isEqualTo(defaultString);
+ }
+
+ /**
+ * Media stream is captured by something else (cast device):
+ * Preference should be disabled
+ * Preference summary should be "unavailable"
+ */
+ @Test
+ public void updateState_mediaStreamIsCapturedByCast_shouldDisableAndSetDefaultSummary() {
+ mShadowAudioManager.setMusicActiveRemotely(true);
+
+ mController.updateState(mPreference);
+
+ assertThat(mPreference.isEnabled()).isFalse();
+ String defaultString = mContext.getString(R.string.media_output_summary_unavailable);
+ assertThat(mPreference.getSummary()).isEqualTo(defaultString);
+ }
+
+ /**
+ * One A2DP Bluetooth device is available and active.
+ * Preference should be enabled
+ * Preference summary should be activate device name
+ */
+ @Test
+ public void updateState_oneA2dpBtDeviceAreAvailable_shouldSetActivatedDeviceName() {
+ mShadowAudioManager.setMode(AudioManager.MODE_NORMAL);
+ when(mA2dpProfile.getConnectedDevices()).thenReturn(mConnectedDevices);
+ when(mA2dpProfile.getActiveDevice()).thenReturn(mBluetoothDevice);
+
+ mController.updateState(mPreference);
+
+ assertThat(mPreference.isEnabled()).isTrue();
+ assertThat(mPreference.getSummary()).isEqualTo(mBluetoothDevice.getName());
+ }
+
+ /**
+ * More than one A2DP Bluetooth devices are available, and second device is active.
+ * Preference should be enabled
+ * Preference summary should be activate device name
+ */
+ @Test
+ public void updateState_moreThanOneA2DpBtDevicesAreAvailable_shouldSetActivatedDeviceName() {
+ ShadowBluetoothDevice shadowBluetoothDevice;
+ mShadowAudioManager.setMode(AudioManager.MODE_NORMAL);
+ BluetoothDevice secondBluetoothDevice;
+ secondBluetoothDevice = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_2);
+ shadowBluetoothDevice = Shadows.shadowOf(secondBluetoothDevice);
+ shadowBluetoothDevice.setName(TEST_DEVICE_NAME_2);
+ List<BluetoothDevice> connectedDevices = new ArrayList<>(2);
+ connectedDevices.add(mBluetoothDevice);
+ connectedDevices.add(secondBluetoothDevice);
+
+ when(mA2dpProfile.getConnectedDevices()).thenReturn(connectedDevices);
+ when(mA2dpProfile.getActiveDevice()).thenReturn(secondBluetoothDevice);
+
+ mController.updateState(mPreference);
+
+ assertThat(mPreference.isEnabled()).isTrue();
+ assertThat(mPreference.getSummary()).isEqualTo(secondBluetoothDevice.getName());
+ }
+
+ /**
+ * A2DP Bluetooth device(s) are available, but wired headset is plugged in and activated
+ * Preference should be enabled
+ * Preference summary should be "This device"
+ */
+ @Test
+ public void updateState_a2dpDevicesAvailableWiredHeadsetIsActivated_shouldSetDefaultSummary() {
+ mShadowAudioManager.setMode(AudioManager.MODE_NORMAL);
+ mShadowAudioManager.setWiredHeadsetOn(true);
+ mShadowAudioManager.setBluetoothA2dpOn(false);
+ when(mA2dpProfile.getConnectedDevices()).thenReturn(mConnectedDevices);
+ when(mA2dpProfile.getActiveDevice()).thenReturn(
+ mBluetoothDevice); // BT device is still activated in this case
+
+ mController.updateState(mPreference);
+
+ assertThat(mPreference.isEnabled()).isTrue();
+ String defaultString = mContext.getString(R.string.media_output_default_summary);
+ assertThat(mPreference.getSummary()).isEqualTo(defaultString);
+ }
+
+
+ /**
+ * A2DP Bluetooth device(s) are available, but current device speaker is activated
+ * Preference should be enabled
+ * Preference summary should be "This device"
+ */
+ @Test
+ public void updateState_a2dpDevicesAvailableCurrentDeviceActivated_shouldSetDefaultSummary() {
+ mShadowAudioManager.setMode(AudioManager.MODE_NORMAL);
+ when(mA2dpProfile.getConnectedDevices()).thenReturn(mConnectedDevices);
+ when(mA2dpProfile.getActiveDevice()).thenReturn(null);
+
+ mController.updateState(mPreference);
+
+ assertThat(mPreference.isEnabled()).isTrue();
+ String defaultString = mContext.getString(R.string.media_output_default_summary);
+ assertThat(mPreference.getSummary()).isEqualTo(defaultString);
+ }
+}
package com.android.settings.testutils.shadow;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.media.AudioDeviceCallback;
import android.media.AudioManager;
+import android.os.Handler;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+import org.robolectric.shadow.api.Shadow;
-@Implements(AudioManager.class)
-public class ShadowAudioManager {
+import java.util.ArrayList;
+@Implements(value = AudioManager.class, inheritImplementationMethods = true)
+public class ShadowAudioManager extends org.robolectric.shadows.ShadowAudioManager {
private int mRingerMode;
+ private boolean mMusicActiveRemotely = false;
+ private ArrayList<AudioDeviceCallback> mDeviceCallbacks = new ArrayList();
@Implementation
+ private int getRingerModeInternal() {
+ return mRingerMode;
+ }
+
+ public static ShadowAudioManager getShadow() {
+ return Shadow.extract(application.getSystemService(AudioManager.class));
+ }
+
public void setRingerModeInternal(int mode) {
mRingerMode = mode;
}
- @Implementation
- private int getRingerModeInternal() {
- return mRingerMode;
+ public void registerAudioDeviceCallback(AudioDeviceCallback callback, Handler handler) {
+ mDeviceCallbacks.add(callback);
+ }
+
+ public void unregisterAudioDeviceCallback(AudioDeviceCallback callback) {
+ if (mDeviceCallbacks.contains(callback)) {
+ mDeviceCallbacks.remove(callback);
+ }
+ }
+
+ public void setMusicActiveRemotely(boolean flag) {
+ mMusicActiveRemotely = flag;
+ }
+
+ public boolean isMusicActiveRemotely() {
+ return mMusicActiveRemotely;
+ }
+
+ @Resetter
+ public void reset() {
+ mDeviceCallbacks.clear();
}
}
--- /dev/null
+/*
+ * Copyright (C) 2018 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.testutils.shadow;
+
+import android.content.Context;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+
+@Implements(Utils.class)
+public class ShadowBluetoothUtils {
+ public static LocalBluetoothManager sLocalBluetoothManager;
+
+
+
+ @Implementation
+ public static LocalBluetoothManager getLocalBtManager(Context context) {
+ return sLocalBluetoothManager;
+ }
+
+ @Resetter
+ public static void reset() {
+ sLocalBluetoothManager = null;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2018 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.testutils.shadow;
+
+import android.media.MediaRouter;
+
+import static org.robolectric.RuntimeEnvironment.application;
+
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@Implements(value = MediaRouter.class, inheritImplementationMethods = true)
+public class ShadowMediaRouter extends org.robolectric.shadows.ShadowMediaRouter {
+ MediaRouter.RouteInfo mSelectedRoute;
+
+ final CopyOnWriteArrayList<MediaRouter.Callback> mCallbacks =
+ new CopyOnWriteArrayList<>();
+
+ public MediaRouter.RouteInfo getSelectedRoute(int type) {
+ return mSelectedRoute;
+ }
+
+ public void addCallback(int types, MediaRouter.Callback cb) {
+ mCallbacks.add(cb);
+ }
+
+ public void removeCallback(MediaRouter.Callback cb) {
+ if (mCallbacks.contains(cb))
+ mCallbacks.remove(cb);
+ }
+
+ public static ShadowMediaRouter getShadow() {
+ return Shadow.extract(application.getSystemService(MediaRouter.class));
+ }
+
+ @Resetter
+ public void reset() {
+ mCallbacks.clear();
+ }
+}