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