DockService.java revision 6fd5bc935ec3de1d74bd4bbbfc467b8cd4101902
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 mBtManager.saveDockAutoConnectSetting(mDevice.getAddress(), isChecked); 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) { 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 int i = 0; 378 switch (state) { 379 case Intent.EXTRA_DOCK_STATE_CAR: 380 items[i] = service.getString(R.string.bluetooth_dock_settings_headset); 381 mProfiles[i] = Profile.HEADSET; 382 if (firstTime) { 383 mCheckedItems[i] = true; 384 } else { 385 mCheckedItems[i] = LocalBluetoothProfileManager.getProfileManager(mBtManager, 386 Profile.HEADSET).isPreferred(device); 387 } 388 ++i; 389 // fall through 390 case Intent.EXTRA_DOCK_STATE_DESK: 391 items[i] = service.getString(R.string.bluetooth_dock_settings_a2dp); 392 mProfiles[i] = Profile.A2DP; 393 if (firstTime) { 394 mCheckedItems[i] = true; 395 } else { 396 mCheckedItems[i] = LocalBluetoothProfileManager.getProfileManager(mBtManager, 397 Profile.A2DP).isPreferred(device); 398 } 399 break; 400 } 401 return items; 402 } 403 404 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 405 @Override 406 public void onReceive(Context context, Intent intent) { 407 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); 408 if (state == BluetoothAdapter.STATE_ON && mPendingDevice != null) { 409 synchronized (mBtSynchroObject) { 410 if (mPendingDevice.equals(mDevice)) { 411 if(DEBUG) Log.d(TAG, "applying settings"); 412 applyBtSettings(mPendingDevice, mPendingStartId); 413 } else if(DEBUG) { 414 Log.d(TAG, "mPendingDevice (" + mPendingDevice + ") != mDevice (" 415 + mDevice + ")"); 416 } 417 418 mPendingDevice = null; 419 DockEventReceiver.finishStartingService(mContext, mPendingStartId); 420 } 421 } 422 } 423 }; 424 425 private void applyBtSettings(final BluetoothDevice device, int startId) { 426 if (device == null || mProfiles == null || mCheckedItems == null) 427 return; 428 429 // Turn on BT if something is enabled 430 synchronized (mBtSynchroObject) { 431 for (boolean enable : mCheckedItems) { 432 if (enable) { 433 int btState = mBtManager.getBluetoothState(); 434 switch (btState) { 435 case BluetoothAdapter.STATE_OFF: 436 case BluetoothAdapter.STATE_TURNING_OFF: 437 case BluetoothAdapter.STATE_TURNING_ON: 438 if (mPendingDevice != null && mPendingDevice.equals(mDevice)) { 439 return; 440 } 441 if (!mRegistered) { 442 registerReceiver(mReceiver, new IntentFilter( 443 BluetoothAdapter.ACTION_STATE_CHANGED)); 444 } 445 mPendingDevice = device; 446 mRegistered = true; 447 mPendingStartId = startId; 448 if (btState != BluetoothAdapter.STATE_TURNING_ON) { 449 // BT is off. Enable it 450 mBtManager.getBluetoothAdapter().enable(); 451 } 452 return; 453 } 454 } 455 } 456 } 457 458 mPendingDevice = null; 459 460 for (int i = 0; i < mProfiles.length; i++) { 461 LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager 462 .getProfileManager(mBtManager, mProfiles[i]); 463 boolean isConnected = profileManager.isConnected(device); 464 CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice(mContext, mBtManager, 465 device); 466 467 if (DEBUG) Log.d(TAG, mProfiles[i].toString() + " = " + mCheckedItems[i]); 468 469 if (mCheckedItems[i] && !isConnected) { 470 // Checked but not connected 471 if (DEBUG) Log.d(TAG, "applyBtSettings - Connecting"); 472 cachedDevice.connect(mProfiles[i]); 473 } else if (!mCheckedItems[i] && isConnected) { 474 // Unchecked but connected 475 if (DEBUG) Log.d(TAG, "applyBtSettings - Disconnecting"); 476 cachedDevice.disconnect(mProfiles[i]); 477 } 478 profileManager.setPreferred(device, mCheckedItems[i]); 479 if (DEBUG) { 480 if (mCheckedItems[i] != profileManager.isPreferred(device)) { 481 Log.e(TAG, "Can't save prefered value"); 482 } 483 } 484 } 485 } 486 487 void handleUndocked(Context context, LocalBluetoothManager localManager, 488 BluetoothDevice device) { 489 if (mDialog != null) { 490 mDialog.dismiss(); 491 mDialog = null; 492 } 493 mDevice = null; 494 mPendingDevice = null; 495 CachedBluetoothDevice cachedBluetoothDevice = getCachedBluetoothDevice(context, 496 localManager, device); 497 cachedBluetoothDevice.disconnect(); 498 } 499 500 private static CachedBluetoothDevice getCachedBluetoothDevice(Context context, 501 LocalBluetoothManager localManager, BluetoothDevice device) { 502 CachedBluetoothDeviceManager cachedDeviceManager = localManager.getCachedDeviceManager(); 503 CachedBluetoothDevice cachedBluetoothDevice = cachedDeviceManager.findDevice(device); 504 if (cachedBluetoothDevice == null) { 505 cachedBluetoothDevice = new CachedBluetoothDevice(context, device); 506 } 507 return cachedBluetoothDevice; 508 } 509 510 // TODO Delete this method if not needed. 511 private Notification getNotification(Service service) { 512 CharSequence title = service.getString(R.string.dock_settings_title); 513 514 Notification n = new Notification(R.drawable.ic_bt_headphones_a2dp, title, System 515 .currentTimeMillis()); 516 517 CharSequence contentText = service.getString(R.string.dock_settings_summary); 518 Intent notificationIntent = new Intent(service, DockEventReceiver.class); 519 notificationIntent.setAction(DockEventReceiver.ACTION_DOCK_SHOW_UI); 520 PendingIntent pendingIntent = PendingIntent.getActivity(service, 0, notificationIntent, 0); 521 522 n.setLatestEventInfo(service, title, contentText, pendingIntent); 523 return n; 524 } 525} 526