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