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