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