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

Commit 03b8aa3a authored by Caxton Chan's avatar Caxton Chan Committed by Android (Google) Code Review
Browse files

Merge "Add audio switch UI in sound settings" into pi-dev

parents 5a2f7315 63bdfa89
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
@@ -30,6 +30,14 @@
        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"
@@ -45,6 +53,14 @@
        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"
+1 −0
Original line number Diff line number Diff line
@@ -25,4 +25,5 @@ public class FeatureFlags {
    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";
}
+335 −0
Original line number Diff line number Diff line
/*
 * 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) {
        }
    }
}
+94 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
+116 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
Loading