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