Home | History | Annotate | Download | only in sound
      1 /*
      2  * Copyright (C) 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.settings.sound;
     18 
     19 import static android.media.AudioManager.STREAM_DEVICES_CHANGED_ACTION;
     20 import static android.media.AudioManager.STREAM_MUSIC;
     21 import static android.media.AudioManager.STREAM_VOICE_CALL;
     22 import static android.media.AudioSystem.DEVICE_OUT_ALL_A2DP;
     23 import static android.media.AudioSystem.DEVICE_OUT_ALL_SCO;
     24 import static android.media.AudioSystem.DEVICE_OUT_HEARING_AID;
     25 import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
     26 
     27 import android.bluetooth.BluetoothDevice;
     28 import android.content.BroadcastReceiver;
     29 import android.content.Context;
     30 import android.content.Intent;
     31 import android.content.IntentFilter;
     32 import android.content.pm.PackageManager;
     33 import android.media.AudioDeviceCallback;
     34 import android.media.AudioDeviceInfo;
     35 import android.media.AudioManager;
     36 import android.media.MediaRouter;
     37 import android.media.MediaRouter.Callback;
     38 import android.os.Handler;
     39 import android.os.Looper;
     40 import android.support.v7.preference.ListPreference;
     41 import android.support.v7.preference.Preference;
     42 import android.support.v7.preference.PreferenceScreen;
     43 import android.text.TextUtils;
     44 import android.util.FeatureFlagUtils;
     45 import android.util.Log;
     46 
     47 import com.android.settings.R;
     48 import com.android.settings.bluetooth.Utils;
     49 import com.android.settings.core.BasePreferenceController;
     50 import com.android.settings.core.FeatureFlags;
     51 import com.android.settingslib.bluetooth.A2dpProfile;
     52 import com.android.settingslib.bluetooth.BluetoothCallback;
     53 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
     54 import com.android.settingslib.bluetooth.HeadsetProfile;
     55 import com.android.settingslib.bluetooth.HearingAidProfile;
     56 import com.android.settingslib.bluetooth.LocalBluetoothManager;
     57 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
     58 import com.android.settingslib.core.lifecycle.LifecycleObserver;
     59 import com.android.settingslib.core.lifecycle.events.OnStart;
     60 import com.android.settingslib.core.lifecycle.events.OnStop;
     61 
     62 import java.util.ArrayList;
     63 import java.util.List;
     64 import java.util.concurrent.ExecutionException;
     65 import java.util.concurrent.FutureTask;
     66 
     67 /**
     68  * Abstract class for audio switcher controller to notify subclass
     69  * updating the current status of switcher entry. Subclasses must overwrite
     70  * {@link #setActiveBluetoothDevice(BluetoothDevice)} to set the
     71  * active device for corresponding profile.
     72  */
     73 public abstract class AudioSwitchPreferenceController extends BasePreferenceController
     74         implements Preference.OnPreferenceChangeListener, BluetoothCallback,
     75         LifecycleObserver, OnStart, OnStop {
     76 
     77     private static final String TAG = "AudioSwitchPreferenceController";
     78     private static final int INVALID_INDEX = -1;
     79 
     80     protected final List<BluetoothDevice> mConnectedDevices;
     81     protected final AudioManager mAudioManager;
     82     protected final MediaRouter mMediaRouter;
     83     protected int mSelectedIndex;
     84     protected Preference mPreference;
     85     protected LocalBluetoothProfileManager mProfileManager;
     86     protected AudioSwitchCallback mAudioSwitchPreferenceCallback;
     87 
     88     private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback;
     89     private final MediaRouterCallback mMediaRouterCallback;
     90     private final WiredHeadsetBroadcastReceiver mReceiver;
     91     private final Handler mHandler;
     92     private LocalBluetoothManager mLocalBluetoothManager;
     93 
     94     public interface AudioSwitchCallback {
     95         void onPreferenceDataChanged(ListPreference preference);
     96     }
     97 
     98     public AudioSwitchPreferenceController(Context context, String preferenceKey) {
     99         super(context, preferenceKey);
    100         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    101         mMediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
    102         mHandler = new Handler(Looper.getMainLooper());
    103         mAudioManagerAudioDeviceCallback = new AudioManagerAudioDeviceCallback();
    104         mReceiver = new WiredHeadsetBroadcastReceiver();
    105         mMediaRouterCallback = new MediaRouterCallback();
    106         mConnectedDevices = new ArrayList<>();
    107         final FutureTask<LocalBluetoothManager> localBtManagerFutureTask = new FutureTask<>(
    108                 // Avoid StrictMode ThreadPolicy violation
    109                 () -> Utils.getLocalBtManager(mContext));
    110         try {
    111             localBtManagerFutureTask.run();
    112             mLocalBluetoothManager = localBtManagerFutureTask.get();
    113         } catch (InterruptedException | ExecutionException e) {
    114             Log.w(TAG, "Error getting LocalBluetoothManager.", e);
    115             return;
    116         }
    117         if (mLocalBluetoothManager == null) {
    118             Log.e(TAG, "Bluetooth is not supported on this device");
    119             return;
    120         }
    121         mProfileManager = mLocalBluetoothManager.getProfileManager();
    122     }
    123 
    124     /**
    125      * Make this method as final, ensure that subclass will checking
    126      * the feature flag and they could mistakenly break it via overriding.
    127      */
    128     @Override
    129     public final int getAvailabilityStatus() {
    130         return FeatureFlagUtils.isEnabled(mContext, FeatureFlags.AUDIO_SWITCHER_SETTINGS) &&
    131                 mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)
    132                 ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
    133     }
    134 
    135     @Override
    136     public boolean onPreferenceChange(Preference preference, Object newValue) {
    137         final String address = (String) newValue;
    138         if (!(preference instanceof ListPreference)) {
    139             return false;
    140         }
    141 
    142         final ListPreference listPreference = (ListPreference) preference;
    143         if (TextUtils.equals(address, mContext.getText(R.string.media_output_default_summary))) {
    144             // Switch to default device which address is device name
    145             mSelectedIndex = getDefaultDeviceIndex();
    146             setActiveBluetoothDevice(null);
    147             listPreference.setSummary(mContext.getText(R.string.media_output_default_summary));
    148         } else {
    149             // Switch to BT device which address is hardware address
    150             final int connectedDeviceIndex = getConnectedDeviceIndex(address);
    151             if (connectedDeviceIndex == INVALID_INDEX) {
    152                 return false;
    153             }
    154             final BluetoothDevice btDevice = mConnectedDevices.get(connectedDeviceIndex);
    155             mSelectedIndex = connectedDeviceIndex;
    156             setActiveBluetoothDevice(btDevice);
    157             listPreference.setSummary(btDevice.getAliasName());
    158         }
    159         return true;
    160     }
    161 
    162     public abstract void setActiveBluetoothDevice(BluetoothDevice device);
    163 
    164     @Override
    165     public void displayPreference(PreferenceScreen screen) {
    166         super.displayPreference(screen);
    167         mPreference = screen.findPreference(mPreferenceKey);
    168         mPreference.setVisible(false);
    169     }
    170 
    171     @Override
    172     public void onStart() {
    173         if (mLocalBluetoothManager == null) {
    174             Log.e(TAG, "Bluetooth is not supported on this device");
    175             return;
    176         }
    177         mLocalBluetoothManager.setForegroundActivity(mContext);
    178         register();
    179     }
    180 
    181     @Override
    182     public void onStop() {
    183         if (mLocalBluetoothManager == null) {
    184             Log.e(TAG, "Bluetooth is not supported on this device");
    185             return;
    186         }
    187         mLocalBluetoothManager.setForegroundActivity(null);
    188         unregister();
    189     }
    190 
    191     /**
    192      * Only concerned about whether the local adapter is connected to any profile of any device and
    193      * are not really concerned about which profile.
    194      */
    195     @Override
    196     public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {
    197     }
    198 
    199     @Override
    200     public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {
    201         updateState(mPreference);
    202     }
    203 
    204     @Override
    205     public void onAudioModeChanged() {
    206         updateState(mPreference);
    207     }
    208 
    209     @Override
    210     public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state,
    211             int bluetoothProfile) {
    212         updateState(mPreference);
    213     }
    214 
    215     @Override
    216     public void onBluetoothStateChanged(int bluetoothState) {
    217     }
    218 
    219     /**
    220      * The local Bluetooth adapter has started the remote device discovery process.
    221      */
    222     @Override
    223     public void onScanningStateChanged(boolean started) {
    224     }
    225 
    226     /**
    227      * Indicates a change in the bond state of a remote
    228      * device. For example, if a device is bonded (paired).
    229      */
    230     @Override
    231     public void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
    232         updateState(mPreference);
    233     }
    234 
    235     @Override
    236     public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {
    237     }
    238 
    239     @Override
    240     public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
    241     }
    242 
    243     public void setCallback(AudioSwitchCallback callback) {
    244         mAudioSwitchPreferenceCallback = callback;
    245     }
    246 
    247     protected boolean isStreamFromOutputDevice(int streamType, int device) {
    248         return (device & mAudioManager.getDevicesForStream(streamType)) != 0;
    249     }
    250 
    251     /**
    252      * get hands free profile(HFP) connected device
    253      */
    254     protected List<BluetoothDevice> getConnectedHfpDevices() {
    255         final List<BluetoothDevice> connectedDevices = new ArrayList<>();
    256         final HeadsetProfile hfpProfile = mProfileManager.getHeadsetProfile();
    257         if (hfpProfile == null) {
    258             return connectedDevices;
    259         }
    260         final List<BluetoothDevice> devices = hfpProfile.getConnectedDevices();
    261         for (BluetoothDevice device : devices) {
    262             if (device.isConnected()) {
    263                 connectedDevices.add(device);
    264             }
    265         }
    266         return connectedDevices;
    267     }
    268 
    269     /**
    270      * get A2dp connected device
    271      */
    272     protected List<BluetoothDevice> getConnectedA2dpDevices() {
    273         final List<BluetoothDevice> connectedDevices = new ArrayList<>();
    274         final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
    275         if (a2dpProfile == null) {
    276             return connectedDevices;
    277         }
    278         final List<BluetoothDevice> devices = a2dpProfile.getConnectedDevices();
    279         for (BluetoothDevice device : devices) {
    280             if (device.isConnected()) {
    281                 connectedDevices.add(device);
    282             }
    283         }
    284         return connectedDevices;
    285     }
    286 
    287     /**
    288      * get hearing aid profile connected device, exclude other devices with same hiSyncId.
    289      */
    290     protected List<BluetoothDevice> getConnectedHearingAidDevices() {
    291         final List<BluetoothDevice> connectedDevices = new ArrayList<>();
    292         final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile();
    293         if (hapProfile == null) {
    294             return connectedDevices;
    295         }
    296         final List<Long> devicesHiSyncIds = new ArrayList<>();
    297         final List<BluetoothDevice> devices = hapProfile.getConnectedDevices();
    298         for (BluetoothDevice device : devices) {
    299             final long hiSyncId = hapProfile.getHiSyncId(device);
    300             // device with same hiSyncId should not be shown in the UI.
    301             // So do not add it into connectedDevices.
    302             if (!devicesHiSyncIds.contains(hiSyncId) && device.isConnected()) {
    303                 devicesHiSyncIds.add(hiSyncId);
    304                 connectedDevices.add(device);
    305             }
    306         }
    307         return connectedDevices;
    308     }
    309 
    310     /**
    311      * According to different stream and output device, find the active device from
    312      * the corresponding profile. Hearing aid device could stream both STREAM_MUSIC
    313      * and STREAM_VOICE_CALL.
    314      *
    315      * @param streamType the type of audio streams.
    316      * @return the active device. Return null if the active device is current device
    317      * or streamType is not STREAM_MUSIC or STREAM_VOICE_CALL.
    318      */
    319     protected BluetoothDevice findActiveDevice(int streamType) {
    320         if (streamType != STREAM_MUSIC && streamType != STREAM_VOICE_CALL) {
    321             return null;
    322         }
    323         if (isStreamFromOutputDevice(STREAM_MUSIC, DEVICE_OUT_ALL_A2DP)) {
    324             return mProfileManager.getA2dpProfile().getActiveDevice();
    325         } else if (isStreamFromOutputDevice(STREAM_VOICE_CALL, DEVICE_OUT_ALL_SCO)) {
    326             return mProfileManager.getHeadsetProfile().getActiveDevice();
    327         } else if (isStreamFromOutputDevice(streamType, DEVICE_OUT_HEARING_AID)) {
    328             // The first element is the left active device; the second element is
    329             // the right active device. And they will have same hiSyncId. If either
    330             // or both side is not active, it will be null on that position.
    331             List<BluetoothDevice> activeDevices =
    332                     mProfileManager.getHearingAidProfile().getActiveDevices();
    333             for (BluetoothDevice btDevice : activeDevices) {
    334                 if (btDevice != null && mConnectedDevices.contains(btDevice)) {
    335                     // also need to check mConnectedDevices, because one of
    336                     // the device(same hiSyncId) might not be shown in the UI.
    337                     return btDevice;
    338                 }
    339             }
    340         }
    341         return null;
    342     }
    343 
    344     int getDefaultDeviceIndex() {
    345         // Default device is after all connected devices.
    346         return mConnectedDevices.size();
    347     }
    348 
    349     void setupPreferenceEntries(CharSequence[] mediaOutputs, CharSequence[] mediaValues,
    350             BluetoothDevice activeDevice) {
    351         // default to current device
    352         mSelectedIndex = getDefaultDeviceIndex();
    353         // default device is after all connected devices.
    354         mediaOutputs[mSelectedIndex] = mContext.getText(R.string.media_output_default_summary);
    355         // use default device name as address
    356         mediaValues[mSelectedIndex] = mContext.getText(R.string.media_output_default_summary);
    357         for (int i = 0, size = mConnectedDevices.size(); i < size; i++) {
    358             final BluetoothDevice btDevice = mConnectedDevices.get(i);
    359             mediaOutputs[i] = btDevice.getAliasName();
    360             mediaValues[i] = btDevice.getAddress();
    361             if (btDevice.equals(activeDevice)) {
    362                 // select the active connected device.
    363                 mSelectedIndex = i;
    364             }
    365         }
    366     }
    367 
    368     void setPreference(CharSequence[] mediaOutputs, CharSequence[] mediaValues,
    369             Preference preference) {
    370         final ListPreference listPreference = (ListPreference) preference;
    371         listPreference.setEntries(mediaOutputs);
    372         listPreference.setEntryValues(mediaValues);
    373         listPreference.setValueIndex(mSelectedIndex);
    374         listPreference.setSummary(mediaOutputs[mSelectedIndex]);
    375         mAudioSwitchPreferenceCallback.onPreferenceDataChanged(listPreference);
    376     }
    377 
    378     private int getConnectedDeviceIndex(String hardwareAddress) {
    379         if (mConnectedDevices != null) {
    380             for (int i = 0, size = mConnectedDevices.size(); i < size; i++) {
    381                 final BluetoothDevice btDevice = mConnectedDevices.get(i);
    382                 if (TextUtils.equals(btDevice.getAddress(), hardwareAddress)) {
    383                     return i;
    384                 }
    385             }
    386         }
    387         return INVALID_INDEX;
    388     }
    389 
    390     private void register() {
    391         mLocalBluetoothManager.getEventManager().registerCallback(this);
    392         mAudioManager.registerAudioDeviceCallback(mAudioManagerAudioDeviceCallback, mHandler);
    393         mMediaRouter.addCallback(ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback);
    394 
    395         // Register for misc other intent broadcasts.
    396         IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
    397         intentFilter.addAction(STREAM_DEVICES_CHANGED_ACTION);
    398         mContext.registerReceiver(mReceiver, intentFilter);
    399     }
    400 
    401     private void unregister() {
    402         mLocalBluetoothManager.getEventManager().unregisterCallback(this);
    403         mAudioManager.unregisterAudioDeviceCallback(mAudioManagerAudioDeviceCallback);
    404         mMediaRouter.removeCallback(mMediaRouterCallback);
    405         mContext.unregisterReceiver(mReceiver);
    406     }
    407 
    408     /** Notifications of audio device connection and disconnection events. */
    409     private class AudioManagerAudioDeviceCallback extends AudioDeviceCallback {
    410         @Override
    411         public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
    412             updateState(mPreference);
    413         }
    414 
    415         @Override
    416         public void onAudioDevicesRemoved(AudioDeviceInfo[] devices) {
    417             updateState(mPreference);
    418         }
    419     }
    420 
    421     /** Receiver for wired headset plugged and unplugged events. */
    422     private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver {
    423         @Override
    424         public void onReceive(Context context, Intent intent) {
    425             final String action = intent.getAction();
    426             if (AudioManager.ACTION_HEADSET_PLUG.equals(action) ||
    427                     AudioManager.STREAM_DEVICES_CHANGED_ACTION.equals(action)) {
    428                 updateState(mPreference);
    429             }
    430         }
    431     }
    432 
    433     /** Callback for cast device events. */
    434     private class MediaRouterCallback extends Callback {
    435         @Override
    436         public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info) {
    437         }
    438 
    439         @Override
    440         public void onRouteUnselected(MediaRouter router, int type, MediaRouter.RouteInfo info) {
    441         }
    442 
    443         @Override
    444         public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
    445             if (info != null && !info.isDefault()) {
    446                 // cast mode
    447                 updateState(mPreference);
    448             }
    449         }
    450 
    451         @Override
    452         public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
    453         }
    454 
    455         @Override
    456         public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
    457             if (info != null && !info.isDefault()) {
    458                 // cast mode
    459                 updateState(mPreference);
    460             }
    461         }
    462 
    463         @Override
    464         public void onRouteGrouped(MediaRouter router, MediaRouter.RouteInfo info,
    465                 MediaRouter.RouteGroup group, int index) {
    466         }
    467 
    468         @Override
    469         public void onRouteUngrouped(MediaRouter router, MediaRouter.RouteInfo info,
    470                 MediaRouter.RouteGroup group) {
    471         }
    472 
    473         @Override
    474         public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo info) {
    475         }
    476     }
    477 }
    478