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