Home | History | Annotate | Download | only in bluetooth
      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 
     17 package com.android.settings.bluetooth;
     18 
     19 import com.android.settings.R;
     20 import com.android.settings.bluetooth.LocalBluetoothProfileManager.ServiceListener;
     21 
     22 import android.app.AlertDialog;
     23 import android.app.Notification;
     24 import android.app.Service;
     25 import android.bluetooth.BluetoothA2dp;
     26 import android.bluetooth.BluetoothAdapter;
     27 import android.bluetooth.BluetoothDevice;
     28 import android.bluetooth.BluetoothHeadset;
     29 import android.bluetooth.BluetoothProfile;
     30 import android.content.DialogInterface;
     31 import android.content.Intent;
     32 import android.content.IntentFilter;
     33 import android.content.SharedPreferences;
     34 import android.os.Handler;
     35 import android.os.HandlerThread;
     36 import android.os.IBinder;
     37 import android.os.Looper;
     38 import android.os.Message;
     39 import android.util.Log;
     40 import android.view.LayoutInflater;
     41 import android.view.View;
     42 import android.view.WindowManager;
     43 import android.widget.CheckBox;
     44 import android.widget.CompoundButton;
     45 
     46 import java.util.Collection;
     47 import java.util.List;
     48 import java.util.Set;
     49 
     50 public final class DockService extends Service implements ServiceListener {
     51 
     52     private static final String TAG = "DockService";
     53 
     54     static final boolean DEBUG = false;
     55 
     56     // Time allowed for the device to be undocked and redocked without severing
     57     // the bluetooth connection
     58     private static final long UNDOCKED_GRACE_PERIOD = 1000;
     59 
     60     // Time allowed for the device to be undocked and redocked without turning
     61     // off Bluetooth
     62     private static final long DISABLE_BT_GRACE_PERIOD = 2000;
     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     // Msg for disabling bt after DISABLE_BT_GRACE_PERIOD millis since
     78     // MSG_TYPE_UNDOCKED_PERMANENT
     79     private static final int MSG_TYPE_DISABLE_BT = 555;
     80 
     81     private static final String SHARED_PREFERENCES_NAME = "dock_settings";
     82 
     83     private static final String KEY_DISABLE_BT_WHEN_UNDOCKED = "disable_bt_when_undock";
     84 
     85     private static final String KEY_DISABLE_BT = "disable_bt";
     86 
     87     private static final String KEY_CONNECT_RETRY_COUNT = "connect_retry_count";
     88 
     89     /*
     90      * If disconnected unexpectedly, reconnect up to 6 times. Each profile counts
     91      * as one time so it's only 3 times for both profiles on the car dock.
     92      */
     93     private static final int MAX_CONNECT_RETRY = 6;
     94 
     95     private static final int INVALID_STARTID = -100;
     96 
     97     // Created in OnCreate()
     98     private volatile Looper mServiceLooper;
     99     private volatile ServiceHandler mServiceHandler;
    100     private Runnable mRunnable;
    101     private LocalBluetoothAdapter mLocalAdapter;
    102     private CachedBluetoothDeviceManager mDeviceManager;
    103     private LocalBluetoothProfileManager mProfileManager;
    104 
    105     // Normally set after getting a docked event and unset when the connection
    106     // is severed. One exception is that mDevice could be null if the service
    107     // was started after the docked event.
    108     private BluetoothDevice mDevice;
    109 
    110     // Created and used for the duration of the dialog
    111     private AlertDialog mDialog;
    112     private LocalBluetoothProfile[] mProfiles;
    113     private boolean[] mCheckedItems;
    114     private int mStartIdAssociatedWithDialog;
    115 
    116     // Set while BT is being enabled.
    117     private BluetoothDevice mPendingDevice;
    118     private int mPendingStartId;
    119     private int mPendingTurnOnStartId = INVALID_STARTID;
    120     private int mPendingTurnOffStartId = INVALID_STARTID;
    121 
    122     @Override
    123     public void onCreate() {
    124         if (DEBUG) Log.d(TAG, "onCreate");
    125 
    126         LocalBluetoothManager manager = LocalBluetoothManager.getInstance(this);
    127         if (manager == null) {
    128             Log.e(TAG, "Can't get LocalBluetoothManager: exiting");
    129             return;
    130         }
    131 
    132         mLocalAdapter = manager.getBluetoothAdapter();
    133         mDeviceManager = manager.getCachedDeviceManager();
    134         mProfileManager = manager.getProfileManager();
    135         if (mProfileManager == null) {
    136             Log.e(TAG, "Can't get LocalBluetoothProfileManager: exiting");
    137             return;
    138         }
    139 
    140         HandlerThread thread = new HandlerThread("DockService");
    141         thread.start();
    142 
    143         mServiceLooper = thread.getLooper();
    144         mServiceHandler = new ServiceHandler(mServiceLooper);
    145     }
    146 
    147     @Override
    148     public void onDestroy() {
    149         if (DEBUG) Log.d(TAG, "onDestroy");
    150         mRunnable = null;
    151         if (mDialog != null) {
    152             mDialog.dismiss();
    153             mDialog = null;
    154         }
    155         if (mProfileManager != null) {
    156             mProfileManager.removeServiceListener(this);
    157         }
    158         if (mServiceLooper != null) {
    159             mServiceLooper.quit();
    160         }
    161 
    162         mLocalAdapter = null;
    163         mDeviceManager = null;
    164         mProfileManager = null;
    165         mServiceLooper = null;
    166         mServiceHandler = null;
    167     }
    168 
    169     @Override
    170     public IBinder onBind(Intent intent) {
    171         // not supported
    172         return null;
    173     }
    174 
    175     private SharedPreferences getPrefs() {
    176         return getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE);
    177     }
    178 
    179     @Override
    180     public int onStartCommand(Intent intent, int flags, int startId) {
    181         if (DEBUG) Log.d(TAG, "onStartCommand startId: " + startId + " flags: " + flags);
    182 
    183         if (intent == null) {
    184             // Nothing to process, stop.
    185             if (DEBUG) Log.d(TAG, "START_NOT_STICKY - intent is null.");
    186 
    187             // NOTE: We MUST not call stopSelf() directly, since we need to
    188             // make sure the wake lock acquired by the Receiver is released.
    189             DockEventReceiver.finishStartingService(this, startId);
    190             return START_NOT_STICKY;
    191         }
    192 
    193         if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) {
    194             handleBtStateChange(intent, startId);
    195             return START_NOT_STICKY;
    196         }
    197 
    198         /*
    199          * This assumes that the intent sender has checked that this is a dock
    200          * and that the intent is for a disconnect
    201          */
    202         final SharedPreferences prefs = getPrefs();
    203         if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
    204             BluetoothDevice disconnectedDevice = intent
    205                     .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    206             int retryCount = prefs.getInt(KEY_CONNECT_RETRY_COUNT, 0);
    207             if (retryCount < MAX_CONNECT_RETRY) {
    208                 prefs.edit().putInt(KEY_CONNECT_RETRY_COUNT, retryCount + 1).apply();
    209                 handleUnexpectedDisconnect(disconnectedDevice, mProfileManager.getHeadsetProfile(), startId);
    210             }
    211             return START_NOT_STICKY;
    212         } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
    213             BluetoothDevice disconnectedDevice = intent
    214                     .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    215 
    216             int retryCount = prefs.getInt(KEY_CONNECT_RETRY_COUNT, 0);
    217             if (retryCount < MAX_CONNECT_RETRY) {
    218                 prefs.edit().putInt(KEY_CONNECT_RETRY_COUNT, retryCount + 1).apply();
    219                 handleUnexpectedDisconnect(disconnectedDevice, mProfileManager.getA2dpProfile(), startId);
    220             }
    221             return START_NOT_STICKY;
    222         }
    223 
    224         Message msg = parseIntent(intent);
    225         if (msg == null) {
    226             // Bad intent
    227             if (DEBUG) Log.d(TAG, "START_NOT_STICKY - Bad intent.");
    228             DockEventReceiver.finishStartingService(this, startId);
    229             return START_NOT_STICKY;
    230         }
    231 
    232         if (msg.what == MSG_TYPE_DOCKED) {
    233             prefs.edit().remove(KEY_CONNECT_RETRY_COUNT).apply();
    234         }
    235 
    236         msg.arg2 = startId;
    237         processMessage(msg);
    238 
    239         return START_NOT_STICKY;
    240     }
    241 
    242     private final class ServiceHandler extends Handler {
    243         private ServiceHandler(Looper looper) {
    244             super(looper);
    245         }
    246 
    247         @Override
    248         public void handleMessage(Message msg) {
    249             processMessage(msg);
    250         }
    251     }
    252 
    253     // This method gets messages from both onStartCommand and mServiceHandler/mServiceLooper
    254     private synchronized void processMessage(Message msg) {
    255         int msgType = msg.what;
    256         final int state = msg.arg1;
    257         final int startId = msg.arg2;
    258         BluetoothDevice device = null;
    259         if (msg.obj != null) {
    260             device = (BluetoothDevice) msg.obj;
    261         }
    262 
    263         if(DEBUG) Log.d(TAG, "processMessage: " + msgType + " state: " + state + " device = "
    264                 + (device == null ? "null" : device.toString()));
    265 
    266         boolean deferFinishCall = false;
    267 
    268         switch (msgType) {
    269             case MSG_TYPE_SHOW_UI:
    270                 createDialog(device, state, startId);
    271                 break;
    272 
    273             case MSG_TYPE_DOCKED:
    274                 deferFinishCall = msgTypeDocked(device, state, startId);
    275                 break;
    276 
    277             case MSG_TYPE_UNDOCKED_PERMANENT:
    278                 deferFinishCall = msgTypeUndockedPermanent(device, startId);
    279                 break;
    280 
    281             case MSG_TYPE_UNDOCKED_TEMPORARY:
    282                 msgTypeUndockedTemporary(device, state, startId);
    283                 break;
    284 
    285             case MSG_TYPE_DISABLE_BT:
    286                 deferFinishCall = msgTypeDisableBluetooth(startId);
    287                 break;
    288         }
    289 
    290         if (mDialog == null && mPendingDevice == null && msgType != MSG_TYPE_UNDOCKED_TEMPORARY
    291                 && !deferFinishCall) {
    292             // NOTE: We MUST not call stopSelf() directly, since we need to
    293             // make sure the wake lock acquired by the Receiver is released.
    294             DockEventReceiver.finishStartingService(this, startId);
    295         }
    296     }
    297 
    298     private boolean msgTypeDisableBluetooth(int startId) {
    299         if (DEBUG) {
    300             Log.d(TAG, "BT DISABLE");
    301         }
    302         final SharedPreferences prefs = getPrefs();
    303         if (mLocalAdapter.disable()) {
    304             prefs.edit().remove(KEY_DISABLE_BT_WHEN_UNDOCKED).apply();
    305             return false;
    306         } else {
    307             // disable() returned an error. Persist a flag to disable BT later
    308             prefs.edit().putBoolean(KEY_DISABLE_BT, true).apply();
    309             mPendingTurnOffStartId = startId;
    310             if(DEBUG) {
    311                 Log.d(TAG, "disable failed. try again later " + startId);
    312             }
    313             return true;
    314         }
    315     }
    316 
    317     private void msgTypeUndockedTemporary(BluetoothDevice device, int state,
    318             int startId) {
    319         // Undocked event received. Queue a delayed msg to sever connection
    320         Message newMsg = mServiceHandler.obtainMessage(MSG_TYPE_UNDOCKED_PERMANENT, state,
    321                 startId, device);
    322         mServiceHandler.sendMessageDelayed(newMsg, UNDOCKED_GRACE_PERIOD);
    323     }
    324 
    325     private boolean msgTypeUndockedPermanent(BluetoothDevice device, int startId) {
    326         // Grace period passed. Disconnect.
    327         handleUndocked(device);
    328         final SharedPreferences prefs = getPrefs();
    329 
    330         if (DEBUG) {
    331             Log.d(TAG, "DISABLE_BT_WHEN_UNDOCKED = "
    332                     + prefs.getBoolean(KEY_DISABLE_BT_WHEN_UNDOCKED, false));
    333         }
    334 
    335         if (prefs.getBoolean(KEY_DISABLE_BT_WHEN_UNDOCKED, false)) {
    336             if (hasOtherConnectedDevices(device)) {
    337                 // Don't disable BT if something is connected
    338                 prefs.edit().remove(KEY_DISABLE_BT_WHEN_UNDOCKED).apply();
    339             } else {
    340                 // BT was disabled when we first docked
    341                 if (DEBUG) {
    342                     Log.d(TAG, "QUEUED BT DISABLE");
    343                 }
    344                 // Queue a delayed msg to disable BT
    345                 Message newMsg = mServiceHandler.obtainMessage(
    346                         MSG_TYPE_DISABLE_BT, 0, startId, null);
    347                 mServiceHandler.sendMessageDelayed(newMsg,
    348                         DISABLE_BT_GRACE_PERIOD);
    349                 return true;
    350             }
    351         }
    352         return false;
    353     }
    354 
    355     private boolean msgTypeDocked(BluetoothDevice device, final int state,
    356             final int startId) {
    357         if (DEBUG) {
    358             // TODO figure out why hasMsg always returns false if device
    359             // is supplied
    360             Log.d(TAG, "1 Has undock perm msg = "
    361                     + mServiceHandler.hasMessages(MSG_TYPE_UNDOCKED_PERMANENT, mDevice));
    362             Log.d(TAG, "2 Has undock perm msg = "
    363                     + mServiceHandler.hasMessages(MSG_TYPE_UNDOCKED_PERMANENT, device));
    364         }
    365 
    366         mServiceHandler.removeMessages(MSG_TYPE_UNDOCKED_PERMANENT);
    367         mServiceHandler.removeMessages(MSG_TYPE_DISABLE_BT);
    368         getPrefs().edit().remove(KEY_DISABLE_BT).apply();
    369 
    370         if (device != null && !device.equals(mDevice)) {
    371             if (mDevice != null) {
    372                 // Not expected. Cleanup/undock existing
    373                 handleUndocked(mDevice);
    374             }
    375 
    376             mDevice = device;
    377 
    378             // Register first in case LocalBluetoothProfileManager
    379             // becomes ready after isManagerReady is called and it
    380             // would be too late to register a service listener.
    381             mProfileManager.addServiceListener(this);
    382             if (mProfileManager.isManagerReady()) {
    383                 handleDocked(device, state, startId);
    384                 // Not needed after all
    385                 mProfileManager.removeServiceListener(this);
    386             } else {
    387                 final BluetoothDevice d = device;
    388                 mRunnable = new Runnable() {
    389                     public void run() {
    390                         handleDocked(d, state, startId);  // FIXME: WTF runnable here?
    391                     }
    392                 };
    393                 return true;
    394             }
    395         }
    396         return false;
    397     }
    398 
    399     synchronized boolean hasOtherConnectedDevices(BluetoothDevice dock) {
    400         Collection<CachedBluetoothDevice> cachedDevices = mDeviceManager.getCachedDevicesCopy();
    401         Set<BluetoothDevice> btDevices = mLocalAdapter.getBondedDevices();
    402         if (btDevices == null || cachedDevices == null || btDevices.isEmpty()) {
    403             return false;
    404         }
    405         if(DEBUG) {
    406             Log.d(TAG, "btDevices = " + btDevices.size());
    407             Log.d(TAG, "cachedDeviceUIs = " + cachedDevices.size());
    408         }
    409 
    410         for (CachedBluetoothDevice deviceUI : cachedDevices) {
    411             BluetoothDevice btDevice = deviceUI.getDevice();
    412             if (!btDevice.equals(dock) && btDevices.contains(btDevice) && deviceUI
    413                     .isConnected()) {
    414                 if(DEBUG) Log.d(TAG, "connected deviceUI = " + deviceUI.getName());
    415                 return true;
    416             }
    417         }
    418         return false;
    419     }
    420 
    421     private Message parseIntent(Intent intent) {
    422         BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    423         int state = intent.getIntExtra(Intent.EXTRA_DOCK_STATE, -1234);
    424 
    425         if (DEBUG) {
    426             Log.d(TAG, "Action: " + intent.getAction() + " State:" + state
    427                     + " Device: " + (device == null ? "null" : device.getAliasName()));
    428         }
    429 
    430         if (device == null) {
    431             Log.w(TAG, "device is null");
    432             return null;
    433         }
    434 
    435         int msgType;
    436         switch (state) {
    437             case Intent.EXTRA_DOCK_STATE_UNDOCKED:
    438                 msgType = MSG_TYPE_UNDOCKED_TEMPORARY;
    439                 break;
    440             case Intent.EXTRA_DOCK_STATE_DESK:
    441             case Intent.EXTRA_DOCK_STATE_LE_DESK:
    442             case Intent.EXTRA_DOCK_STATE_HE_DESK:
    443             case Intent.EXTRA_DOCK_STATE_CAR:
    444                 if (DockEventReceiver.ACTION_DOCK_SHOW_UI.equals(intent.getAction())) {
    445                     msgType = MSG_TYPE_SHOW_UI;
    446                 } else {
    447                     msgType = MSG_TYPE_DOCKED;
    448                 }
    449                 break;
    450             default:
    451                 return null;
    452         }
    453 
    454         return mServiceHandler.obtainMessage(msgType, state, 0, device);
    455     }
    456 
    457     private void createDialog(BluetoothDevice device,
    458             int state, int startId) {
    459         if (mDialog != null) {
    460             // Shouldn't normally happen
    461             mDialog.dismiss();
    462             mDialog = null;
    463         }
    464         mDevice = device;
    465         switch (state) {
    466             case Intent.EXTRA_DOCK_STATE_CAR:
    467             case Intent.EXTRA_DOCK_STATE_DESK:
    468             case Intent.EXTRA_DOCK_STATE_LE_DESK:
    469             case Intent.EXTRA_DOCK_STATE_HE_DESK:
    470                 break;
    471             default:
    472                 return;
    473         }
    474 
    475         startForeground(0, new Notification());
    476 
    477         // Device in a new dock.
    478         boolean firstTime = !LocalBluetoothPreferences.hasDockAutoConnectSetting(this, device.getAddress());
    479 
    480         CharSequence[] items = initBtSettings(device, state, firstTime);
    481 
    482         final AlertDialog.Builder ab = new AlertDialog.Builder(this);
    483         ab.setTitle(getString(R.string.bluetooth_dock_settings_title));
    484 
    485         // Profiles
    486         ab.setMultiChoiceItems(items, mCheckedItems, mMultiClickListener);
    487 
    488         // Remember this settings
    489         LayoutInflater inflater = (LayoutInflater)
    490                 getSystemService(LAYOUT_INFLATER_SERVICE);
    491         float pixelScaleFactor = getResources().getDisplayMetrics().density;
    492         View view = inflater.inflate(R.layout.remember_dock_setting, null);
    493         CheckBox rememberCheckbox = (CheckBox) view.findViewById(R.id.remember);
    494 
    495         // check "Remember setting" by default if no value was saved
    496         boolean checked = firstTime || LocalBluetoothPreferences.getDockAutoConnectSetting(this, device.getAddress());
    497         rememberCheckbox.setChecked(checked);
    498         rememberCheckbox.setOnCheckedChangeListener(mCheckedChangeListener);
    499         int viewSpacingLeft = (int) (14 * pixelScaleFactor);
    500         int viewSpacingRight = (int) (14 * pixelScaleFactor);
    501         ab.setView(view, viewSpacingLeft, 0 /* top */, viewSpacingRight, 0 /* bottom */);
    502         if (DEBUG) {
    503             Log.d(TAG, "Auto connect = "
    504                     + LocalBluetoothPreferences.getDockAutoConnectSetting(this, device.getAddress()));
    505         }
    506 
    507         // Ok Button
    508         ab.setPositiveButton(getString(android.R.string.ok), mClickListener);
    509 
    510         mStartIdAssociatedWithDialog = startId;
    511         mDialog = ab.create();
    512         mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
    513         mDialog.setOnDismissListener(mDismissListener);
    514         mDialog.show();
    515     }
    516 
    517     // Called when the individual bt profiles are clicked.
    518     private final DialogInterface.OnMultiChoiceClickListener mMultiClickListener =
    519             new DialogInterface.OnMultiChoiceClickListener() {
    520                 public void onClick(DialogInterface dialog, int which, boolean isChecked) {
    521                     if (DEBUG) {
    522                         Log.d(TAG, "Item " + which + " changed to " + isChecked);
    523                     }
    524                     mCheckedItems[which] = isChecked;
    525                 }
    526             };
    527 
    528 
    529     // Called when the "Remember" Checkbox is clicked
    530     private final CompoundButton.OnCheckedChangeListener mCheckedChangeListener =
    531             new CompoundButton.OnCheckedChangeListener() {
    532                 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    533                     if (DEBUG) {
    534                         Log.d(TAG, "onCheckedChanged: Remember Settings = " + isChecked);
    535                     }
    536                     if (mDevice != null) {
    537                         LocalBluetoothPreferences.saveDockAutoConnectSetting(
    538                                 DockService.this, mDevice.getAddress(), isChecked);
    539                     }
    540                 }
    541             };
    542 
    543 
    544     // Called when the dialog is dismissed
    545     private final DialogInterface.OnDismissListener mDismissListener =
    546             new DialogInterface.OnDismissListener() {
    547                 public void onDismiss(DialogInterface dialog) {
    548                     // NOTE: We MUST not call stopSelf() directly, since we need to
    549                     // make sure the wake lock acquired by the Receiver is released.
    550                     if (mPendingDevice == null) {
    551                         DockEventReceiver.finishStartingService(
    552                                 DockService.this, mStartIdAssociatedWithDialog);
    553                     }
    554                     stopForeground(true);
    555                 }
    556             };
    557 
    558     // Called when clicked on the OK button
    559     private final DialogInterface.OnClickListener mClickListener =
    560             new DialogInterface.OnClickListener() {
    561                 public void onClick(DialogInterface dialog, int which) {
    562                     if (which == DialogInterface.BUTTON_POSITIVE
    563                             && mDevice != null) {
    564                         if (!LocalBluetoothPreferences
    565                                 .hasDockAutoConnectSetting(
    566                                         DockService.this,
    567                                         mDevice.getAddress())) {
    568                             LocalBluetoothPreferences
    569                                     .saveDockAutoConnectSetting(
    570                                             DockService.this,
    571                                             mDevice.getAddress(), true);
    572                         }
    573 
    574                         applyBtSettings(mDevice, mStartIdAssociatedWithDialog);
    575                     }
    576                 }
    577             };
    578 
    579     private CharSequence[] initBtSettings(BluetoothDevice device,
    580             int state, boolean firstTime) {
    581         // TODO Avoid hardcoding dock and profiles. Read from system properties
    582         int numOfProfiles;
    583         switch (state) {
    584             case Intent.EXTRA_DOCK_STATE_DESK:
    585             case Intent.EXTRA_DOCK_STATE_LE_DESK:
    586             case Intent.EXTRA_DOCK_STATE_HE_DESK:
    587                 numOfProfiles = 1;
    588                 break;
    589             case Intent.EXTRA_DOCK_STATE_CAR:
    590                 numOfProfiles = 2;
    591                 break;
    592             default:
    593                 return null;
    594         }
    595 
    596         mProfiles = new LocalBluetoothProfile[numOfProfiles];
    597         mCheckedItems = new boolean[numOfProfiles];
    598         CharSequence[] items = new CharSequence[numOfProfiles];
    599 
    600         // FIXME: convert switch to something else
    601         switch (state) {
    602             case Intent.EXTRA_DOCK_STATE_CAR:
    603                 items[0] = getString(R.string.bluetooth_dock_settings_headset);
    604                 items[1] = getString(R.string.bluetooth_dock_settings_a2dp);
    605                 mProfiles[0] = mProfileManager.getHeadsetProfile();
    606                 mProfiles[1] = mProfileManager.getA2dpProfile();
    607                 if (firstTime) {
    608                     // Enable by default for car dock
    609                     mCheckedItems[0] = true;
    610                     mCheckedItems[1] = true;
    611                 } else {
    612                     mCheckedItems[0] = mProfiles[0].isPreferred(device);
    613                     mCheckedItems[1] = mProfiles[1].isPreferred(device);
    614                 }
    615                 break;
    616 
    617             case Intent.EXTRA_DOCK_STATE_DESK:
    618             case Intent.EXTRA_DOCK_STATE_LE_DESK:
    619             case Intent.EXTRA_DOCK_STATE_HE_DESK:
    620                 items[0] = getString(R.string.bluetooth_dock_settings_a2dp);
    621                 mProfiles[0] = mProfileManager.getA2dpProfile();
    622                 if (firstTime) {
    623                     // Disable by default for desk dock
    624                     mCheckedItems[0] = false;
    625                 } else {
    626                     mCheckedItems[0] = mProfiles[0].isPreferred(device);
    627                 }
    628                 break;
    629         }
    630         return items;
    631     }
    632 
    633     // TODO: move to background thread to fix strict mode warnings
    634     private void handleBtStateChange(Intent intent, int startId) {
    635         int btState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
    636         synchronized (this) {
    637             if(DEBUG) Log.d(TAG, "BtState = " + btState + " mPendingDevice = " + mPendingDevice);
    638             if (btState == BluetoothAdapter.STATE_ON) {
    639                 handleBluetoothStateOn(startId);
    640             } else if (btState == BluetoothAdapter.STATE_TURNING_OFF) {
    641                 // Remove the flag to disable BT if someone is turning off bt.
    642                 // The rational is that:
    643                 // a) if BT is off at undock time, no work needs to be done
    644                 // b) if BT is on at undock time, the user wants it on.
    645                 getPrefs().edit().remove(KEY_DISABLE_BT_WHEN_UNDOCKED).apply();
    646                 DockEventReceiver.finishStartingService(this, startId);
    647             } else if (btState == BluetoothAdapter.STATE_OFF) {
    648                 // Bluetooth was turning off as we were trying to turn it on.
    649                 // Let's try again
    650                 if(DEBUG) Log.d(TAG, "Bluetooth = OFF mPendingDevice = " + mPendingDevice);
    651 
    652                 if (mPendingTurnOffStartId != INVALID_STARTID) {
    653                     DockEventReceiver.finishStartingService(this, mPendingTurnOffStartId);
    654                     getPrefs().edit().remove(KEY_DISABLE_BT).apply();
    655                     mPendingTurnOffStartId = INVALID_STARTID;
    656                 }
    657 
    658                 if (mPendingDevice != null) {
    659                     mLocalAdapter.enable();
    660                     mPendingTurnOnStartId = startId;
    661                 } else {
    662                     DockEventReceiver.finishStartingService(this, startId);
    663                 }
    664             }
    665         }
    666     }
    667 
    668     private void handleBluetoothStateOn(int startId) {
    669         if (mPendingDevice != null) {
    670             if (mPendingDevice.equals(mDevice)) {
    671                 if(DEBUG) {
    672                     Log.d(TAG, "applying settings");
    673                 }
    674                 applyBtSettings(mPendingDevice, mPendingStartId);
    675             } else if(DEBUG) {
    676                 Log.d(TAG, "mPendingDevice  (" + mPendingDevice + ") != mDevice ("
    677                         + mDevice + ')');
    678             }
    679 
    680             mPendingDevice = null;
    681             DockEventReceiver.finishStartingService(this, mPendingStartId);
    682         } else {
    683             final SharedPreferences prefs = getPrefs();
    684             if (DEBUG) {
    685                 Log.d(TAG, "A DISABLE_BT_WHEN_UNDOCKED = "
    686                         + prefs.getBoolean(KEY_DISABLE_BT_WHEN_UNDOCKED, false));
    687             }
    688             // Reconnect if docked and bluetooth was enabled by user.
    689             Intent i = registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT));
    690             if (i != null) {
    691                 int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE,
    692                         Intent.EXTRA_DOCK_STATE_UNDOCKED);
    693                 if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) {
    694                     BluetoothDevice device = i
    695                             .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    696                     if (device != null) {
    697                         connectIfEnabled(device);
    698                     }
    699                 } else if (prefs.getBoolean(KEY_DISABLE_BT, false)
    700                         && mLocalAdapter.disable()) {
    701                     mPendingTurnOffStartId = startId;
    702                     prefs.edit().remove(KEY_DISABLE_BT).apply();
    703                     return;
    704                 }
    705             }
    706         }
    707 
    708         if (mPendingTurnOnStartId != INVALID_STARTID) {
    709             DockEventReceiver.finishStartingService(this, mPendingTurnOnStartId);
    710             mPendingTurnOnStartId = INVALID_STARTID;
    711         }
    712 
    713         DockEventReceiver.finishStartingService(this, startId);
    714     }
    715 
    716     private synchronized void handleUnexpectedDisconnect(BluetoothDevice disconnectedDevice,
    717             LocalBluetoothProfile profile, int startId) {
    718         if (DEBUG) {
    719             Log.d(TAG, "handling failed connect for " + disconnectedDevice);
    720         }
    721 
    722             // Reconnect if docked.
    723             if (disconnectedDevice != null) {
    724                 // registerReceiver can't be called from a BroadcastReceiver
    725                 Intent intent = registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT));
    726                 if (intent != null) {
    727                     int state = intent.getIntExtra(Intent.EXTRA_DOCK_STATE,
    728                             Intent.EXTRA_DOCK_STATE_UNDOCKED);
    729                     if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) {
    730                         BluetoothDevice dockedDevice = intent
    731                                 .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    732                         if (dockedDevice != null && dockedDevice.equals(disconnectedDevice)) {
    733                             CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice(
    734                                     dockedDevice);
    735                             cachedDevice.connectProfile(profile);
    736                         }
    737                     }
    738                 }
    739             }
    740 
    741             DockEventReceiver.finishStartingService(this, startId);
    742     }
    743 
    744     private synchronized void connectIfEnabled(BluetoothDevice device) {
    745         CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice(
    746                 device);
    747         List<LocalBluetoothProfile> profiles = cachedDevice.getConnectableProfiles();
    748         for (LocalBluetoothProfile profile : profiles) {
    749             if (profile.getPreferred(device) == BluetoothProfile.PRIORITY_AUTO_CONNECT) {
    750                 cachedDevice.connect(false);
    751                 return;
    752             }
    753         }
    754     }
    755 
    756     private synchronized void applyBtSettings(BluetoothDevice device, int startId) {
    757         if (device == null || mProfiles == null || mCheckedItems == null
    758                 || mLocalAdapter == null) {
    759             return;
    760         }
    761 
    762         // Turn on BT if something is enabled
    763         for (boolean enable : mCheckedItems) {
    764             if (enable) {
    765                 int btState = mLocalAdapter.getBluetoothState();
    766                 if (DEBUG) {
    767                     Log.d(TAG, "BtState = " + btState);
    768                 }
    769                 // May have race condition as the phone comes in and out and in the dock.
    770                 // Always turn on BT
    771                 mLocalAdapter.enable();
    772 
    773                 // if adapter was previously OFF, TURNING_OFF, or TURNING_ON
    774                 if (btState != BluetoothAdapter.STATE_ON) {
    775                     if (mPendingDevice != null && mPendingDevice.equals(mDevice)) {
    776                         return;
    777                     }
    778 
    779                     mPendingDevice = device;
    780                     mPendingStartId = startId;
    781                     if (btState != BluetoothAdapter.STATE_TURNING_ON) {
    782                         getPrefs().edit().putBoolean(
    783                                 KEY_DISABLE_BT_WHEN_UNDOCKED, true).apply();
    784                     }
    785                     return;
    786                 }
    787             }
    788         }
    789 
    790         mPendingDevice = null;
    791 
    792         boolean callConnect = false;
    793         CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice(
    794                 device);
    795         for (int i = 0; i < mProfiles.length; i++) {
    796             LocalBluetoothProfile profile = mProfiles[i];
    797             if (DEBUG) Log.d(TAG, profile.toString() + " = " + mCheckedItems[i]);
    798 
    799             if (mCheckedItems[i]) {
    800                 // Checked but not connected
    801                 callConnect = true;
    802             } else if (!mCheckedItems[i]) {
    803                 // Unchecked, may or may not be connected.
    804                 int status = profile.getConnectionStatus(cachedDevice.getDevice());
    805                 if (status == BluetoothProfile.STATE_CONNECTED) {
    806                     if (DEBUG) Log.d(TAG, "applyBtSettings - Disconnecting");
    807                     cachedDevice.disconnect(mProfiles[i]);
    808                 }
    809             }
    810             profile.setPreferred(device, mCheckedItems[i]);
    811             if (DEBUG) {
    812                 if (mCheckedItems[i] != profile.isPreferred(device)) {
    813                     Log.e(TAG, "Can't save preferred value");
    814                 }
    815             }
    816         }
    817 
    818         if (callConnect) {
    819             if (DEBUG) Log.d(TAG, "applyBtSettings - Connecting");
    820             cachedDevice.connect(false);
    821         }
    822     }
    823 
    824     private synchronized void handleDocked(BluetoothDevice device, int state,
    825             int startId) {
    826         if (LocalBluetoothPreferences.getDockAutoConnectSetting(this, device.getAddress())) {
    827             // Setting == auto connect
    828             initBtSettings(device, state, false);
    829             applyBtSettings(mDevice, startId);
    830         } else {
    831             createDialog(device, state, startId);
    832         }
    833     }
    834 
    835     private synchronized void handleUndocked(BluetoothDevice device) {
    836         mRunnable = null;
    837         mProfileManager.removeServiceListener(this);
    838         if (mDialog != null) {
    839             mDialog.dismiss();
    840             mDialog = null;
    841         }
    842         mDevice = null;
    843         mPendingDevice = null;
    844         CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice(device);
    845         cachedDevice.disconnect();
    846     }
    847 
    848     private CachedBluetoothDevice getCachedBluetoothDevice(BluetoothDevice device) {
    849         CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
    850         if (cachedDevice == null) {
    851             cachedDevice = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, device);
    852         }
    853         return cachedDevice;
    854     }
    855 
    856     public synchronized void onServiceConnected() {
    857         if (mRunnable != null) {
    858             mRunnable.run();
    859             mRunnable = null;
    860             mProfileManager.removeServiceListener(this);
    861         }
    862     }
    863 
    864     public void onServiceDisconnected() {
    865         // FIXME: shouldn't I do something on service disconnected too?
    866     }
    867 }
    868