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