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