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