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

Commit abdf7390 authored by timhypeng's avatar timhypeng Committed by tim peng
Browse files

Sound + Output Switcher on Sound Setting

1. Show "play media to" item when Previously Connected device is available
2. Click "Play media to" to launch output slice
3. Update test case

Bug: 126475101
Test: make -j50 RunSettingsRoboTests
Change-Id: Id8afd1a2407acb78c11e81d2420ae8c16130a321
parent 94240c5a
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -41,7 +41,7 @@
        settings:controller="com.android.settings.notification.MediaVolumePreferenceController"/>

    <!-- Media output switcher -->
    <ListPreference
    <Preference
        android:key="media_output"
        android:title="@string/media_output_title"
        android:dialogTitle="@string/media_output_title"
+0 −7
Original line number Diff line number Diff line
@@ -38,7 +38,6 @@ import com.android.settings.core.OnActivityResultListener;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.sound.HandsFreeProfileOutputPreferenceController;
import com.android.settings.sound.MediaOutputPreferenceController;
import com.android.settings.widget.PreferenceCategoryController;
import com.android.settings.widget.UpdatableListPreferenceDialogFragment;
import com.android.settingslib.core.AbstractPreferenceController;
@@ -77,7 +76,6 @@ public class SoundSettings extends DashboardFragment implements OnActivityResult

    private RingtonePreference mRequestPreference;
    private UpdatableListPreferenceDialogFragment mDialogFragment;
    private String mMediaOutputControllerKey;
    private String mHfpOutputControllerKey;

    @Override
@@ -132,8 +130,6 @@ public class SoundSettings extends DashboardFragment implements OnActivityResult
        final int metricsCategory;
        if (mHfpOutputControllerKey.equals(preference.getKey())) {
            metricsCategory = SettingsEnums.DIALOG_SWITCH_HFP_DEVICES;
        } else if (mMediaOutputControllerKey.equals(preference.getKey())) {
            metricsCategory = SettingsEnums.DIALOG_SWITCH_A2DP_DEVICES;
        } else {
            metricsCategory = Instrumentable.METRICS_CATEGORY_UNKNOWN;
        }
@@ -186,9 +182,6 @@ public class SoundSettings extends DashboardFragment implements OnActivityResult
        volumeControllers.add(use(CallVolumePreferenceController.class));
        volumeControllers.add(use(RemoteVolumePreferenceController.class));

        use(MediaOutputPreferenceController.class).setCallback(listPreference ->
                onPreferenceDataChanged(listPreference));
        mMediaOutputControllerKey = use(MediaOutputPreferenceController.class).getPreferenceKey();
        use(HandsFreeProfileOutputPreferenceController.class).setCallback(listPreference ->
                onPreferenceDataChanged(listPreference));
        mHfpOutputControllerKey =
+35 −91
Original line number Diff line number Diff line
@@ -32,7 +32,6 @@ import android.media.MediaRouter;
import android.media.MediaRouter.Callback;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.FeatureFlagUtils;
import android.util.Log;

@@ -40,7 +39,6 @@ import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;

import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.FeatureFlags;
@@ -63,15 +61,11 @@ import java.util.concurrent.FutureTask;
/**
 * 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 {
        implements BluetoothCallback, LifecycleObserver, OnStart, OnStop {

    private static final String TAG = "AudioSwitchPrefCtrl";
    private static final int INVALID_INDEX = -1;

    protected final List<BluetoothDevice> mConnectedDevices;
    protected final AudioManager mAudioManager;
@@ -128,35 +122,6 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont
                ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
    }

    @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.getAliasName());
        }
        return true;
    }

    public abstract void setActiveBluetoothDevice(BluetoothDevice device);

    @Override
    public void displayPreference(PreferenceScreen screen) {
        super.displayPreference(screen);
@@ -184,6 +149,12 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont
        unregister();
    }

    @Override
    public void onBluetoothStateChanged(int bluetoothState) {
        // To handle the case that Bluetooth on and no connected devices
        updateState(mPreference);
    }

    @Override
    public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {
        updateState(mPreference);
@@ -236,17 +207,34 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont
    }

    /**
     * get A2dp connected device
     * get A2dp devices on all states
     * (STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED,  STATE_DISCONNECTING)
     */
    protected List<BluetoothDevice> getConnectedA2dpDevices() {
        final List<BluetoothDevice> connectedDevices = new ArrayList<>();
    protected List<BluetoothDevice> getConnectableA2dpDevices() {
        final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
        if (a2dpProfile == null) {
            return new ArrayList<>();
        }
        return a2dpProfile.getConnectableDevices();
    }

    /**
     * get hearing aid profile connected device, exclude other devices with same hiSyncId.
     */
    protected List<BluetoothDevice> getConnectedHearingAidDevices() {
        final List<BluetoothDevice> connectedDevices = new ArrayList<>();
        final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile();
        if (hapProfile == null) {
            return connectedDevices;
        }
        final List<BluetoothDevice> devices = a2dpProfile.getConnectedDevices();
        final List<Long> devicesHiSyncIds = new ArrayList<>();
        final List<BluetoothDevice> devices = hapProfile.getConnectedDevices();
        for (BluetoothDevice device : devices) {
            if (device.isConnected()) {
            final long hiSyncId = hapProfile.getHiSyncId(device);
            // device with same hiSyncId should not be shown in the UI.
            // So do not add it into connectedDevices.
            if (!devicesHiSyncIds.contains(hiSyncId) && device.isConnected()) {
                devicesHiSyncIds.add(hiSyncId);
                connectedDevices.add(device);
            }
        }
@@ -254,21 +242,23 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont
    }

    /**
     * get hearing aid profile connected device, exclude other devices with same hiSyncId.
     * get hearing aid profile devices on all states
     * (STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED,  STATE_DISCONNECTING)
     * exclude other devices with same hiSyncId.
     */
    protected List<BluetoothDevice> getConnectedHearingAidDevices() {
    protected List<BluetoothDevice> getConnectableHearingAidDevices() {
        final List<BluetoothDevice> connectedDevices = new ArrayList<>();
        final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile();
        if (hapProfile == null) {
            return connectedDevices;
        }
        final List<Long> devicesHiSyncIds = new ArrayList<>();
        final List<BluetoothDevice> devices = hapProfile.getConnectedDevices();
        final List<BluetoothDevice> devices = hapProfile.getConnectableDevices();
        for (BluetoothDevice device : devices) {
            final long hiSyncId = hapProfile.getHiSyncId(device);
            // device with same hiSyncId should not be shown in the UI.
            // So do not add it into connectedDevices.
            if (!devicesHiSyncIds.contains(hiSyncId) && device.isConnected()) {
            if (!devicesHiSyncIds.contains(hiSyncId)) {
                devicesHiSyncIds.add(hiSyncId);
                connectedDevices.add(device);
            }
@@ -306,52 +296,6 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont
     */
    public abstract BluetoothDevice findActiveDevice();

    int getDefaultDeviceIndex() {
        // Default device is after all connected devices.
        return mConnectedDevices.size();
    }

    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.getAliasName();
            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]);
        mAudioSwitchPreferenceCallback.onPreferenceDataChanged(listPreference);
    }

    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);
+81 −3
Original line number Diff line number Diff line
@@ -20,7 +20,9 @@ import static android.bluetooth.IBluetoothHearingAid.HI_SYNC_ID_INVALID;

import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.text.TextUtils;

import androidx.preference.ListPreference;
import androidx.preference.Preference;

import com.android.settings.R;
@@ -32,13 +34,55 @@ import com.android.settingslib.bluetooth.HearingAidProfile;
 * This class allows switching between HFP-connected & HAP-connected BT devices
 * while in on-call state.
 */
public class HandsFreeProfileOutputPreferenceController extends
        AudioSwitchPreferenceController {
public class HandsFreeProfileOutputPreferenceController extends AudioSwitchPreferenceController
        implements Preference.OnPreferenceChangeListener {

    private static final int INVALID_INDEX = -1;

    public HandsFreeProfileOutputPreferenceController(Context context, String key) {
        super(context, key);
    }

    @Override
    public boolean onPreferenceChange(Preference preference, Object newValue) {
        final String address = (String) newValue;
        if (!(preference instanceof ListPreference)) {
            return false;
        }

        final CharSequence defaultSummary = mContext.getText(R.string.media_output_default_summary);
        final ListPreference listPreference = (ListPreference) preference;
        if (TextUtils.equals(address, defaultSummary)) {
            // Switch to default device which address is device name
            mSelectedIndex = getDefaultDeviceIndex();
            setActiveBluetoothDevice(null);
            listPreference.setSummary(defaultSummary);
        } 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.getAliasName());
        }
        return true;
    }

    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;
    }

    @Override
    public void updateState(Preference preference) {
        if (preference == null) {
@@ -83,7 +127,41 @@ public class HandsFreeProfileOutputPreferenceController extends
        setPreference(mediaOutputs, mediaValues, preference);
    }

    @Override
    int getDefaultDeviceIndex() {
        // Default device is after all connected devices.
        return mConnectedDevices.size();
    }

    void setupPreferenceEntries(CharSequence[] mediaOutputs, CharSequence[] mediaValues,
            BluetoothDevice activeDevice) {
        // default to current device
        mSelectedIndex = getDefaultDeviceIndex();
        // default device is after all connected devices.
        final CharSequence defaultSummary = mContext.getText(R.string.media_output_default_summary);
        mediaOutputs[mSelectedIndex] = defaultSummary;
        // use default device name as address
        mediaValues[mSelectedIndex] = defaultSummary;
        for (int i = 0, size = mConnectedDevices.size(); i < size; i++) {
            final BluetoothDevice btDevice = mConnectedDevices.get(i);
            mediaOutputs[i] = btDevice.getAliasName();
            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]);
        mAudioSwitchPreferenceCallback.onPreferenceDataChanged(listPreference);
    }

    public void setActiveBluetoothDevice(BluetoothDevice device) {
        if (!Utils.isAudioModeOngoingCall(mContext)) {
            return;
+55 −38
Original line number Diff line number Diff line
@@ -16,13 +16,14 @@

package com.android.settings.sound;

import static android.bluetooth.IBluetoothHearingAid.HI_SYNC_ID_INVALID;
import static android.media.AudioManager.STREAM_MUSIC;
import static android.media.AudioSystem.DEVICE_OUT_REMOTE_SUBMIX;

import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.text.TextUtils;

import androidx.preference.Preference;

@@ -30,12 +31,16 @@ import com.android.settings.R;
import com.android.settingslib.Utils;
import com.android.settingslib.bluetooth.A2dpProfile;
import com.android.settingslib.bluetooth.HearingAidProfile;
import com.android.settingslib.media.MediaOutputSliceConstants;

import java.util.List;

/**
 * This class which allows switching between A2dp-connected & HAP-connected BT devices.
 * A few conditions will disable this switcher:
 * - No available BT device(s)
 * - Media stream captured by cast device
 * This class allows launching MediaOutputSlice to switch output device.
 * Preference would hide only when
 * - Bluetooth = OFF
 * - Bluetooth = ON and Connected Devices = 0 and Previously Connected = 0
 * - Media stream captured by remote device
 * - During a call.
 */
public class MediaOutputPreferenceController extends AudioSwitchPreferenceController {
@@ -66,40 +71,22 @@ public class MediaOutputPreferenceController extends AudioSwitchPreferenceContro
            return;
        }

        mConnectedDevices.clear();
        // Otherwise, list all of the A2DP connected device and display the active device.
        if (mAudioManager.getMode() == AudioManager.MODE_NORMAL) {
            mConnectedDevices.addAll(getConnectedA2dpDevices());
            mConnectedDevices.addAll(getConnectedHearingAidDevices());
        }

        final int numDevices = mConnectedDevices.size();
        mPreference.setVisible((numDevices == 0) ? false : true);
        CharSequence[] mediaOutputs = new CharSequence[numDevices + 1];
        CharSequence[] mediaValues = new CharSequence[numDevices + 1];

        // Setup devices entries, select active connected device
        setupPreferenceEntries(mediaOutputs, mediaValues, findActiveDevice());

        // 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) {
            return;
        }
        final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile();
        final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
        if (hapProfile != null && a2dpProfile != null && device == null) {
            hapProfile.setActiveDevice(null);
            a2dpProfile.setActiveDevice(null);
        } else if (hapProfile != null && hapProfile.getHiSyncId(device) != HI_SYNC_ID_INVALID) {
            hapProfile.setActiveDevice(device);
        } else if (a2dpProfile != null) {
            a2dpProfile.setActiveDevice(device);
        boolean deviceConnectable = false;
        BluetoothDevice activeDevice = null;
        // Show preference if there is connected or previously connected device
        // Find active device and set its name as the preference's summary
        List<BluetoothDevice> connectableA2dpDevices = getConnectableA2dpDevices();
        List<BluetoothDevice> connectableHADevices = getConnectableHearingAidDevices();
        if (mAudioManager.getMode() == AudioManager.MODE_NORMAL
                && ((connectableA2dpDevices != null && !connectableA2dpDevices.isEmpty())
                || (connectableHADevices != null && !connectableHADevices.isEmpty()))) {
            deviceConnectable = true;
            activeDevice = findActiveDevice();
        }
        mPreference.setVisible(deviceConnectable);
        mPreference.setSummary((activeDevice == null) ?
                mContext.getText(R.string.media_output_default_summary) :
                activeDevice.getAliasName());
    }

    @Override
@@ -112,4 +99,34 @@ public class MediaOutputPreferenceController extends AudioSwitchPreferenceContro
        }
        return activeDevice;
    }

    /**
     * Find active hearing aid device
     */
    @Override
    protected BluetoothDevice findActiveHearingAidDevice() {
        final HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();

        if (hearingAidProfile != null) {
            List<BluetoothDevice> activeDevices = hearingAidProfile.getActiveDevices();
            for (BluetoothDevice btDevice : activeDevices) {
                if (btDevice != null) {
                    return btDevice;
                }
            }
        }
        return null;
    }

    @Override
    public boolean handlePreferenceTreeClick(Preference preference) {
        if (TextUtils.equals(preference.getKey(), getPreferenceKey())) {
            final Intent intent = new Intent()
                    .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            mContext.startActivity(intent);
            return true;
        }
        return false;
    }
}
Loading