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