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 21 import static com.android.settingslib.media.flags.Flags.enableOutputSwitcherForSystemRouting; 22 23 import android.bluetooth.BluetoothDevice; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.content.pm.PackageManager; 29 import android.media.AudioDeviceCallback; 30 import android.media.AudioDeviceInfo; 31 import android.media.AudioManager; 32 import android.media.MediaRouter; 33 import android.media.session.MediaController; 34 import android.media.session.MediaSessionManager; 35 import android.os.Handler; 36 import android.os.Looper; 37 import android.util.FeatureFlagUtils; 38 import android.util.Log; 39 40 import androidx.annotation.Nullable; 41 import androidx.preference.ListPreference; 42 import androidx.preference.Preference; 43 import androidx.preference.PreferenceScreen; 44 45 import com.android.settings.bluetooth.Utils; 46 import com.android.settings.core.BasePreferenceController; 47 import com.android.settings.core.FeatureFlags; 48 import com.android.settings.sounde.AudioSwitchUtils; 49 import com.android.settingslib.bluetooth.A2dpProfile; 50 import com.android.settingslib.bluetooth.BluetoothCallback; 51 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 52 import com.android.settingslib.bluetooth.HeadsetProfile; 53 import com.android.settingslib.bluetooth.HearingAidProfile; 54 import com.android.settingslib.bluetooth.LeAudioProfile; 55 import com.android.settingslib.bluetooth.LocalBluetoothManager; 56 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 57 import com.android.settingslib.core.lifecycle.LifecycleObserver; 58 import com.android.settingslib.core.lifecycle.events.OnStart; 59 import com.android.settingslib.core.lifecycle.events.OnStop; 60 61 import java.util.ArrayList; 62 import java.util.Collection; 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 */ 71 public abstract class AudioSwitchPreferenceController extends BasePreferenceController 72 implements BluetoothCallback, LifecycleObserver, OnStart, OnStop, 73 LocalBluetoothProfileManager.ServiceListener { 74 75 private static final String TAG = "AudioSwitchPrefCtrl"; 76 77 protected final List<BluetoothDevice> mConnectedDevices; 78 protected final AudioManager mAudioManager; 79 protected final MediaRouter mMediaRouter; 80 protected int mSelectedIndex; 81 protected Preference mPreference; 82 protected LocalBluetoothProfileManager mProfileManager; 83 protected AudioSwitchCallback mAudioSwitchPreferenceCallback; 84 85 private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback; 86 private final WiredHeadsetBroadcastReceiver mReceiver; 87 private final Handler mHandler; 88 private LocalBluetoothManager mLocalBluetoothManager; 89 @Nullable private MediaSessionManager.OnActiveSessionsChangedListener mSessionListener; 90 @Nullable private MediaSessionManager mMediaSessionManager; 91 92 public interface AudioSwitchCallback { onPreferenceDataChanged(ListPreference preference)93 void onPreferenceDataChanged(ListPreference preference); 94 } 95 AudioSwitchPreferenceController(Context context, String preferenceKey)96 public AudioSwitchPreferenceController(Context context, String preferenceKey) { 97 super(context, preferenceKey); 98 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 99 mMediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE); 100 mHandler = new Handler(Looper.getMainLooper()); 101 mAudioManagerAudioDeviceCallback = new AudioManagerAudioDeviceCallback(); 102 mReceiver = new WiredHeadsetBroadcastReceiver(); 103 mConnectedDevices = new ArrayList<>(); 104 final FutureTask<LocalBluetoothManager> localBtManagerFutureTask = new FutureTask<>( 105 // Avoid StrictMode ThreadPolicy violation 106 () -> Utils.getLocalBtManager(mContext)); 107 try { 108 localBtManagerFutureTask.run(); 109 mLocalBluetoothManager = localBtManagerFutureTask.get(); 110 } catch (InterruptedException | ExecutionException e) { 111 Log.w(TAG, "Error getting LocalBluetoothManager.", e); 112 return; 113 } 114 if (mLocalBluetoothManager == null) { 115 Log.e(TAG, "Bluetooth is not supported on this device"); 116 return; 117 } 118 mProfileManager = mLocalBluetoothManager.getProfileManager(); 119 120 if (enableOutputSwitcherForSystemRouting()) { 121 mMediaSessionManager = context.getSystemService(MediaSessionManager.class); 122 mSessionListener = new SessionChangeListener(); 123 } else { 124 mMediaSessionManager = null; 125 mSessionListener = null; 126 } 127 } 128 129 /** 130 * Make this method as final, ensure that subclass will checking 131 * the feature flag and they could mistakenly break it via overriding. 132 */ 133 @Override getAvailabilityStatus()134 public final int getAvailabilityStatus() { 135 return FeatureFlagUtils.isEnabled(mContext, FeatureFlags.AUDIO_SWITCHER_SETTINGS) && 136 mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) 137 ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; 138 } 139 140 @Override displayPreference(PreferenceScreen screen)141 public void displayPreference(PreferenceScreen screen) { 142 super.displayPreference(screen); 143 mPreference = screen.findPreference(mPreferenceKey); 144 mPreference.setVisible(false); 145 } 146 147 @Override onStart()148 public void onStart() { 149 if (mLocalBluetoothManager == null) { 150 Log.e(TAG, "Bluetooth is not supported on this device"); 151 return; 152 } 153 mLocalBluetoothManager.setForegroundActivity(mContext); 154 if (!AudioSwitchUtils.isLeAudioProfileReady(mProfileManager)) { 155 if (mProfileManager != null) { 156 mProfileManager.addServiceListener(this); 157 } 158 } 159 register(); 160 } 161 162 @Override onStop()163 public void onStop() { 164 if (mLocalBluetoothManager == null) { 165 Log.e(TAG, "Bluetooth is not supported on this device"); 166 return; 167 } 168 mLocalBluetoothManager.setForegroundActivity(null); 169 if (mProfileManager != null) { 170 mProfileManager.removeServiceListener(this); 171 } 172 unregister(); 173 } 174 175 @Override onBluetoothStateChanged(int bluetoothState)176 public void onBluetoothStateChanged(int bluetoothState) { 177 // To handle the case that Bluetooth on and no connected devices 178 updateState(mPreference); 179 } 180 181 @Override onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile)182 public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) { 183 updateState(mPreference); 184 } 185 186 @Override onAudioModeChanged()187 public void onAudioModeChanged() { 188 updateState(mPreference); 189 } 190 191 @Override onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, int bluetoothProfile)192 public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, 193 int bluetoothProfile) { 194 updateState(mPreference); 195 } 196 197 /** 198 * Indicates a change in the bond state of a remote 199 * device. For example, if a device is bonded (paired). 200 */ 201 @Override onDeviceAdded(CachedBluetoothDevice cachedDevice)202 public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { 203 updateState(mPreference); 204 } 205 206 @Override onServiceConnected()207 public void onServiceConnected() { 208 Log.d(TAG, "onServiceConnected"); 209 if (AudioSwitchUtils.isLeAudioProfileReady(mProfileManager)) { 210 updateState(mPreference); 211 } 212 } 213 214 @Override onServiceDisconnected()215 public void onServiceDisconnected() { 216 Log.d(TAG, "onServiceDisconnected()"); 217 // Do nothing. 218 } 219 setCallback(AudioSwitchCallback callback)220 public void setCallback(AudioSwitchCallback callback) { 221 mAudioSwitchPreferenceCallback = callback; 222 } 223 isStreamFromOutputDevice(int streamType, int device)224 protected boolean isStreamFromOutputDevice(int streamType, int device) { 225 return (device & mAudioManager.getDevicesForStream(streamType)) != 0; 226 } 227 228 /** 229 * get hands free profile(HFP) connected device 230 */ getConnectedHfpDevices()231 protected List<BluetoothDevice> getConnectedHfpDevices() { 232 final List<BluetoothDevice> connectedDevices = new ArrayList<>(); 233 final HeadsetProfile hfpProfile = mProfileManager.getHeadsetProfile(); 234 if (hfpProfile == null) { 235 return connectedDevices; 236 } 237 final List<BluetoothDevice> devices = hfpProfile.getConnectedDevices(); 238 for (BluetoothDevice device : devices) { 239 if (device.isConnected()) { 240 connectedDevices.add(device); 241 } 242 } 243 return connectedDevices; 244 } 245 246 /** 247 * get A2dp devices on all states 248 * (STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED, STATE_DISCONNECTING) 249 */ getConnectedA2dpDevices()250 protected List<BluetoothDevice> getConnectedA2dpDevices() { 251 final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 252 if (a2dpProfile == null) { 253 return new ArrayList<>(); 254 } 255 return a2dpProfile.getConnectedDevices(); 256 } 257 258 /** 259 * Get LE Audio profile connected devices 260 */ getConnectedLeAudioDevices()261 protected List<BluetoothDevice> getConnectedLeAudioDevices() { 262 final List<BluetoothDevice> connectedDevices = new ArrayList<>(); 263 final LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile(); 264 if (leAudioProfile == null) { 265 Log.d(TAG, "LeAudioProfile is null"); 266 return connectedDevices; 267 } 268 final List<BluetoothDevice> devices = leAudioProfile.getConnectedDevices(); 269 if (devices == null) { 270 Log.d(TAG, "No connected LeAudioProfile devices"); 271 return connectedDevices; 272 } 273 for (BluetoothDevice device : devices) { 274 if (device.isConnected() && isDeviceInCachedList(device)) { 275 connectedDevices.add(device); 276 } 277 } 278 return connectedDevices; 279 } 280 281 /** 282 * Confirm if the device exists in the cached devices list. If return true, it means 283 * the device is main device in the LE Audio device group. Otherwise, the device is the member 284 * device in the group. 285 */ isDeviceInCachedList(BluetoothDevice device)286 protected boolean isDeviceInCachedList(BluetoothDevice device) { 287 Collection<CachedBluetoothDevice> cachedDevices = 288 mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy(); 289 for (CachedBluetoothDevice cachedDevice : cachedDevices) { 290 if (cachedDevice.getDevice().equals(device)) { 291 return true; 292 } 293 } 294 return false; 295 } 296 297 /** 298 * get hearing aid profile connected device, exclude other devices with same hiSyncId. 299 */ getConnectedHearingAidDevices()300 protected List<BluetoothDevice> getConnectedHearingAidDevices() { 301 final List<BluetoothDevice> connectedDevices = new ArrayList<>(); 302 final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile(); 303 if (hapProfile == null) { 304 return connectedDevices; 305 } 306 final List<Long> devicesHiSyncIds = new ArrayList<>(); 307 final List<BluetoothDevice> devices = hapProfile.getConnectedDevices(); 308 for (BluetoothDevice device : devices) { 309 final long hiSyncId = hapProfile.getHiSyncId(device); 310 // device with same hiSyncId should not be shown in the UI. 311 // So do not add it into connectedDevices. 312 if (!devicesHiSyncIds.contains(hiSyncId) && device.isConnected()) { 313 devicesHiSyncIds.add(hiSyncId); 314 connectedDevices.add(device); 315 } 316 } 317 return connectedDevices; 318 } 319 320 /** 321 * Find active hearing aid device 322 */ findActiveHearingAidDevice()323 protected BluetoothDevice findActiveHearingAidDevice() { 324 final HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 325 326 if (hearingAidProfile != null) { 327 // The first element is the left active device; the second element is 328 // the right active device. And they will have same hiSyncId. If either 329 // or both side is not active, it will be null on that position. 330 List<BluetoothDevice> activeDevices = hearingAidProfile.getActiveDevices(); 331 for (BluetoothDevice btDevice : activeDevices) { 332 if (btDevice != null && mConnectedDevices.contains(btDevice)) { 333 // also need to check mConnectedDevices, because one of 334 // the device(same hiSyncId) might not be shown in the UI. 335 return btDevice; 336 } 337 } 338 } 339 return null; 340 } 341 342 /** 343 * Find active LE Audio device 344 */ findActiveLeAudioDevice()345 protected BluetoothDevice findActiveLeAudioDevice() { 346 final LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile(); 347 348 if (leAudioProfile != null) { 349 List<BluetoothDevice> activeDevices = leAudioProfile.getActiveDevices(); 350 for (BluetoothDevice leAudioDevice : activeDevices) { 351 if (leAudioDevice != null) { 352 return leAudioDevice; 353 } 354 } 355 } 356 Log.d(TAG, "There is no LE audio profile or no active LE audio device"); 357 return null; 358 } 359 360 /** 361 * Find the active device from the corresponding profile. 362 * 363 * @return the active device. Return null if the 364 * corresponding profile don't have active device. 365 */ findActiveDevice()366 public abstract BluetoothDevice findActiveDevice(); 367 register()368 private void register() { 369 mLocalBluetoothManager.getEventManager().registerCallback(this); 370 mAudioManager.registerAudioDeviceCallback(mAudioManagerAudioDeviceCallback, mHandler); 371 372 // Register for misc other intent broadcasts. 373 IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); 374 intentFilter.addAction(STREAM_DEVICES_CHANGED_ACTION); 375 376 if (enableOutputSwitcherForSystemRouting()) { 377 mContext.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED); 378 if (mMediaSessionManager != null) { 379 mMediaSessionManager.addOnActiveSessionsChangedListener( 380 mSessionListener, null, mHandler); 381 } 382 } else { 383 mContext.registerReceiver(mReceiver, intentFilter); 384 } 385 } 386 unregister()387 private void unregister() { 388 mLocalBluetoothManager.getEventManager().unregisterCallback(this); 389 mAudioManager.unregisterAudioDeviceCallback(mAudioManagerAudioDeviceCallback); 390 mContext.unregisterReceiver(mReceiver); 391 if (enableOutputSwitcherForSystemRouting()) { 392 if (mMediaSessionManager != null) { 393 mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionListener); 394 } 395 } 396 } 397 398 /** Notifications of audio device connection and disconnection events. */ 399 private class AudioManagerAudioDeviceCallback extends AudioDeviceCallback { 400 @Override onAudioDevicesAdded(AudioDeviceInfo[] addedDevices)401 public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { 402 updateState(mPreference); 403 } 404 405 @Override onAudioDevicesRemoved(AudioDeviceInfo[] devices)406 public void onAudioDevicesRemoved(AudioDeviceInfo[] devices) { 407 updateState(mPreference); 408 } 409 } 410 411 /** Receiver for wired headset plugged and unplugged events. */ 412 private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver { 413 @Override onReceive(Context context, Intent intent)414 public void onReceive(Context context, Intent intent) { 415 final String action = intent.getAction(); 416 if (AudioManager.ACTION_HEADSET_PLUG.equals(action) || 417 AudioManager.STREAM_DEVICES_CHANGED_ACTION.equals(action)) { 418 updateState(mPreference); 419 } 420 } 421 } 422 423 private class SessionChangeListener 424 implements MediaSessionManager.OnActiveSessionsChangedListener { 425 @Override onActiveSessionsChanged(List<MediaController> controllers)426 public void onActiveSessionsChanged(List<MediaController> controllers) { 427 updateState(mPreference); 428 } 429 } 430 } 431