1 /* 2 * Copyright (C) 2008 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.bluetooth; 18 19 import com.android.settings.R; 20 21 import android.bluetooth.BluetoothA2dp; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothHeadset; 24 import android.bluetooth.BluetoothUuid; 25 import android.os.Handler; 26 import android.os.ParcelUuid; 27 import android.util.Log; 28 29 import java.util.HashMap; 30 import java.util.HashSet; 31 import java.util.Iterator; 32 import java.util.LinkedList; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.Set; 36 37 /** 38 * LocalBluetoothProfileManager is an abstract class defining the basic 39 * functionality related to a profile. 40 */ 41 public abstract class LocalBluetoothProfileManager { 42 private static final String TAG = "LocalBluetoothProfileManager"; 43 44 /* package */ static final ParcelUuid[] HEADSET_PROFILE_UUIDS = new ParcelUuid[] { 45 BluetoothUuid.HSP, 46 BluetoothUuid.Handsfree, 47 }; 48 49 /* package */ static final ParcelUuid[] A2DP_PROFILE_UUIDS = new ParcelUuid[] { 50 BluetoothUuid.AudioSink, 51 BluetoothUuid.AdvAudioDist, 52 }; 53 54 /* package */ static final ParcelUuid[] OPP_PROFILE_UUIDS = new ParcelUuid[] { 55 BluetoothUuid.ObexObjectPush 56 }; 57 58 /** 59 * An interface for notifying BluetoothHeadset IPC clients when they have 60 * been connected to the BluetoothHeadset service. 61 */ 62 public interface ServiceListener { 63 /** 64 * Called to notify the client when this proxy object has been 65 * connected to the BluetoothHeadset service. Clients must wait for 66 * this callback before making IPC calls on the BluetoothHeadset 67 * service. 68 */ onServiceConnected()69 public void onServiceConnected(); 70 71 /** 72 * Called to notify the client that this proxy object has been 73 * disconnected from the BluetoothHeadset service. Clients must not 74 * make IPC calls on the BluetoothHeadset service after this callback. 75 * This callback will currently only occur if the application hosting 76 * the BluetoothHeadset service, but may be called more often in future. 77 */ onServiceDisconnected()78 public void onServiceDisconnected(); 79 } 80 81 // TODO: close profiles when we're shutting down 82 private static Map<Profile, LocalBluetoothProfileManager> sProfileMap = 83 new HashMap<Profile, LocalBluetoothProfileManager>(); 84 85 protected LocalBluetoothManager mLocalManager; 86 init(LocalBluetoothManager localManager)87 public static void init(LocalBluetoothManager localManager) { 88 synchronized (sProfileMap) { 89 if (sProfileMap.size() == 0) { 90 LocalBluetoothProfileManager profileManager; 91 92 profileManager = new A2dpProfileManager(localManager); 93 sProfileMap.put(Profile.A2DP, profileManager); 94 95 profileManager = new HeadsetProfileManager(localManager); 96 sProfileMap.put(Profile.HEADSET, profileManager); 97 98 profileManager = new OppProfileManager(localManager); 99 sProfileMap.put(Profile.OPP, profileManager); 100 } 101 } 102 } 103 104 private static LinkedList<ServiceListener> mServiceListeners = new LinkedList<ServiceListener>(); 105 addServiceListener(ServiceListener l)106 public static void addServiceListener(ServiceListener l) { 107 mServiceListeners.add(l); 108 } 109 removeServiceListener(ServiceListener l)110 public static void removeServiceListener(ServiceListener l) { 111 mServiceListeners.remove(l); 112 } 113 isManagerReady()114 public static boolean isManagerReady() { 115 // Getting just the headset profile is fine for now. Will need to deal with A2DP 116 // and others if they aren't always in a ready state. 117 LocalBluetoothProfileManager profileManager = sProfileMap.get(Profile.HEADSET); 118 if (profileManager == null) { 119 return sProfileMap.size() > 0; 120 } 121 return profileManager.isProfileReady(); 122 } 123 getProfileManager(LocalBluetoothManager localManager, Profile profile)124 public static LocalBluetoothProfileManager getProfileManager(LocalBluetoothManager localManager, 125 Profile profile) { 126 // Note: This code assumes that "localManager" is same as the 127 // LocalBluetoothManager that was used to initialize the sProfileMap. 128 // If that every changes, we can't just keep one copy of sProfileMap. 129 synchronized (sProfileMap) { 130 LocalBluetoothProfileManager profileManager = sProfileMap.get(profile); 131 if (profileManager == null) { 132 Log.e(TAG, "profileManager can't be found for " + profile.toString()); 133 } 134 return profileManager; 135 } 136 } 137 138 /** 139 * Temporary method to fill profiles based on a device's class. 140 * 141 * NOTE: This list happens to define the connection order. We should put this logic in a more 142 * well known place when this method is no longer temporary. 143 * @param uuids of the remote device 144 * @param profiles The list of profiles to fill 145 */ updateProfiles(ParcelUuid[] uuids, List<Profile> profiles)146 public static void updateProfiles(ParcelUuid[] uuids, List<Profile> profiles) { 147 profiles.clear(); 148 149 if (uuids == null) { 150 return; 151 } 152 153 if (BluetoothUuid.containsAnyUuid(uuids, HEADSET_PROFILE_UUIDS)) { 154 profiles.add(Profile.HEADSET); 155 } 156 157 if (BluetoothUuid.containsAnyUuid(uuids, A2DP_PROFILE_UUIDS)) { 158 profiles.add(Profile.A2DP); 159 } 160 161 if (BluetoothUuid.containsAnyUuid(uuids, OPP_PROFILE_UUIDS)) { 162 profiles.add(Profile.OPP); 163 } 164 } 165 LocalBluetoothProfileManager(LocalBluetoothManager localManager)166 protected LocalBluetoothProfileManager(LocalBluetoothManager localManager) { 167 mLocalManager = localManager; 168 } 169 getConnectedDevices()170 public abstract Set<BluetoothDevice> getConnectedDevices(); 171 connect(BluetoothDevice device)172 public abstract boolean connect(BluetoothDevice device); 173 disconnect(BluetoothDevice device)174 public abstract boolean disconnect(BluetoothDevice device); 175 getConnectionStatus(BluetoothDevice device)176 public abstract int getConnectionStatus(BluetoothDevice device); 177 getSummary(BluetoothDevice device)178 public abstract int getSummary(BluetoothDevice device); 179 convertState(int a2dpState)180 public abstract int convertState(int a2dpState); 181 isPreferred(BluetoothDevice device)182 public abstract boolean isPreferred(BluetoothDevice device); 183 getPreferred(BluetoothDevice device)184 public abstract int getPreferred(BluetoothDevice device); 185 setPreferred(BluetoothDevice device, boolean preferred)186 public abstract void setPreferred(BluetoothDevice device, boolean preferred); 187 isConnected(BluetoothDevice device)188 public boolean isConnected(BluetoothDevice device) { 189 return SettingsBtStatus.isConnectionStatusConnected(getConnectionStatus(device)); 190 } 191 isProfileReady()192 public abstract boolean isProfileReady(); 193 194 // TODO: int instead of enum 195 public enum Profile { 196 HEADSET(R.string.bluetooth_profile_headset), 197 A2DP(R.string.bluetooth_profile_a2dp), 198 OPP(R.string.bluetooth_profile_opp); 199 200 public final int localizedString; 201 Profile(int localizedString)202 private Profile(int localizedString) { 203 this.localizedString = localizedString; 204 } 205 } 206 207 /** 208 * A2dpProfileManager is an abstraction for the {@link BluetoothA2dp} service. 209 */ 210 private static class A2dpProfileManager extends LocalBluetoothProfileManager { 211 private BluetoothA2dp mService; 212 A2dpProfileManager(LocalBluetoothManager localManager)213 public A2dpProfileManager(LocalBluetoothManager localManager) { 214 super(localManager); 215 mService = new BluetoothA2dp(localManager.getContext()); 216 } 217 218 @Override getConnectedDevices()219 public Set<BluetoothDevice> getConnectedDevices() { 220 return mService.getNonDisconnectedSinks(); 221 } 222 223 @Override connect(BluetoothDevice device)224 public boolean connect(BluetoothDevice device) { 225 Set<BluetoothDevice> sinks = mService.getNonDisconnectedSinks(); 226 if (sinks != null) { 227 for (BluetoothDevice sink : sinks) { 228 mService.disconnectSink(sink); 229 } 230 } 231 return mService.connectSink(device); 232 } 233 234 @Override disconnect(BluetoothDevice device)235 public boolean disconnect(BluetoothDevice device) { 236 // Downgrade priority as user is disconnecting the sink. 237 if (mService.getSinkPriority(device) > BluetoothA2dp.PRIORITY_ON) { 238 mService.setSinkPriority(device, BluetoothA2dp.PRIORITY_ON); 239 } 240 return mService.disconnectSink(device); 241 } 242 243 @Override getConnectionStatus(BluetoothDevice device)244 public int getConnectionStatus(BluetoothDevice device) { 245 return convertState(mService.getSinkState(device)); 246 } 247 248 @Override getSummary(BluetoothDevice device)249 public int getSummary(BluetoothDevice device) { 250 int connectionStatus = getConnectionStatus(device); 251 252 if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus)) { 253 return R.string.bluetooth_a2dp_profile_summary_connected; 254 } else { 255 return SettingsBtStatus.getConnectionStatusSummary(connectionStatus); 256 } 257 } 258 259 @Override isPreferred(BluetoothDevice device)260 public boolean isPreferred(BluetoothDevice device) { 261 return mService.getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF; 262 } 263 264 @Override getPreferred(BluetoothDevice device)265 public int getPreferred(BluetoothDevice device) { 266 return mService.getSinkPriority(device); 267 } 268 269 @Override setPreferred(BluetoothDevice device, boolean preferred)270 public void setPreferred(BluetoothDevice device, boolean preferred) { 271 if (preferred) { 272 if (mService.getSinkPriority(device) < BluetoothA2dp.PRIORITY_ON) { 273 mService.setSinkPriority(device, BluetoothA2dp.PRIORITY_ON); 274 } 275 } else { 276 mService.setSinkPriority(device, BluetoothA2dp.PRIORITY_OFF); 277 } 278 } 279 280 @Override convertState(int a2dpState)281 public int convertState(int a2dpState) { 282 switch (a2dpState) { 283 case BluetoothA2dp.STATE_CONNECTED: 284 return SettingsBtStatus.CONNECTION_STATUS_CONNECTED; 285 case BluetoothA2dp.STATE_CONNECTING: 286 return SettingsBtStatus.CONNECTION_STATUS_CONNECTING; 287 case BluetoothA2dp.STATE_DISCONNECTED: 288 return SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED; 289 case BluetoothA2dp.STATE_DISCONNECTING: 290 return SettingsBtStatus.CONNECTION_STATUS_DISCONNECTING; 291 case BluetoothA2dp.STATE_PLAYING: 292 return SettingsBtStatus.CONNECTION_STATUS_ACTIVE; 293 default: 294 return SettingsBtStatus.CONNECTION_STATUS_UNKNOWN; 295 } 296 } 297 298 @Override isProfileReady()299 public boolean isProfileReady() { 300 return true; 301 } 302 } 303 304 /** 305 * HeadsetProfileManager is an abstraction for the {@link BluetoothHeadset} service. 306 */ 307 private static class HeadsetProfileManager extends LocalBluetoothProfileManager 308 implements BluetoothHeadset.ServiceListener { 309 private BluetoothHeadset mService; 310 private Handler mUiHandler = new Handler(); 311 private boolean profileReady = false; 312 private BluetoothDevice mDelayedConnectDevice = null; 313 private BluetoothDevice mDelayedDisconnectDevice = null; 314 HeadsetProfileManager(LocalBluetoothManager localManager)315 public HeadsetProfileManager(LocalBluetoothManager localManager) { 316 super(localManager); 317 mService = new BluetoothHeadset(localManager.getContext(), this); 318 } 319 onServiceConnected()320 public void onServiceConnected() { 321 profileReady = true; 322 // This could be called on a non-UI thread, funnel to UI thread. 323 // Delay for a few seconds to allow other proxies to connect. 324 mUiHandler.postDelayed(new Runnable() { 325 public void run() { 326 BluetoothDevice device = mService.getCurrentHeadset(); 327 328 if (mDelayedConnectDevice != null) { 329 Log.i(TAG, "service ready: connecting..."); 330 BluetoothDevice newDevice = mDelayedConnectDevice; 331 mDelayedConnectDevice = null; 332 333 if (!newDevice.equals(device)) { 334 if (device != null) { 335 Log.i(TAG, "disconnecting old headset"); 336 mService.disconnectHeadset(device); 337 } 338 Log.i(TAG, "connecting to pending headset"); 339 mService.connectHeadset(newDevice); 340 } 341 } else if (mDelayedDisconnectDevice != null) { 342 Log.i(TAG, "service ready: disconnecting..."); 343 if (mDelayedDisconnectDevice.equals(device)) { 344 Log.i(TAG, "disconnecting headset"); 345 mService.disconnectHeadset(device); 346 } 347 mDelayedDisconnectDevice = null; 348 } else { 349 /* 350 * We just bound to the service, so refresh the UI of the 351 * headset device. 352 */ 353 if (device == null) return; 354 mLocalManager.getCachedDeviceManager() 355 .onProfileStateChanged(device, Profile.HEADSET, 356 BluetoothHeadset.STATE_CONNECTED); 357 } 358 } 359 }, 2000); // wait 2 seconds for other proxies to connect 360 361 if (mServiceListeners.size() > 0) { 362 Iterator<ServiceListener> it = mServiceListeners.iterator(); 363 while(it.hasNext()) { 364 it.next().onServiceConnected(); 365 } 366 } 367 } 368 onServiceDisconnected()369 public void onServiceDisconnected() { 370 profileReady = false; 371 if (mServiceListeners.size() > 0) { 372 Iterator<ServiceListener> it = mServiceListeners.iterator(); 373 while(it.hasNext()) { 374 it.next().onServiceDisconnected(); 375 } 376 } 377 } 378 379 @Override isProfileReady()380 public boolean isProfileReady() { 381 return profileReady; 382 } 383 384 @Override getConnectedDevices()385 public Set<BluetoothDevice> getConnectedDevices() { 386 Set<BluetoothDevice> devices = null; 387 BluetoothDevice device = mService.getCurrentHeadset(); 388 if (device != null) { 389 devices = new HashSet<BluetoothDevice>(); 390 devices.add(device); 391 } 392 return devices; 393 } 394 395 @Override connect(BluetoothDevice device)396 public boolean connect(BluetoothDevice device) { 397 // Delay connection until onServiceConnected() if the 398 // manager isn't ready 399 if (!isManagerReady()) { 400 Log.w(TAG, "HeadsetProfileManager delaying connect, " 401 + "manager not ready"); 402 mDelayedConnectDevice = device; 403 mDelayedDisconnectDevice = null; 404 return true; // hopefully it will succeed 405 } 406 407 // Since connectHeadset fails if already connected to a headset, we 408 // disconnect from any headset first 409 BluetoothDevice currDevice = mService.getCurrentHeadset(); 410 if (currDevice != null) { 411 mService.disconnectHeadset(currDevice); 412 } 413 return mService.connectHeadset(device); 414 } 415 416 @Override disconnect(BluetoothDevice device)417 public boolean disconnect(BluetoothDevice device) { 418 // Delay connection until onServiceConnected() if the 419 // manager isn't ready 420 if (!isManagerReady()) { 421 Log.w(TAG, "HeadsetProfileManager delaying disconnect, " 422 + "manager not ready"); 423 mDelayedConnectDevice = null; 424 mDelayedDisconnectDevice = device; 425 return true; // hopefully it will succeed 426 } 427 428 BluetoothDevice currDevice = mService.getCurrentHeadset(); 429 if (currDevice != null && currDevice.equals(device)) { 430 // Downgrade prority as user is disconnecting the headset. 431 if (mService.getPriority(device) > BluetoothHeadset.PRIORITY_ON) { 432 mService.setPriority(device, BluetoothHeadset.PRIORITY_ON); 433 } 434 return mService.disconnectHeadset(device); 435 } else { 436 return false; 437 } 438 } 439 440 @Override getConnectionStatus(BluetoothDevice device)441 public int getConnectionStatus(BluetoothDevice device) { 442 BluetoothDevice currentDevice = mService.getCurrentHeadset(); 443 return currentDevice != null && currentDevice.equals(device) 444 ? convertState(mService.getState(device)) 445 : SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED; 446 } 447 448 @Override getSummary(BluetoothDevice device)449 public int getSummary(BluetoothDevice device) { 450 int connectionStatus = getConnectionStatus(device); 451 452 if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus)) { 453 return R.string.bluetooth_headset_profile_summary_connected; 454 } else { 455 return SettingsBtStatus.getConnectionStatusSummary(connectionStatus); 456 } 457 } 458 459 @Override isPreferred(BluetoothDevice device)460 public boolean isPreferred(BluetoothDevice device) { 461 return mService.getPriority(device) > BluetoothHeadset.PRIORITY_OFF; 462 } 463 464 @Override getPreferred(BluetoothDevice device)465 public int getPreferred(BluetoothDevice device) { 466 return mService.getPriority(device); 467 } 468 469 @Override setPreferred(BluetoothDevice device, boolean preferred)470 public void setPreferred(BluetoothDevice device, boolean preferred) { 471 if (preferred) { 472 if (mService.getPriority(device) < BluetoothHeadset.PRIORITY_ON) { 473 mService.setPriority(device, BluetoothHeadset.PRIORITY_ON); 474 } 475 } else { 476 mService.setPriority(device, BluetoothHeadset.PRIORITY_OFF); 477 } 478 } 479 480 @Override convertState(int headsetState)481 public int convertState(int headsetState) { 482 switch (headsetState) { 483 case BluetoothHeadset.STATE_CONNECTED: 484 return SettingsBtStatus.CONNECTION_STATUS_CONNECTED; 485 case BluetoothHeadset.STATE_CONNECTING: 486 return SettingsBtStatus.CONNECTION_STATUS_CONNECTING; 487 case BluetoothHeadset.STATE_DISCONNECTED: 488 return SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED; 489 default: 490 return SettingsBtStatus.CONNECTION_STATUS_UNKNOWN; 491 } 492 } 493 } 494 495 /** 496 * OppProfileManager 497 */ 498 private static class OppProfileManager extends LocalBluetoothProfileManager { 499 OppProfileManager(LocalBluetoothManager localManager)500 public OppProfileManager(LocalBluetoothManager localManager) { 501 super(localManager); 502 } 503 504 @Override getConnectedDevices()505 public Set<BluetoothDevice> getConnectedDevices() { 506 return null; 507 } 508 509 @Override connect(BluetoothDevice device)510 public boolean connect(BluetoothDevice device) { 511 return false; 512 } 513 514 @Override disconnect(BluetoothDevice device)515 public boolean disconnect(BluetoothDevice device) { 516 return false; 517 } 518 519 @Override getConnectionStatus(BluetoothDevice device)520 public int getConnectionStatus(BluetoothDevice device) { 521 return -1; 522 } 523 524 @Override getSummary(BluetoothDevice device)525 public int getSummary(BluetoothDevice device) { 526 int connectionStatus = getConnectionStatus(device); 527 528 if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus)) { 529 return R.string.bluetooth_opp_profile_summary_connected; 530 } else { 531 return R.string.bluetooth_opp_profile_summary_not_connected; 532 } 533 } 534 535 @Override isPreferred(BluetoothDevice device)536 public boolean isPreferred(BluetoothDevice device) { 537 return false; 538 } 539 540 @Override getPreferred(BluetoothDevice device)541 public int getPreferred(BluetoothDevice device) { 542 return -1; 543 } 544 545 @Override setPreferred(BluetoothDevice device, boolean preferred)546 public void setPreferred(BluetoothDevice device, boolean preferred) { 547 } 548 549 @Override isProfileReady()550 public boolean isProfileReady() { 551 return true; 552 } 553 554 @Override convertState(int oppState)555 public int convertState(int oppState) { 556 switch (oppState) { 557 case 0: 558 return SettingsBtStatus.CONNECTION_STATUS_CONNECTED; 559 case 1: 560 return SettingsBtStatus.CONNECTION_STATUS_CONNECTING; 561 case 2: 562 return SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED; 563 default: 564 return SettingsBtStatus.CONNECTION_STATUS_UNKNOWN; 565 } 566 } 567 } 568 } 569