PhonePolicy.java revision 3cc3ca76069837340db03751e64b6df230e5d8cc
1/* 2 * Copyright (C) 2017 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 17package com.android.bluetooth.btservice; 18 19import android.bluetooth.BluetoothA2dp; 20import android.bluetooth.BluetoothAdapter; 21import android.bluetooth.BluetoothDevice; 22import android.bluetooth.BluetoothHeadset; 23import android.bluetooth.BluetoothProfile; 24import android.bluetooth.BluetoothUuid; 25import android.content.BroadcastReceiver; 26import android.content.Context; 27import android.content.Intent; 28import android.content.IntentFilter; 29import android.os.Handler; 30import android.os.Looper; 31import android.os.Message; 32import android.os.ParcelUuid; 33import android.os.Parcelable; 34import android.support.annotation.VisibleForTesting; 35import android.util.Log; 36 37import com.android.bluetooth.a2dp.A2dpService; 38import com.android.bluetooth.hearingaid.HearingAidService; 39import com.android.bluetooth.hfp.HeadsetService; 40import com.android.bluetooth.hid.HidHostService; 41import com.android.bluetooth.pan.PanService; 42import com.android.internal.R; 43 44import java.util.HashSet; 45import java.util.List; 46 47// Describes the phone policy 48// 49// The policy should be as decoupled from the stack as possible. In an ideal world we should not 50// need to have this policy talk with any non-public APIs and one way to enforce that would be to 51// keep this file outside the Bluetooth process. Unfortunately, keeping a separate process alive is 52// an expensive and a tedious task. 53// 54// Best practices: 55// a) PhonePolicy should be ALL private methods 56// -- Use broadcasts which can be listened in on the BroadcastReceiver 57// b) NEVER call from the PhonePolicy into the Java stack, unless public APIs. It is OK to call into 58// the non public versions as long as public versions exist (so that a 3rd party policy can mimick) 59// us. 60// 61// Policy description: 62// 63// Policies are usually governed by outside events that may warrant an action. We talk about various 64// events and the resulting outcome from this policy: 65// 66// 1. Adapter turned ON: At this point we will try to auto-connect the (device, profile) pairs which 67// have PRIORITY_AUTO_CONNECT. The fact that we *only* auto-connect Headset and A2DP is something 68// that is hardcoded and specific to phone policy (see autoConnect() function) 69// 2. When the profile connection-state changes: At this point if a new profile gets CONNECTED we 70// will try to connect other profiles on the same device. This is to avoid collision if devices 71// somehow end up trying to connect at same time or general connection issues. 72class PhonePolicy { 73 private static final boolean DBG = true; 74 private static final String TAG = "BluetoothPhonePolicy"; 75 76 // Message types for the handler (internal messages generated by intents or timeouts) 77 private static final int MESSAGE_PROFILE_CONNECTION_STATE_CHANGED = 1; 78 private static final int MESSAGE_PROFILE_INIT_PRIORITIES = 2; 79 private static final int MESSAGE_CONNECT_OTHER_PROFILES = 3; 80 private static final int MESSAGE_ADAPTER_STATE_TURNED_ON = 4; 81 82 // Timeouts 83 @VisibleForTesting 84 static int sConnectOtherProfilesTimeoutMillis = 6000; // 6s 85 86 private final AdapterService mAdapterService; 87 private final ServiceFactory mFactory; 88 private final Handler mHandler; 89 private final HashSet<BluetoothDevice> mHeadsetRetrySet = new HashSet<>(); 90 private final HashSet<BluetoothDevice> mA2dpRetrySet = new HashSet<>(); 91 private final HashSet<BluetoothDevice> mConnectOtherProfilesDeviceSet = new HashSet<>(); 92 93 // Broadcast receiver for all changes to states of various profiles 94 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 95 @Override 96 public void onReceive(Context context, Intent intent) { 97 String action = intent.getAction(); 98 if (action == null) { 99 errorLog("Received intent with null action"); 100 return; 101 } 102 switch (action) { 103 case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED: 104 mHandler.obtainMessage(MESSAGE_PROFILE_CONNECTION_STATE_CHANGED, 105 BluetoothProfile.HEADSET, -1, // No-op argument 106 intent).sendToTarget(); 107 break; 108 case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED: 109 mHandler.obtainMessage(MESSAGE_PROFILE_CONNECTION_STATE_CHANGED, 110 BluetoothProfile.A2DP, -1, // No-op argument 111 intent).sendToTarget(); 112 break; 113 case BluetoothAdapter.ACTION_STATE_CHANGED: 114 // Only pass the message on if the adapter has actually changed state from 115 // non-ON to ON. NOTE: ON is the state depicting BREDR ON and not just BLE ON. 116 int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); 117 if (newState == BluetoothAdapter.STATE_ON) { 118 mHandler.obtainMessage(MESSAGE_ADAPTER_STATE_TURNED_ON).sendToTarget(); 119 } 120 break; 121 case BluetoothDevice.ACTION_UUID: 122 mHandler.obtainMessage(MESSAGE_PROFILE_INIT_PRIORITIES, intent).sendToTarget(); 123 break; 124 default: 125 Log.e(TAG, "Received unexpected intent, action=" + action); 126 break; 127 } 128 } 129 }; 130 131 @VisibleForTesting 132 BroadcastReceiver getBroadcastReceiver() { 133 return mReceiver; 134 } 135 136 // Handler to handoff intents to class thread 137 class PhonePolicyHandler extends Handler { 138 PhonePolicyHandler(Looper looper) { 139 super(looper); 140 } 141 142 @Override 143 public void handleMessage(Message msg) { 144 switch (msg.what) { 145 case MESSAGE_PROFILE_INIT_PRIORITIES: { 146 Intent intent = (Intent) msg.obj; 147 BluetoothDevice device = 148 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 149 Parcelable[] uuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID); 150 debugLog("Received ACTION_UUID for device " + device); 151 if (uuids != null) { 152 ParcelUuid[] uuidsToSend = new ParcelUuid[uuids.length]; 153 for (int i = 0; i < uuidsToSend.length; i++) { 154 uuidsToSend[i] = (ParcelUuid) uuids[i]; 155 debugLog("index=" + i + "uuid=" + uuidsToSend[i]); 156 } 157 processInitProfilePriorities(device, uuidsToSend); 158 } 159 } 160 break; 161 162 case MESSAGE_PROFILE_CONNECTION_STATE_CHANGED: { 163 Intent intent = (Intent) msg.obj; 164 BluetoothDevice device = 165 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 166 int prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1); 167 int nextState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 168 processProfileStateChanged(device, msg.arg1, nextState, prevState); 169 } 170 break; 171 172 case MESSAGE_CONNECT_OTHER_PROFILES: { 173 // Called when we try connect some profiles in processConnectOtherProfiles but 174 // we send a delayed message to try connecting the remaining profiles 175 BluetoothDevice device = (BluetoothDevice) msg.obj; 176 processConnectOtherProfiles(device); 177 mConnectOtherProfilesDeviceSet.remove(device); 178 break; 179 } 180 case MESSAGE_ADAPTER_STATE_TURNED_ON: 181 // Call auto connect when adapter switches state to ON 182 resetStates(); 183 autoConnect(); 184 break; 185 } 186 } 187 } 188 189 ; 190 191 // Policy API functions for lifecycle management (protected) 192 protected void start() { 193 IntentFilter filter = new IntentFilter(); 194 filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); 195 filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); 196 filter.addAction(BluetoothDevice.ACTION_UUID); 197 filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); 198 mAdapterService.registerReceiver(mReceiver, filter); 199 } 200 201 protected void cleanup() { 202 mAdapterService.unregisterReceiver(mReceiver); 203 resetStates(); 204 } 205 206 PhonePolicy(AdapterService service, ServiceFactory factory) { 207 mAdapterService = service; 208 mFactory = factory; 209 mHandler = new PhonePolicyHandler(service.getMainLooper()); 210 } 211 212 // Policy implementation, all functions MUST be private 213 private void processInitProfilePriorities(BluetoothDevice device, ParcelUuid[] uuids) { 214 debugLog("processInitProfilePriorities() - device " + device); 215 HidHostService hidService = mFactory.getHidHostService(); 216 A2dpService a2dpService = mFactory.getA2dpService(); 217 HeadsetService headsetService = mFactory.getHeadsetService(); 218 PanService panService = mFactory.getPanService(); 219 HearingAidService hearingAidService = mFactory.getHearingAidService(); 220 221 // Set profile priorities only for the profiles discovered on the remote device. 222 // This avoids needless auto-connect attempts to profiles non-existent on the remote device 223 if ((hidService != null) && (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hid) 224 || BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) && ( 225 hidService.getPriority(device) == BluetoothProfile.PRIORITY_UNDEFINED)) { 226 hidService.setPriority(device, BluetoothProfile.PRIORITY_ON); 227 } 228 229 // If we do not have a stored priority for HFP/A2DP (all roles) then default to on. 230 if ((headsetService != null) && ((BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HSP) 231 || BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Handsfree)) && ( 232 headsetService.getPriority(device) == BluetoothProfile.PRIORITY_UNDEFINED))) { 233 headsetService.setPriority(device, BluetoothProfile.PRIORITY_ON); 234 } 235 236 if ((a2dpService != null) && (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.AudioSink) 237 || BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.AdvAudioDist)) && ( 238 a2dpService.getPriority(device) == BluetoothProfile.PRIORITY_UNDEFINED)) { 239 a2dpService.setPriority(device, BluetoothProfile.PRIORITY_ON); 240 } 241 242 if ((panService != null) && (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.PANU) && ( 243 panService.getPriority(device) == BluetoothProfile.PRIORITY_UNDEFINED) 244 && mAdapterService.getResources() 245 .getBoolean(R.bool.config_bluetooth_pan_enable_autoconnect))) { 246 panService.setPriority(device, BluetoothProfile.PRIORITY_ON); 247 } 248 249 if ((hearingAidService != null) 250 && BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HearingAid) 251 && (hearingAidService.getPriority(device) == BluetoothProfile.PRIORITY_UNDEFINED)) { 252 debugLog("setting hearing aid profile priority for device " + device); 253 hearingAidService.setPriority(device, BluetoothProfile.PRIORITY_ON); 254 } 255 } 256 257 private void processProfileStateChanged(BluetoothDevice device, int profileId, int nextState, 258 int prevState) { 259 debugLog("processProfileStateChanged, device=" + device + ", profile=" + profileId + ", " 260 + prevState + " -> " + nextState); 261 if (((profileId == BluetoothProfile.A2DP) || (profileId == BluetoothProfile.HEADSET))) { 262 if (nextState == BluetoothProfile.STATE_CONNECTED) { 263 switch (profileId) { 264 case BluetoothProfile.A2DP: 265 mA2dpRetrySet.remove(device); 266 break; 267 case BluetoothProfile.HEADSET: 268 mHeadsetRetrySet.remove(device); 269 break; 270 } 271 connectOtherProfile(device); 272 setProfileAutoConnectionPriority(device, profileId, true); 273 } 274 if (prevState == BluetoothProfile.STATE_CONNECTING 275 && nextState == BluetoothProfile.STATE_DISCONNECTED) { 276 setProfileAutoConnectionPriority(device, profileId, false); 277 } 278 279 } 280 } 281 282 private void resetStates() { 283 mHeadsetRetrySet.clear(); 284 mA2dpRetrySet.clear(); 285 } 286 287 private void autoConnect() { 288 if (mAdapterService.getState() != BluetoothAdapter.STATE_ON) { 289 errorLog("autoConnect() - BT is not ON. Exiting autoConnect"); 290 return; 291 } 292 293 if (!mAdapterService.isQuietModeEnabled()) { 294 debugLog("autoConnect() - Initiate auto connection on BT on..."); 295 // Phone profiles. 296 autoConnectHeadset(); 297 autoConnectA2dp(); 298 } else { 299 debugLog("autoConnect() - BT is in quiet mode. Not initiating auto connections"); 300 } 301 } 302 303 private void autoConnectHeadset() { 304 final HeadsetService hsService = mFactory.getHeadsetService(); 305 if (hsService == null) { 306 errorLog("autoConnectHeadset, service is null"); 307 return; 308 } 309 final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices(); 310 if (bondedDevices == null) { 311 errorLog("autoConnectHeadset, bondedDevices are null"); 312 return; 313 } 314 for (BluetoothDevice device : bondedDevices) { 315 int priority = hsService.getPriority(device); 316 debugLog("autoConnectHeadset, attempt auto-connect with device " + device 317 + " priority " + priority); 318 if (priority == BluetoothProfile.PRIORITY_AUTO_CONNECT) { 319 debugLog("autoConnectHeadset, Connecting HFP with " + device); 320 hsService.connect(device); 321 } else { 322 debugLog("autoConnectHeadset, skipped auto-connect with device " + device 323 + " priority " + priority); 324 } 325 } 326 } 327 328 private void autoConnectA2dp() { 329 final A2dpService a2dpService = mFactory.getA2dpService(); 330 if (a2dpService == null) { 331 errorLog("autoConnectA2dp, service is null"); 332 return; 333 } 334 final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices(); 335 if (bondedDevices == null) { 336 errorLog("autoConnectA2dp, bondedDevices are null"); 337 return; 338 } 339 for (BluetoothDevice device : bondedDevices) { 340 int priority = a2dpService.getPriority(device); 341 debugLog("autoConnectA2dp, attempt auto-connect with device " + device 342 + " priority " + priority); 343 if (priority == BluetoothProfile.PRIORITY_AUTO_CONNECT) { 344 debugLog("autoConnectA2dp, connecting A2DP with " + device); 345 a2dpService.connect(device); 346 } else { 347 debugLog("autoConnectA2dp, skipped auto-connect with device " + device 348 + " priority " + priority); 349 } 350 } 351 } 352 353 private void connectOtherProfile(BluetoothDevice device) { 354 if (mAdapterService.isQuietModeEnabled()) { 355 debugLog("connectOtherProfile: in quiet mode, skip connect other profile " + device); 356 return; 357 } 358 if (mConnectOtherProfilesDeviceSet.contains(device)) { 359 debugLog("connectOtherProfile: already scheduled callback for " + device); 360 return; 361 } 362 mConnectOtherProfilesDeviceSet.add(device); 363 Message m = mHandler.obtainMessage(MESSAGE_CONNECT_OTHER_PROFILES); 364 m.obj = device; 365 mHandler.sendMessageDelayed(m, sConnectOtherProfilesTimeoutMillis); 366 } 367 368 // This function is called whenever a profile is connected. This allows any other bluetooth 369 // profiles which are not already connected or in the process of connecting to attempt to 370 // connect to the device that initiated the connection. In the event that this function is 371 // invoked and there are no current bluetooth connections no new profiles will be connected. 372 private void processConnectOtherProfiles(BluetoothDevice device) { 373 debugLog("processConnectOtherProfiles, device=" + device); 374 if (mAdapterService.getState() != BluetoothAdapter.STATE_ON) { 375 warnLog("processConnectOtherProfiles, adapter is not ON " + mAdapterService.getState()); 376 return; 377 } 378 HeadsetService hsService = mFactory.getHeadsetService(); 379 A2dpService a2dpService = mFactory.getA2dpService(); 380 PanService panService = mFactory.getPanService(); 381 382 boolean atLeastOneProfileConnectedForDevice = false; 383 boolean allProfilesEmpty = true; 384 List<BluetoothDevice> a2dpConnDevList = null; 385 List<BluetoothDevice> hsConnDevList = null; 386 List<BluetoothDevice> panConnDevList = null; 387 388 if (hsService != null) { 389 hsConnDevList = hsService.getConnectedDevices(); 390 allProfilesEmpty &= hsConnDevList.isEmpty(); 391 atLeastOneProfileConnectedForDevice |= hsConnDevList.contains(device); 392 } 393 if (a2dpService != null) { 394 a2dpConnDevList = a2dpService.getConnectedDevices(); 395 allProfilesEmpty &= a2dpConnDevList.isEmpty(); 396 atLeastOneProfileConnectedForDevice |= a2dpConnDevList.contains(device); 397 } 398 if (panService != null) { 399 panConnDevList = panService.getConnectedDevices(); 400 allProfilesEmpty &= panConnDevList.isEmpty(); 401 atLeastOneProfileConnectedForDevice |= panConnDevList.contains(device); 402 } 403 404 if (!atLeastOneProfileConnectedForDevice) { 405 // Consider this device as fully disconnected, don't bother connecting others 406 debugLog("processConnectOtherProfiles, all profiles disconnected for " + device); 407 mHeadsetRetrySet.remove(device); 408 mA2dpRetrySet.remove(device); 409 if (allProfilesEmpty) { 410 debugLog("processConnectOtherProfiles, all profiles disconnected for all devices"); 411 // reset retry status so that in the next round we can start retrying connections 412 resetStates(); 413 } 414 return; 415 } 416 417 if (hsService != null) { 418 if (!mHeadsetRetrySet.contains(device) && (hsService.getPriority(device) 419 >= BluetoothProfile.PRIORITY_ON) && (hsService.getConnectionState(device) 420 == BluetoothProfile.STATE_DISCONNECTED)) { 421 debugLog("Retrying connection to Headset with device " + device); 422 mHeadsetRetrySet.add(device); 423 hsService.connect(device); 424 } 425 } 426 if (a2dpService != null) { 427 if (!mA2dpRetrySet.contains(device) && (a2dpService.getPriority(device) 428 >= BluetoothProfile.PRIORITY_ON) && (a2dpService.getConnectionState(device) 429 == BluetoothProfile.STATE_DISCONNECTED)) { 430 debugLog("Retrying connection to A2DP with device " + device); 431 mA2dpRetrySet.add(device); 432 a2dpService.connect(device); 433 } 434 } 435 if (panService != null) { 436 // TODO: the panConnDevList.isEmpty() check below should be removed once 437 // Multi-PAN is supported. 438 if (panConnDevList.isEmpty() && (panService.getPriority(device) 439 >= BluetoothProfile.PRIORITY_ON) && (panService.getConnectionState(device) 440 == BluetoothProfile.STATE_DISCONNECTED)) { 441 debugLog("Retrying connection to PAN with device " + device); 442 panService.connect(device); 443 } 444 } 445 } 446 447 private void setProfileAutoConnectionPriority(BluetoothDevice device, int profileId, 448 boolean autoConnect) { 449 debugLog("setProfileAutoConnectionPriority: device=" + device + ", profile=" + profileId 450 + ", autoConnect=" + autoConnect); 451 switch (profileId) { 452 case BluetoothProfile.HEADSET: { 453 HeadsetService hsService = mFactory.getHeadsetService(); 454 if (hsService == null) { 455 warnLog("setProfileAutoConnectionPriority: HEADSET service is null"); 456 break; 457 } 458 removeAutoConnectFromDisconnectedHeadsets(hsService); 459 if (autoConnect) { 460 hsService.setPriority(device, BluetoothProfile.PRIORITY_AUTO_CONNECT); 461 } 462 break; 463 } 464 case BluetoothProfile.A2DP: { 465 A2dpService a2dpService = mFactory.getA2dpService(); 466 if (a2dpService == null) { 467 warnLog("setProfileAutoConnectionPriority: A2DP service is null"); 468 break; 469 } 470 removeAutoConnectFromDisconnectedA2dpSinks(a2dpService); 471 if (autoConnect) { 472 a2dpService.setPriority(device, BluetoothProfile.PRIORITY_AUTO_CONNECT); 473 } 474 break; 475 } 476 default: 477 Log.w(TAG, "Tried to set AutoConnect priority on invalid profile " + profileId); 478 break; 479 } 480 } 481 482 private void removeAutoConnectFromDisconnectedHeadsets(HeadsetService hsService) { 483 List<BluetoothDevice> connectedDeviceList = hsService.getConnectedDevices(); 484 for (BluetoothDevice device : mAdapterService.getBondedDevices()) { 485 if (hsService.getPriority(device) >= BluetoothProfile.PRIORITY_AUTO_CONNECT 486 && !connectedDeviceList.contains(device)) { 487 debugLog("removeAutoConnectFromDisconnectedHeadsets, device " + device 488 + " PRIORITY_ON"); 489 hsService.setPriority(device, BluetoothProfile.PRIORITY_ON); 490 } 491 } 492 } 493 494 private void removeAutoConnectFromDisconnectedA2dpSinks(A2dpService a2dpService) { 495 List<BluetoothDevice> connectedDeviceList = a2dpService.getConnectedDevices(); 496 for (BluetoothDevice device : mAdapterService.getBondedDevices()) { 497 if (a2dpService.getPriority(device) >= BluetoothProfile.PRIORITY_AUTO_CONNECT 498 && !connectedDeviceList.contains(device)) { 499 debugLog("removeAutoConnectFromDisconnectedA2dpSinks, device " + device 500 + " PRIORITY_ON"); 501 a2dpService.setPriority(device, BluetoothProfile.PRIORITY_ON); 502 } 503 } 504 } 505 506 private static void debugLog(String msg) { 507 if (DBG) { 508 Log.i(TAG, msg); 509 } 510 } 511 512 private static void warnLog(String msg) { 513 Log.w(TAG, msg); 514 } 515 516 private static void errorLog(String msg) { 517 Log.e(TAG, msg); 518 } 519} 520