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