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