DockService.java revision 845e740fc63657438b9085376c8e7d60d8334a72
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 17package com.android.settings.bluetooth; 18 19import android.app.AlertDialog; 20import android.app.Notification; 21import android.app.Service; 22import android.bluetooth.BluetoothA2dp; 23import android.bluetooth.BluetoothAdapter; 24import android.bluetooth.BluetoothDevice; 25import android.bluetooth.BluetoothHeadset; 26import android.content.Context; 27import android.content.DialogInterface; 28import android.content.Intent; 29import android.content.IntentFilter; 30import android.os.Handler; 31import android.os.HandlerThread; 32import android.os.IBinder; 33import android.os.Looper; 34import android.os.Message; 35import android.util.Log; 36import android.view.LayoutInflater; 37import android.view.View; 38import android.view.WindowManager; 39import android.widget.CheckBox; 40import android.widget.CompoundButton; 41 42import com.android.settings.R; 43import com.android.settings.bluetooth.LocalBluetoothProfileManager.Profile; 44 45import java.util.List; 46 47public class DockService extends Service implements AlertDialog.OnMultiChoiceClickListener, 48 DialogInterface.OnClickListener, DialogInterface.OnDismissListener, 49 CompoundButton.OnCheckedChangeListener { 50 51 private static final String TAG = "DockService"; 52 53 static final boolean DEBUG = false; 54 55 // Time allowed for the device to be undocked and redocked without severing 56 // the bluetooth connection 57 private static final long UNDOCKED_GRACE_PERIOD = 1000; 58 59 // Msg for user wanting the UI to setup the dock 60 private static final int MSG_TYPE_SHOW_UI = 111; 61 62 // Msg for device docked event 63 private static final int MSG_TYPE_DOCKED = 222; 64 65 // Msg for device undocked event 66 private static final int MSG_TYPE_UNDOCKED_TEMPORARY = 333; 67 68 // Msg for undocked command to be process after UNDOCKED_GRACE_PERIOD millis 69 // since MSG_TYPE_UNDOCKED_TEMPORARY 70 private static final int MSG_TYPE_UNDOCKED_PERMANENT = 444; 71 72 // Created in OnCreate() 73 private volatile Looper mServiceLooper; 74 private volatile ServiceHandler mServiceHandler; 75 private DockService mContext; 76 private LocalBluetoothManager mBtManager; 77 78 // Normally set after getting a docked event and unset when the connection 79 // is severed. One exception is that mDevice could be null if the service 80 // was started after the docked event. 81 private BluetoothDevice mDevice; 82 83 // Created and used for the duration of the dialog 84 private AlertDialog mDialog; 85 private Profile[] mProfiles; 86 private boolean[] mCheckedItems; 87 private int mStartIdAssociatedWithDialog; 88 89 // Set while BT is being enabled. 90 private BluetoothDevice mPendingDevice; 91 private int mPendingStartId; 92 93 private Object mBtSynchroObject = new Object(); 94 95 @Override 96 public void onCreate() { 97 if (DEBUG) Log.d(TAG, "onCreate"); 98 99 mBtManager = LocalBluetoothManager.getInstance(this); 100 mContext = this; 101 102 HandlerThread thread = new HandlerThread("DockService"); 103 thread.start(); 104 105 mServiceLooper = thread.getLooper(); 106 mServiceHandler = new ServiceHandler(mServiceLooper); 107 } 108 109 @Override 110 public void onDestroy() { 111 if (DEBUG) Log.d(TAG, "onDestroy"); 112 if (mDialog != null) { 113 mDialog.dismiss(); 114 mDialog = null; 115 } 116 mServiceLooper.quit(); 117 } 118 119 @Override 120 public IBinder onBind(Intent intent) { 121 // not supported 122 return null; 123 } 124 125 @Override 126 public int onStartCommand(Intent intent, int flags, int startId) { 127 if (DEBUG) Log.d(TAG, "onStartCommand startId:" + startId + " flags: " + flags); 128 129 if (intent == null) { 130 // Nothing to process, stop. 131 if (DEBUG) Log.d(TAG, "START_NOT_STICKY - intent is null."); 132 133 // NOTE: We MUST not call stopSelf() directly, since we need to 134 // make sure the wake lock acquired by the Receiver is released. 135 DockEventReceiver.finishStartingService(this, startId); 136 return START_NOT_STICKY; 137 } 138 139 if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) { 140 handleBtStateChange(intent); 141 DockEventReceiver.finishStartingService(this, startId); 142 return START_NOT_STICKY; 143 } 144 145 Message msg = parseIntent(intent); 146 if (msg == null) { 147 // Bad intent 148 if (DEBUG) Log.d(TAG, "START_NOT_STICKY - Bad intent."); 149 DockEventReceiver.finishStartingService(this, startId); 150 return START_NOT_STICKY; 151 } 152 153 msg.arg2 = startId; 154 processMessage(msg); 155 156 return START_NOT_STICKY; 157 } 158 159 private final class ServiceHandler extends Handler { 160 public ServiceHandler(Looper looper) { 161 super(looper); 162 } 163 164 @Override 165 public void handleMessage(Message msg) { 166 processMessage(msg); 167 } 168 } 169 170 // This method gets messages from both onStartCommand and mServiceHandler/mServiceLooper 171 private synchronized void processMessage(Message msg) { 172 int msgType = msg.what; 173 int state = msg.arg1; 174 int startId = msg.arg2; 175 BluetoothDevice device = (BluetoothDevice) msg.obj; 176 177 if(DEBUG) Log.d(TAG, "processMessage: " + msgType + " state: " + state + " device = " 178 + (msg.obj == null ? "null" : device.toString())); 179 180 switch (msgType) { 181 case MSG_TYPE_SHOW_UI: 182 if (mDialog != null) { 183 // Shouldn't normally happen 184 mDialog.dismiss(); 185 mDialog = null; 186 } 187 mDevice = device; 188 createDialog(mContext, mDevice, state, startId); 189 break; 190 191 case MSG_TYPE_DOCKED: 192 if (DEBUG) { 193 // TODO figure out why hasMsg always returns false if device 194 // is supplied 195 Log.d(TAG, "1 Has undock perm msg = " 196 + mServiceHandler.hasMessages(MSG_TYPE_UNDOCKED_PERMANENT, mDevice)); 197 Log.d(TAG, "2 Has undock perm msg = " 198 + mServiceHandler.hasMessages(MSG_TYPE_UNDOCKED_PERMANENT, device)); 199 } 200 201 mServiceHandler.removeMessages(MSG_TYPE_UNDOCKED_PERMANENT); 202 203 if (!device.equals(mDevice)) { 204 if (mDevice != null) { 205 // Not expected. Cleanup/undock existing 206 handleUndocked(mContext, mBtManager, mDevice); 207 } 208 209 mDevice = device; 210 if (mBtManager.getDockAutoConnectSetting(device.getAddress())) { 211 // Setting == auto connect 212 initBtSettings(mContext, device, state, false); 213 applyBtSettings(mDevice, startId); 214 } else { 215 createDialog(mContext, mDevice, state, startId); 216 } 217 } 218 break; 219 220 case MSG_TYPE_UNDOCKED_PERMANENT: 221 // Grace period passed. Disconnect. 222 handleUndocked(mContext, mBtManager, device); 223 break; 224 225 case MSG_TYPE_UNDOCKED_TEMPORARY: 226 // Undocked event received. Queue a delayed msg to sever connection 227 Message newMsg = mServiceHandler.obtainMessage(MSG_TYPE_UNDOCKED_PERMANENT, state, 228 startId, device); 229 mServiceHandler.sendMessageDelayed(newMsg, UNDOCKED_GRACE_PERIOD); 230 break; 231 } 232 233 if (mDialog == null && mPendingDevice == null && msgType != MSG_TYPE_UNDOCKED_TEMPORARY) { 234 // NOTE: We MUST not call stopSelf() directly, since we need to 235 // make sure the wake lock acquired by the Receiver is released. 236 DockEventReceiver.finishStartingService(DockService.this, startId); 237 } 238 } 239 240 private Message parseIntent(Intent intent) { 241 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 242 int state = intent.getIntExtra(Intent.EXTRA_DOCK_STATE, -1234); 243 244 if (DEBUG) { 245 Log.d(TAG, "Action: " + intent.getAction() + " State:" + state 246 + " Device: " + (device == null ? "null" : device.getName())); 247 } 248 249 if (device == null) { 250 Log.w(TAG, "device is null"); 251 return null; 252 } 253 254 int msgType; 255 switch (state) { 256 case Intent.EXTRA_DOCK_STATE_UNDOCKED: 257 msgType = MSG_TYPE_UNDOCKED_TEMPORARY; 258 break; 259 case Intent.EXTRA_DOCK_STATE_DESK: 260 case Intent.EXTRA_DOCK_STATE_CAR: 261 if (DockEventReceiver.ACTION_DOCK_SHOW_UI.equals(intent.getAction())) { 262 msgType = MSG_TYPE_SHOW_UI; 263 } else { 264 msgType = MSG_TYPE_DOCKED; 265 } 266 break; 267 default: 268 return null; 269 } 270 271 return mServiceHandler.obtainMessage(msgType, state, 0, device); 272 } 273 274 private boolean createDialog(DockService service, BluetoothDevice device, int state, 275 int startId) { 276 switch (state) { 277 case Intent.EXTRA_DOCK_STATE_CAR: 278 case Intent.EXTRA_DOCK_STATE_DESK: 279 break; 280 default: 281 return false; 282 } 283 284 startForeground(0, new Notification()); 285 286 // Device in a new dock. 287 boolean firstTime = !mBtManager.hasDockAutoConnectSetting(device.getAddress()); 288 289 CharSequence[] items = initBtSettings(service, device, state, firstTime); 290 291 final AlertDialog.Builder ab = new AlertDialog.Builder(service); 292 ab.setTitle(service.getString(R.string.bluetooth_dock_settings_title)); 293 294 // Profiles 295 ab.setMultiChoiceItems(items, mCheckedItems, service); 296 297 // Remember this settings 298 LayoutInflater inflater = (LayoutInflater) service 299 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 300 float pixelScaleFactor = service.getResources().getDisplayMetrics().density; 301 View view = inflater.inflate(R.layout.remember_dock_setting, null); 302 CheckBox rememberCheckbox = (CheckBox) view.findViewById(R.id.remember); 303 304 // check "Remember setting" by default if no value was saved 305 boolean checked = firstTime || mBtManager.getDockAutoConnectSetting(device.getAddress()); 306 rememberCheckbox.setChecked(checked); 307 rememberCheckbox.setOnCheckedChangeListener(this); 308 int viewSpacingLeft = (int) (14 * pixelScaleFactor); 309 int viewSpacingRight = (int) (14 * pixelScaleFactor); 310 ab.setView(view, viewSpacingLeft, 0 /* top */, viewSpacingRight, 0 /* bottom */); 311 if (DEBUG) { 312 Log.d(TAG, "Auto connect = " 313 + mBtManager.getDockAutoConnectSetting(device.getAddress())); 314 } 315 316 // Ok Button 317 ab.setPositiveButton(service.getString(android.R.string.ok), service); 318 319 mStartIdAssociatedWithDialog = startId; 320 mDialog = ab.create(); 321 mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG); 322 mDialog.setOnDismissListener(service); 323 mDialog.show(); 324 return true; 325 } 326 327 // Called when the individual bt profiles are clicked. 328 public void onClick(DialogInterface dialog, int which, boolean isChecked) { 329 if (DEBUG) Log.d(TAG, "Item " + which + " changed to " + isChecked); 330 mCheckedItems[which] = isChecked; 331 } 332 333 // Called when the "Remember" Checkbox is clicked 334 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 335 if (DEBUG) Log.d(TAG, "onCheckedChanged: Remember Settings = " + isChecked); 336 if (mDevice != null) { 337 mBtManager.saveDockAutoConnectSetting(mDevice.getAddress(), isChecked); 338 } 339 } 340 341 // Called when the dialog is dismissed 342 public void onDismiss(DialogInterface dialog) { 343 // NOTE: We MUST not call stopSelf() directly, since we need to 344 // make sure the wake lock acquired by the Receiver is released. 345 if (mPendingDevice == null) { 346 DockEventReceiver.finishStartingService(mContext, mStartIdAssociatedWithDialog); 347 } 348 mContext.stopForeground(true); 349 } 350 351 // Called when clicked on the OK button 352 public void onClick(DialogInterface dialog, int which) { 353 if (which == DialogInterface.BUTTON_POSITIVE && mDevice != null) { 354 if (!mBtManager.hasDockAutoConnectSetting(mDevice.getAddress())) { 355 mBtManager.saveDockAutoConnectSetting(mDevice.getAddress(), true); 356 } 357 358 applyBtSettings(mDevice, mStartIdAssociatedWithDialog); 359 } 360 } 361 362 private CharSequence[] initBtSettings(DockService service, BluetoothDevice device, int state, 363 boolean firstTime) { 364 // TODO Avoid hardcoding dock and profiles. Read from system properties 365 int numOfProfiles = 0; 366 switch (state) { 367 case Intent.EXTRA_DOCK_STATE_DESK: 368 numOfProfiles = 1; 369 break; 370 case Intent.EXTRA_DOCK_STATE_CAR: 371 numOfProfiles = 2; 372 break; 373 default: 374 return null; 375 } 376 377 mProfiles = new Profile[numOfProfiles]; 378 mCheckedItems = new boolean[numOfProfiles]; 379 CharSequence[] items = new CharSequence[numOfProfiles]; 380 381 switch (state) { 382 case Intent.EXTRA_DOCK_STATE_CAR: 383 items[0] = service.getString(R.string.bluetooth_dock_settings_headset); 384 items[1] = service.getString(R.string.bluetooth_dock_settings_a2dp); 385 mProfiles[0] = Profile.HEADSET; 386 mProfiles[1] = Profile.A2DP; 387 if (firstTime) { 388 // Enable by default for car dock 389 mCheckedItems[0] = true; 390 mCheckedItems[1] = true; 391 } else { 392 mCheckedItems[0] = LocalBluetoothProfileManager.getProfileManager(mBtManager, 393 Profile.HEADSET).isPreferred(device); 394 mCheckedItems[1] = LocalBluetoothProfileManager.getProfileManager(mBtManager, 395 Profile.A2DP).isPreferred(device); 396 } 397 break; 398 399 case Intent.EXTRA_DOCK_STATE_DESK: 400 items[0] = service.getString(R.string.bluetooth_dock_settings_a2dp); 401 mProfiles[0] = Profile.A2DP; 402 if (firstTime) { 403 // Disable by default for desk dock 404 mCheckedItems[0] = false; 405 } else { 406 mCheckedItems[0] = LocalBluetoothProfileManager.getProfileManager(mBtManager, 407 Profile.A2DP).isPreferred(device); 408 } 409 break; 410 } 411 return items; 412 } 413 414 public void handleBtStateChange(Intent intent) { 415 int btState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); 416 if (btState == BluetoothAdapter.STATE_ON) { 417 synchronized (mBtSynchroObject) { 418 if (mPendingDevice != null) { 419 if (mPendingDevice.equals(mDevice)) { 420 if(DEBUG) Log.d(TAG, "applying settings"); 421 applyBtSettings(mPendingDevice, mPendingStartId); 422 } else if(DEBUG) { 423 Log.d(TAG, "mPendingDevice (" + mPendingDevice + ") != mDevice (" 424 + mDevice + ")"); 425 } 426 427 mPendingDevice = null; 428 DockEventReceiver.finishStartingService(mContext, mPendingStartId); 429 } else { 430 // Reconnect if docked and bluetooth was enabled by user. 431 Intent i = registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT)); 432 if (i != null) { 433 int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE, 434 Intent.EXTRA_DOCK_STATE_UNDOCKED); 435 if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) { 436 BluetoothDevice device = i 437 .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 438 if (device != null) { 439 connectIfEnabled(device); 440 } 441 } 442 } 443 } 444 } 445 } 446 } 447 448 private synchronized void connectIfEnabled(BluetoothDevice device) { 449 CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice(mContext, mBtManager, device); 450 List<Profile> profiles = cachedDevice.getConnectableProfiles(); 451 for (int i = 0; i < profiles.size(); i++) { 452 LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager 453 .getProfileManager(mBtManager, profiles.get(i)); 454 int auto; 455 if (Profile.A2DP == profiles.get(i)) { 456 auto = BluetoothA2dp.PRIORITY_AUTO_CONNECT; 457 } else if (Profile.HEADSET == profiles.get(i)) { 458 auto = BluetoothHeadset.PRIORITY_AUTO_CONNECT; 459 } else { 460 continue; 461 } 462 463 if (profileManager.getPreferred(device) == auto) { 464 cachedDevice.connect(); 465 break; 466 } 467 } 468 } 469 470 private synchronized void applyBtSettings(final BluetoothDevice device, int startId) { 471 if (device == null || mProfiles == null || mCheckedItems == null) 472 return; 473 474 // Turn on BT if something is enabled 475 synchronized (mBtSynchroObject) { 476 for (boolean enable : mCheckedItems) { 477 if (enable) { 478 int btState = mBtManager.getBluetoothState(); 479 switch (btState) { 480 case BluetoothAdapter.STATE_OFF: 481 case BluetoothAdapter.STATE_TURNING_OFF: 482 case BluetoothAdapter.STATE_TURNING_ON: 483 if (mPendingDevice != null && mPendingDevice.equals(mDevice)) { 484 return; 485 } 486 487 mPendingDevice = device; 488 mPendingStartId = startId; 489 if (btState != BluetoothAdapter.STATE_TURNING_ON) { 490 // BT is off. Enable it 491 mBtManager.getBluetoothAdapter().enable(); 492 } 493 return; 494 } 495 } 496 } 497 } 498 499 mPendingDevice = null; 500 501 boolean callConnect = false; 502 CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice(mContext, mBtManager, 503 device); 504 for (int i = 0; i < mProfiles.length; i++) { 505 LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager 506 .getProfileManager(mBtManager, mProfiles[i]); 507 508 if (DEBUG) Log.d(TAG, mProfiles[i].toString() + " = " + mCheckedItems[i]); 509 510 if (mCheckedItems[i]) { 511 // Checked but not connected 512 callConnect = true; 513 } else if (!mCheckedItems[i]) { 514 // Unchecked but connected 515 if (DEBUG) Log.d(TAG, "applyBtSettings - Disconnecting"); 516 cachedDevice.disconnect(mProfiles[i]); 517 } 518 profileManager.setPreferred(device, mCheckedItems[i]); 519 if (DEBUG) { 520 if (mCheckedItems[i] != profileManager.isPreferred(device)) { 521 Log.e(TAG, "Can't save prefered value"); 522 } 523 } 524 } 525 526 if (callConnect) { 527 if (DEBUG) Log.d(TAG, "applyBtSettings - Connecting"); 528 cachedDevice.connect(); 529 } 530 } 531 532 private synchronized void handleUndocked(Context context, LocalBluetoothManager localManager, 533 BluetoothDevice device) { 534 if (mDialog != null) { 535 mDialog.dismiss(); 536 mDialog = null; 537 } 538 mDevice = null; 539 mPendingDevice = null; 540 CachedBluetoothDevice cachedBluetoothDevice = getCachedBluetoothDevice(context, 541 localManager, device); 542 cachedBluetoothDevice.disconnect(); 543 } 544 545 private static CachedBluetoothDevice getCachedBluetoothDevice(Context context, 546 LocalBluetoothManager localManager, BluetoothDevice device) { 547 CachedBluetoothDeviceManager cachedDeviceManager = localManager.getCachedDeviceManager(); 548 CachedBluetoothDevice cachedBluetoothDevice = cachedDeviceManager.findDevice(device); 549 if (cachedBluetoothDevice == null) { 550 cachedBluetoothDevice = new CachedBluetoothDevice(context, device); 551 } 552 return cachedBluetoothDevice; 553 } 554} 555