1 /* 2 * Copyright (C) 2009 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 import com.android.settings.bluetooth.LocalBluetoothProfileManager.ServiceListener; 21 22 import android.app.AlertDialog; 23 import android.app.Notification; 24 import android.app.Service; 25 import android.bluetooth.BluetoothA2dp; 26 import android.bluetooth.BluetoothAdapter; 27 import android.bluetooth.BluetoothDevice; 28 import android.bluetooth.BluetoothHeadset; 29 import android.bluetooth.BluetoothProfile; 30 import android.content.DialogInterface; 31 import android.content.Intent; 32 import android.content.IntentFilter; 33 import android.content.SharedPreferences; 34 import android.os.Handler; 35 import android.os.HandlerThread; 36 import android.os.IBinder; 37 import android.os.Looper; 38 import android.os.Message; 39 import android.provider.Settings; 40 import android.util.Log; 41 import android.view.LayoutInflater; 42 import android.view.View; 43 import android.view.WindowManager; 44 import android.widget.CheckBox; 45 import android.widget.CompoundButton; 46 47 import java.util.Collection; 48 import java.util.List; 49 import java.util.Set; 50 51 public final class DockService extends Service implements ServiceListener { 52 53 private static final String TAG = "DockService"; 54 55 static final boolean DEBUG = false; 56 57 // Time allowed for the device to be undocked and redocked without severing 58 // the bluetooth connection 59 private static final long UNDOCKED_GRACE_PERIOD = 1000; 60 61 // Time allowed for the device to be undocked and redocked without turning 62 // off Bluetooth 63 private static final long DISABLE_BT_GRACE_PERIOD = 2000; 64 65 // Msg for user wanting the UI to setup the dock 66 private static final int MSG_TYPE_SHOW_UI = 111; 67 68 // Msg for device docked event 69 private static final int MSG_TYPE_DOCKED = 222; 70 71 // Msg for device undocked event 72 private static final int MSG_TYPE_UNDOCKED_TEMPORARY = 333; 73 74 // Msg for undocked command to be process after UNDOCKED_GRACE_PERIOD millis 75 // since MSG_TYPE_UNDOCKED_TEMPORARY 76 private static final int MSG_TYPE_UNDOCKED_PERMANENT = 444; 77 78 // Msg for disabling bt after DISABLE_BT_GRACE_PERIOD millis since 79 // MSG_TYPE_UNDOCKED_PERMANENT 80 private static final int MSG_TYPE_DISABLE_BT = 555; 81 82 private static final String SHARED_PREFERENCES_NAME = "dock_settings"; 83 84 private static final String KEY_DISABLE_BT_WHEN_UNDOCKED = "disable_bt_when_undock"; 85 86 private static final String KEY_DISABLE_BT = "disable_bt"; 87 88 private static final String KEY_CONNECT_RETRY_COUNT = "connect_retry_count"; 89 90 /* 91 * If disconnected unexpectedly, reconnect up to 6 times. Each profile counts 92 * as one time so it's only 3 times for both profiles on the car dock. 93 */ 94 private static final int MAX_CONNECT_RETRY = 6; 95 96 private static final int INVALID_STARTID = -100; 97 98 // Created in OnCreate() 99 private volatile Looper mServiceLooper; 100 private volatile ServiceHandler mServiceHandler; 101 private Runnable mRunnable; 102 private LocalBluetoothAdapter mLocalAdapter; 103 private CachedBluetoothDeviceManager mDeviceManager; 104 private LocalBluetoothProfileManager mProfileManager; 105 106 // Normally set after getting a docked event and unset when the connection 107 // is severed. One exception is that mDevice could be null if the service 108 // was started after the docked event. 109 private BluetoothDevice mDevice; 110 111 // Created and used for the duration of the dialog 112 private AlertDialog mDialog; 113 private LocalBluetoothProfile[] mProfiles; 114 private boolean[] mCheckedItems; 115 private int mStartIdAssociatedWithDialog; 116 117 // Set while BT is being enabled. 118 private BluetoothDevice mPendingDevice; 119 private int mPendingStartId; 120 private int mPendingTurnOnStartId = INVALID_STARTID; 121 private int mPendingTurnOffStartId = INVALID_STARTID; 122 123 private CheckBox mAudioMediaCheckbox; 124 125 @Override 126 public void onCreate() { 127 if (DEBUG) Log.d(TAG, "onCreate"); 128 129 LocalBluetoothManager manager = LocalBluetoothManager.getInstance(this); 130 if (manager == null) { 131 Log.e(TAG, "Can't get LocalBluetoothManager: exiting"); 132 return; 133 } 134 135 mLocalAdapter = manager.getBluetoothAdapter(); 136 mDeviceManager = manager.getCachedDeviceManager(); 137 mProfileManager = manager.getProfileManager(); 138 if (mProfileManager == null) { 139 Log.e(TAG, "Can't get LocalBluetoothProfileManager: exiting"); 140 return; 141 } 142 143 HandlerThread thread = new HandlerThread("DockService"); 144 thread.start(); 145 146 mServiceLooper = thread.getLooper(); 147 mServiceHandler = new ServiceHandler(mServiceLooper); 148 } 149 150 @Override 151 public void onDestroy() { 152 if (DEBUG) Log.d(TAG, "onDestroy"); 153 mRunnable = null; 154 if (mDialog != null) { 155 mDialog.dismiss(); 156 mDialog = null; 157 } 158 if (mProfileManager != null) { 159 mProfileManager.removeServiceListener(this); 160 } 161 if (mServiceLooper != null) { 162 mServiceLooper.quit(); 163 } 164 165 mLocalAdapter = null; 166 mDeviceManager = null; 167 mProfileManager = null; 168 mServiceLooper = null; 169 mServiceHandler = null; 170 } 171 172 @Override 173 public IBinder onBind(Intent intent) { 174 // not supported 175 return null; 176 } 177 178 private SharedPreferences getPrefs() { 179 return getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE); 180 } 181 182 @Override 183 public int onStartCommand(Intent intent, int flags, int startId) { 184 if (DEBUG) Log.d(TAG, "onStartCommand startId: " + startId + " flags: " + flags); 185 186 if (intent == null) { 187 // Nothing to process, stop. 188 if (DEBUG) Log.d(TAG, "START_NOT_STICKY - intent is null."); 189 190 // NOTE: We MUST not call stopSelf() directly, since we need to 191 // make sure the wake lock acquired by the Receiver is released. 192 DockEventReceiver.finishStartingService(this, startId); 193 return START_NOT_STICKY; 194 } 195 196 if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) { 197 handleBtStateChange(intent, startId); 198 return START_NOT_STICKY; 199 } 200 201 /* 202 * This assumes that the intent sender has checked that this is a dock 203 * and that the intent is for a disconnect 204 */ 205 final SharedPreferences prefs = getPrefs(); 206 if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { 207 BluetoothDevice disconnectedDevice = intent 208 .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 209 int retryCount = prefs.getInt(KEY_CONNECT_RETRY_COUNT, 0); 210 if (retryCount < MAX_CONNECT_RETRY) { 211 prefs.edit().putInt(KEY_CONNECT_RETRY_COUNT, retryCount + 1).apply(); 212 handleUnexpectedDisconnect(disconnectedDevice, mProfileManager.getHeadsetProfile(), startId); 213 } 214 return START_NOT_STICKY; 215 } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { 216 BluetoothDevice disconnectedDevice = intent 217 .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 218 219 int retryCount = prefs.getInt(KEY_CONNECT_RETRY_COUNT, 0); 220 if (retryCount < MAX_CONNECT_RETRY) { 221 prefs.edit().putInt(KEY_CONNECT_RETRY_COUNT, retryCount + 1).apply(); 222 handleUnexpectedDisconnect(disconnectedDevice, mProfileManager.getA2dpProfile(), startId); 223 } 224 return START_NOT_STICKY; 225 } 226 227 Message msg = parseIntent(intent); 228 if (msg == null) { 229 // Bad intent 230 if (DEBUG) Log.d(TAG, "START_NOT_STICKY - Bad intent."); 231 DockEventReceiver.finishStartingService(this, startId); 232 return START_NOT_STICKY; 233 } 234 235 if (msg.what == MSG_TYPE_DOCKED) { 236 prefs.edit().remove(KEY_CONNECT_RETRY_COUNT).apply(); 237 } 238 239 msg.arg2 = startId; 240 processMessage(msg); 241 242 return START_NOT_STICKY; 243 } 244 245 private final class ServiceHandler extends Handler { 246 private ServiceHandler(Looper looper) { 247 super(looper); 248 } 249 250 @Override 251 public void handleMessage(Message msg) { 252 processMessage(msg); 253 } 254 } 255 256 // This method gets messages from both onStartCommand and mServiceHandler/mServiceLooper 257 private synchronized void processMessage(Message msg) { 258 int msgType = msg.what; 259 final int state = msg.arg1; 260 final int startId = msg.arg2; 261 BluetoothDevice device = null; 262 if (msg.obj != null) { 263 device = (BluetoothDevice) msg.obj; 264 } 265 266 if(DEBUG) Log.d(TAG, "processMessage: " + msgType + " state: " + state + " device = " 267 + (device == null ? "null" : device.toString())); 268 269 boolean deferFinishCall = false; 270 271 switch (msgType) { 272 case MSG_TYPE_SHOW_UI: 273 if (device != null) { 274 createDialog(device, state, startId); 275 } 276 break; 277 278 case MSG_TYPE_DOCKED: 279 deferFinishCall = msgTypeDocked(device, state, startId); 280 break; 281 282 case MSG_TYPE_UNDOCKED_PERMANENT: 283 deferFinishCall = msgTypeUndockedPermanent(device, startId); 284 break; 285 286 case MSG_TYPE_UNDOCKED_TEMPORARY: 287 msgTypeUndockedTemporary(device, state, startId); 288 break; 289 290 case MSG_TYPE_DISABLE_BT: 291 deferFinishCall = msgTypeDisableBluetooth(startId); 292 break; 293 } 294 295 if (mDialog == null && mPendingDevice == null && msgType != MSG_TYPE_UNDOCKED_TEMPORARY 296 && !deferFinishCall) { 297 // NOTE: We MUST not call stopSelf() directly, since we need to 298 // make sure the wake lock acquired by the Receiver is released. 299 DockEventReceiver.finishStartingService(this, startId); 300 } 301 } 302 303 private boolean msgTypeDisableBluetooth(int startId) { 304 if (DEBUG) { 305 Log.d(TAG, "BT DISABLE"); 306 } 307 final SharedPreferences prefs = getPrefs(); 308 if (mLocalAdapter.disable()) { 309 prefs.edit().remove(KEY_DISABLE_BT_WHEN_UNDOCKED).apply(); 310 return false; 311 } else { 312 // disable() returned an error. Persist a flag to disable BT later 313 prefs.edit().putBoolean(KEY_DISABLE_BT, true).apply(); 314 mPendingTurnOffStartId = startId; 315 if(DEBUG) { 316 Log.d(TAG, "disable failed. try again later " + startId); 317 } 318 return true; 319 } 320 } 321 322 private void msgTypeUndockedTemporary(BluetoothDevice device, int state, 323 int startId) { 324 // Undocked event received. Queue a delayed msg to sever connection 325 Message newMsg = mServiceHandler.obtainMessage(MSG_TYPE_UNDOCKED_PERMANENT, state, 326 startId, device); 327 mServiceHandler.sendMessageDelayed(newMsg, UNDOCKED_GRACE_PERIOD); 328 } 329 330 private boolean msgTypeUndockedPermanent(BluetoothDevice device, int startId) { 331 // Grace period passed. Disconnect. 332 handleUndocked(device); 333 if (device != null) { 334 final SharedPreferences prefs = getPrefs(); 335 336 if (DEBUG) { 337 Log.d(TAG, "DISABLE_BT_WHEN_UNDOCKED = " 338 + prefs.getBoolean(KEY_DISABLE_BT_WHEN_UNDOCKED, false)); 339 } 340 341 if (prefs.getBoolean(KEY_DISABLE_BT_WHEN_UNDOCKED, false)) { 342 if (hasOtherConnectedDevices(device)) { 343 // Don't disable BT if something is connected 344 prefs.edit().remove(KEY_DISABLE_BT_WHEN_UNDOCKED).apply(); 345 } else { 346 // BT was disabled when we first docked 347 if (DEBUG) { 348 Log.d(TAG, "QUEUED BT DISABLE"); 349 } 350 // Queue a delayed msg to disable BT 351 Message newMsg = mServiceHandler.obtainMessage( 352 MSG_TYPE_DISABLE_BT, 0, startId, null); 353 mServiceHandler.sendMessageDelayed(newMsg, 354 DISABLE_BT_GRACE_PERIOD); 355 return true; 356 } 357 } 358 } 359 return false; 360 } 361 362 private boolean msgTypeDocked(BluetoothDevice device, final int state, 363 final int startId) { 364 if (DEBUG) { 365 // TODO figure out why hasMsg always returns false if device 366 // is supplied 367 Log.d(TAG, "1 Has undock perm msg = " 368 + mServiceHandler.hasMessages(MSG_TYPE_UNDOCKED_PERMANENT, mDevice)); 369 Log.d(TAG, "2 Has undock perm msg = " 370 + mServiceHandler.hasMessages(MSG_TYPE_UNDOCKED_PERMANENT, device)); 371 } 372 373 mServiceHandler.removeMessages(MSG_TYPE_UNDOCKED_PERMANENT); 374 mServiceHandler.removeMessages(MSG_TYPE_DISABLE_BT); 375 getPrefs().edit().remove(KEY_DISABLE_BT).apply(); 376 377 if (device != null) { 378 if (!device.equals(mDevice)) { 379 if (mDevice != null) { 380 // Not expected. Cleanup/undock existing 381 handleUndocked(mDevice); 382 } 383 384 mDevice = device; 385 386 // Register first in case LocalBluetoothProfileManager 387 // becomes ready after isManagerReady is called and it 388 // would be too late to register a service listener. 389 mProfileManager.addServiceListener(this); 390 if (mProfileManager.isManagerReady()) { 391 handleDocked(device, state, startId); 392 // Not needed after all 393 mProfileManager.removeServiceListener(this); 394 } else { 395 final BluetoothDevice d = device; 396 mRunnable = new Runnable() { 397 public void run() { 398 handleDocked(d, state, startId); // FIXME: WTF runnable here? 399 } 400 }; 401 return true; 402 } 403 } 404 } else { 405 // display dialog to enable dock for media audio only in the case of low end docks and 406 // if not already selected by user 407 int dockAudioMediaEnabled = Settings.Global.getInt(getContentResolver(), 408 Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, -1); 409 if (dockAudioMediaEnabled == -1 && 410 state == Intent.EXTRA_DOCK_STATE_LE_DESK) { 411 handleDocked(null, state, startId); 412 return true; 413 } 414 } 415 return false; 416 } 417 418 synchronized boolean hasOtherConnectedDevices(BluetoothDevice dock) { 419 Collection<CachedBluetoothDevice> cachedDevices = mDeviceManager.getCachedDevicesCopy(); 420 Set<BluetoothDevice> btDevices = mLocalAdapter.getBondedDevices(); 421 if (btDevices == null || cachedDevices == null || btDevices.isEmpty()) { 422 return false; 423 } 424 if(DEBUG) { 425 Log.d(TAG, "btDevices = " + btDevices.size()); 426 Log.d(TAG, "cachedDeviceUIs = " + cachedDevices.size()); 427 } 428 429 for (CachedBluetoothDevice deviceUI : cachedDevices) { 430 BluetoothDevice btDevice = deviceUI.getDevice(); 431 if (!btDevice.equals(dock) && btDevices.contains(btDevice) && deviceUI 432 .isConnected()) { 433 if(DEBUG) Log.d(TAG, "connected deviceUI = " + deviceUI.getName()); 434 return true; 435 } 436 } 437 return false; 438 } 439 440 private Message parseIntent(Intent intent) { 441 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 442 int state = intent.getIntExtra(Intent.EXTRA_DOCK_STATE, -1234); 443 444 if (DEBUG) { 445 Log.d(TAG, "Action: " + intent.getAction() + " State:" + state 446 + " Device: " + (device == null ? "null" : device.getAliasName())); 447 } 448 449 int msgType; 450 switch (state) { 451 case Intent.EXTRA_DOCK_STATE_UNDOCKED: 452 msgType = MSG_TYPE_UNDOCKED_TEMPORARY; 453 break; 454 case Intent.EXTRA_DOCK_STATE_DESK: 455 case Intent.EXTRA_DOCK_STATE_HE_DESK: 456 case Intent.EXTRA_DOCK_STATE_CAR: 457 if (device == null) { 458 Log.w(TAG, "device is null"); 459 return null; 460 } 461 /// Fall Through /// 462 case Intent.EXTRA_DOCK_STATE_LE_DESK: 463 if (DockEventReceiver.ACTION_DOCK_SHOW_UI.equals(intent.getAction())) { 464 if (device == null) { 465 Log.w(TAG, "device is null"); 466 return null; 467 } 468 msgType = MSG_TYPE_SHOW_UI; 469 } else { 470 msgType = MSG_TYPE_DOCKED; 471 } 472 break; 473 default: 474 return null; 475 } 476 477 return mServiceHandler.obtainMessage(msgType, state, 0, device); 478 } 479 480 private void createDialog(BluetoothDevice device, 481 int state, int startId) { 482 if (mDialog != null) { 483 // Shouldn't normally happen 484 mDialog.dismiss(); 485 mDialog = null; 486 } 487 mDevice = device; 488 switch (state) { 489 case Intent.EXTRA_DOCK_STATE_CAR: 490 case Intent.EXTRA_DOCK_STATE_DESK: 491 case Intent.EXTRA_DOCK_STATE_LE_DESK: 492 case Intent.EXTRA_DOCK_STATE_HE_DESK: 493 break; 494 default: 495 return; 496 } 497 498 startForeground(0, new Notification()); 499 500 final AlertDialog.Builder ab = new AlertDialog.Builder(this); 501 View view; 502 LayoutInflater inflater = (LayoutInflater)getSystemService(LAYOUT_INFLATER_SERVICE); 503 504 mAudioMediaCheckbox = null; 505 506 if (device != null) { 507 // Device in a new dock. 508 boolean firstTime = 509 !LocalBluetoothPreferences.hasDockAutoConnectSetting(this, device.getAddress()); 510 511 CharSequence[] items = initBtSettings(device, state, firstTime); 512 513 ab.setTitle(getString(R.string.bluetooth_dock_settings_title)); 514 515 // Profiles 516 ab.setMultiChoiceItems(items, mCheckedItems, mMultiClickListener); 517 518 // Remember this settings 519 view = inflater.inflate(R.layout.remember_dock_setting, null); 520 CheckBox rememberCheckbox = (CheckBox) view.findViewById(R.id.remember); 521 522 // check "Remember setting" by default if no value was saved 523 boolean checked = firstTime || 524 LocalBluetoothPreferences.getDockAutoConnectSetting(this, device.getAddress()); 525 rememberCheckbox.setChecked(checked); 526 rememberCheckbox.setOnCheckedChangeListener(mCheckedChangeListener); 527 if (DEBUG) { 528 Log.d(TAG, "Auto connect = " 529 + LocalBluetoothPreferences.getDockAutoConnectSetting(this, device.getAddress())); 530 } 531 } else { 532 ab.setTitle(getString(R.string.bluetooth_dock_settings_title)); 533 534 view = inflater.inflate(R.layout.dock_audio_media_enable_dialog, null); 535 mAudioMediaCheckbox = 536 (CheckBox) view.findViewById(R.id.dock_audio_media_enable_cb); 537 538 boolean checked = Settings.Global.getInt(getContentResolver(), 539 Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, 0) == 1; 540 541 mAudioMediaCheckbox.setChecked(checked); 542 mAudioMediaCheckbox.setOnCheckedChangeListener(mCheckedChangeListener); 543 } 544 545 float pixelScaleFactor = getResources().getDisplayMetrics().density; 546 int viewSpacingLeft = (int) (14 * pixelScaleFactor); 547 int viewSpacingRight = (int) (14 * pixelScaleFactor); 548 ab.setView(view, viewSpacingLeft, 0 /* top */, viewSpacingRight, 0 /* bottom */); 549 550 // Ok Button 551 ab.setPositiveButton(getString(android.R.string.ok), mClickListener); 552 553 mStartIdAssociatedWithDialog = startId; 554 mDialog = ab.create(); 555 mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); 556 mDialog.setOnDismissListener(mDismissListener); 557 mDialog.show(); 558 } 559 560 // Called when the individual bt profiles are clicked. 561 private final DialogInterface.OnMultiChoiceClickListener mMultiClickListener = 562 new DialogInterface.OnMultiChoiceClickListener() { 563 public void onClick(DialogInterface dialog, int which, boolean isChecked) { 564 if (DEBUG) { 565 Log.d(TAG, "Item " + which + " changed to " + isChecked); 566 } 567 mCheckedItems[which] = isChecked; 568 } 569 }; 570 571 572 // Called when the "Remember" Checkbox is clicked 573 private final CompoundButton.OnCheckedChangeListener mCheckedChangeListener = 574 new CompoundButton.OnCheckedChangeListener() { 575 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 576 if (DEBUG) { 577 Log.d(TAG, "onCheckedChanged: Remember Settings = " + isChecked); 578 } 579 if (mDevice != null) { 580 LocalBluetoothPreferences.saveDockAutoConnectSetting( 581 DockService.this, mDevice.getAddress(), isChecked); 582 } else { 583 Settings.Global.putInt(getContentResolver(), 584 Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, isChecked ? 1 : 0); 585 } 586 } 587 }; 588 589 590 // Called when the dialog is dismissed 591 private final DialogInterface.OnDismissListener mDismissListener = 592 new DialogInterface.OnDismissListener() { 593 public void onDismiss(DialogInterface dialog) { 594 // NOTE: We MUST not call stopSelf() directly, since we need to 595 // make sure the wake lock acquired by the Receiver is released. 596 if (mPendingDevice == null) { 597 DockEventReceiver.finishStartingService( 598 DockService.this, mStartIdAssociatedWithDialog); 599 } 600 stopForeground(true); 601 } 602 }; 603 604 // Called when clicked on the OK button 605 private final DialogInterface.OnClickListener mClickListener = 606 new DialogInterface.OnClickListener() { 607 public void onClick(DialogInterface dialog, int which) { 608 if (which == DialogInterface.BUTTON_POSITIVE) { 609 if (mDevice != null) { 610 if (!LocalBluetoothPreferences 611 .hasDockAutoConnectSetting( 612 DockService.this, 613 mDevice.getAddress())) { 614 LocalBluetoothPreferences 615 .saveDockAutoConnectSetting( 616 DockService.this, 617 mDevice.getAddress(), true); 618 } 619 620 applyBtSettings(mDevice, mStartIdAssociatedWithDialog); 621 } else if (mAudioMediaCheckbox != null) { 622 Settings.Global.putInt(getContentResolver(), 623 Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, 624 mAudioMediaCheckbox.isChecked() ? 1 : 0); 625 } 626 } 627 } 628 }; 629 630 private CharSequence[] initBtSettings(BluetoothDevice device, 631 int state, boolean firstTime) { 632 // TODO Avoid hardcoding dock and profiles. Read from system properties 633 int numOfProfiles; 634 switch (state) { 635 case Intent.EXTRA_DOCK_STATE_DESK: 636 case Intent.EXTRA_DOCK_STATE_LE_DESK: 637 case Intent.EXTRA_DOCK_STATE_HE_DESK: 638 numOfProfiles = 1; 639 break; 640 case Intent.EXTRA_DOCK_STATE_CAR: 641 numOfProfiles = 2; 642 break; 643 default: 644 return null; 645 } 646 647 mProfiles = new LocalBluetoothProfile[numOfProfiles]; 648 mCheckedItems = new boolean[numOfProfiles]; 649 CharSequence[] items = new CharSequence[numOfProfiles]; 650 651 // FIXME: convert switch to something else 652 switch (state) { 653 case Intent.EXTRA_DOCK_STATE_CAR: 654 items[0] = getString(R.string.bluetooth_dock_settings_headset); 655 items[1] = getString(R.string.bluetooth_dock_settings_a2dp); 656 mProfiles[0] = mProfileManager.getHeadsetProfile(); 657 mProfiles[1] = mProfileManager.getA2dpProfile(); 658 if (firstTime) { 659 // Enable by default for car dock 660 mCheckedItems[0] = true; 661 mCheckedItems[1] = true; 662 } else { 663 mCheckedItems[0] = mProfiles[0].isPreferred(device); 664 mCheckedItems[1] = mProfiles[1].isPreferred(device); 665 } 666 break; 667 668 case Intent.EXTRA_DOCK_STATE_DESK: 669 case Intent.EXTRA_DOCK_STATE_LE_DESK: 670 case Intent.EXTRA_DOCK_STATE_HE_DESK: 671 items[0] = getString(R.string.bluetooth_dock_settings_a2dp); 672 mProfiles[0] = mProfileManager.getA2dpProfile(); 673 if (firstTime) { 674 // Disable by default for desk dock 675 mCheckedItems[0] = false; 676 } else { 677 mCheckedItems[0] = mProfiles[0].isPreferred(device); 678 } 679 break; 680 } 681 return items; 682 } 683 684 // TODO: move to background thread to fix strict mode warnings 685 private void handleBtStateChange(Intent intent, int startId) { 686 int btState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); 687 synchronized (this) { 688 if(DEBUG) Log.d(TAG, "BtState = " + btState + " mPendingDevice = " + mPendingDevice); 689 if (btState == BluetoothAdapter.STATE_ON) { 690 handleBluetoothStateOn(startId); 691 } else if (btState == BluetoothAdapter.STATE_TURNING_OFF) { 692 // Remove the flag to disable BT if someone is turning off bt. 693 // The rational is that: 694 // a) if BT is off at undock time, no work needs to be done 695 // b) if BT is on at undock time, the user wants it on. 696 getPrefs().edit().remove(KEY_DISABLE_BT_WHEN_UNDOCKED).apply(); 697 DockEventReceiver.finishStartingService(this, startId); 698 } else if (btState == BluetoothAdapter.STATE_OFF) { 699 // Bluetooth was turning off as we were trying to turn it on. 700 // Let's try again 701 if(DEBUG) Log.d(TAG, "Bluetooth = OFF mPendingDevice = " + mPendingDevice); 702 703 if (mPendingTurnOffStartId != INVALID_STARTID) { 704 DockEventReceiver.finishStartingService(this, mPendingTurnOffStartId); 705 getPrefs().edit().remove(KEY_DISABLE_BT).apply(); 706 mPendingTurnOffStartId = INVALID_STARTID; 707 } 708 709 if (mPendingDevice != null) { 710 mLocalAdapter.enable(); 711 mPendingTurnOnStartId = startId; 712 } else { 713 DockEventReceiver.finishStartingService(this, startId); 714 } 715 } 716 } 717 } 718 719 private void handleBluetoothStateOn(int startId) { 720 if (mPendingDevice != null) { 721 if (mPendingDevice.equals(mDevice)) { 722 if(DEBUG) { 723 Log.d(TAG, "applying settings"); 724 } 725 applyBtSettings(mPendingDevice, mPendingStartId); 726 } else if(DEBUG) { 727 Log.d(TAG, "mPendingDevice (" + mPendingDevice + ") != mDevice (" 728 + mDevice + ')'); 729 } 730 731 mPendingDevice = null; 732 DockEventReceiver.finishStartingService(this, mPendingStartId); 733 } else { 734 final SharedPreferences prefs = getPrefs(); 735 if (DEBUG) { 736 Log.d(TAG, "A DISABLE_BT_WHEN_UNDOCKED = " 737 + prefs.getBoolean(KEY_DISABLE_BT_WHEN_UNDOCKED, false)); 738 } 739 // Reconnect if docked and bluetooth was enabled by user. 740 Intent i = registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT)); 741 if (i != null) { 742 int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE, 743 Intent.EXTRA_DOCK_STATE_UNDOCKED); 744 if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) { 745 BluetoothDevice device = i 746 .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 747 if (device != null) { 748 connectIfEnabled(device); 749 } 750 } else if (prefs.getBoolean(KEY_DISABLE_BT, false) 751 && mLocalAdapter.disable()) { 752 mPendingTurnOffStartId = startId; 753 prefs.edit().remove(KEY_DISABLE_BT).apply(); 754 return; 755 } 756 } 757 } 758 759 if (mPendingTurnOnStartId != INVALID_STARTID) { 760 DockEventReceiver.finishStartingService(this, mPendingTurnOnStartId); 761 mPendingTurnOnStartId = INVALID_STARTID; 762 } 763 764 DockEventReceiver.finishStartingService(this, startId); 765 } 766 767 private synchronized void handleUnexpectedDisconnect(BluetoothDevice disconnectedDevice, 768 LocalBluetoothProfile profile, int startId) { 769 if (DEBUG) { 770 Log.d(TAG, "handling failed connect for " + disconnectedDevice); 771 } 772 773 // Reconnect if docked. 774 if (disconnectedDevice != null) { 775 // registerReceiver can't be called from a BroadcastReceiver 776 Intent intent = registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT)); 777 if (intent != null) { 778 int state = intent.getIntExtra(Intent.EXTRA_DOCK_STATE, 779 Intent.EXTRA_DOCK_STATE_UNDOCKED); 780 if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) { 781 BluetoothDevice dockedDevice = intent 782 .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 783 if (dockedDevice != null && dockedDevice.equals(disconnectedDevice)) { 784 CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice( 785 dockedDevice); 786 cachedDevice.connectProfile(profile); 787 } 788 } 789 } 790 } 791 792 DockEventReceiver.finishStartingService(this, startId); 793 } 794 795 private synchronized void connectIfEnabled(BluetoothDevice device) { 796 CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice( 797 device); 798 List<LocalBluetoothProfile> profiles = cachedDevice.getConnectableProfiles(); 799 for (LocalBluetoothProfile profile : profiles) { 800 if (profile.getPreferred(device) == BluetoothProfile.PRIORITY_AUTO_CONNECT) { 801 cachedDevice.connect(false); 802 return; 803 } 804 } 805 } 806 807 private synchronized void applyBtSettings(BluetoothDevice device, int startId) { 808 if (device == null || mProfiles == null || mCheckedItems == null 809 || mLocalAdapter == null) { 810 return; 811 } 812 813 // Turn on BT if something is enabled 814 for (boolean enable : mCheckedItems) { 815 if (enable) { 816 int btState = mLocalAdapter.getBluetoothState(); 817 if (DEBUG) { 818 Log.d(TAG, "BtState = " + btState); 819 } 820 // May have race condition as the phone comes in and out and in the dock. 821 // Always turn on BT 822 mLocalAdapter.enable(); 823 824 // if adapter was previously OFF, TURNING_OFF, or TURNING_ON 825 if (btState != BluetoothAdapter.STATE_ON) { 826 if (mPendingDevice != null && mPendingDevice.equals(mDevice)) { 827 return; 828 } 829 830 mPendingDevice = device; 831 mPendingStartId = startId; 832 if (btState != BluetoothAdapter.STATE_TURNING_ON) { 833 getPrefs().edit().putBoolean( 834 KEY_DISABLE_BT_WHEN_UNDOCKED, true).apply(); 835 } 836 return; 837 } 838 } 839 } 840 841 mPendingDevice = null; 842 843 boolean callConnect = false; 844 CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice( 845 device); 846 for (int i = 0; i < mProfiles.length; i++) { 847 LocalBluetoothProfile profile = mProfiles[i]; 848 if (DEBUG) Log.d(TAG, profile.toString() + " = " + mCheckedItems[i]); 849 850 if (mCheckedItems[i]) { 851 // Checked but not connected 852 callConnect = true; 853 } else if (!mCheckedItems[i]) { 854 // Unchecked, may or may not be connected. 855 int status = profile.getConnectionStatus(cachedDevice.getDevice()); 856 if (status == BluetoothProfile.STATE_CONNECTED) { 857 if (DEBUG) Log.d(TAG, "applyBtSettings - Disconnecting"); 858 cachedDevice.disconnect(mProfiles[i]); 859 } 860 } 861 profile.setPreferred(device, mCheckedItems[i]); 862 if (DEBUG) { 863 if (mCheckedItems[i] != profile.isPreferred(device)) { 864 Log.e(TAG, "Can't save preferred value"); 865 } 866 } 867 } 868 869 if (callConnect) { 870 if (DEBUG) Log.d(TAG, "applyBtSettings - Connecting"); 871 cachedDevice.connect(false); 872 } 873 } 874 875 private synchronized void handleDocked(BluetoothDevice device, int state, 876 int startId) { 877 if (device != null && 878 LocalBluetoothPreferences.getDockAutoConnectSetting(this, device.getAddress())) { 879 // Setting == auto connect 880 initBtSettings(device, state, false); 881 applyBtSettings(mDevice, startId); 882 } else { 883 createDialog(device, state, startId); 884 } 885 } 886 887 private synchronized void handleUndocked(BluetoothDevice device) { 888 mRunnable = null; 889 mProfileManager.removeServiceListener(this); 890 if (mDialog != null) { 891 mDialog.dismiss(); 892 mDialog = null; 893 } 894 mDevice = null; 895 mPendingDevice = null; 896 if (device != null) { 897 CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice(device); 898 cachedDevice.disconnect(); 899 } 900 } 901 902 private CachedBluetoothDevice getCachedBluetoothDevice(BluetoothDevice device) { 903 CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device); 904 if (cachedDevice == null) { 905 cachedDevice = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, device); 906 } 907 return cachedDevice; 908 } 909 910 public synchronized void onServiceConnected() { 911 if (mRunnable != null) { 912 mRunnable.run(); 913 mRunnable = null; 914 mProfileManager.removeServiceListener(this); 915 } 916 } 917 918 public void onServiceDisconnected() { 919 // FIXME: shouldn't I do something on service disconnected too? 920 } 921 } 922