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
onCreate()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
onDestroy()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
onBind(Intent intent)181     public IBinder onBind(Intent intent) {
182         // not supported
183         return null;
184     }
185 
getPrefs()186     private SharedPreferences getPrefs() {
187         return getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE);
188     }
189 
190     @Override
onStartCommand(Intent intent, int flags, int startId)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 {
ServiceHandler(Looper looper)254         private ServiceHandler(Looper looper) {
255             super(looper);
256         }
257 
258         @Override
handleMessage(Message msg)259         public void handleMessage(Message msg) {
260             processMessage(msg);
261         }
262     }
263 
264     // This method gets messages from both onStartCommand and mServiceHandler/mServiceLooper
processMessage(Message msg)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 
msgTypeDisableBluetooth(int startId)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 
msgTypeUndockedTemporary(BluetoothDevice device, int state, int startId)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 
msgTypeUndockedPermanent(BluetoothDevice device, int startId)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 
msgTypeDocked(BluetoothDevice device, final int state, final int startId)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 
hasOtherConnectedDevices(BluetoothDevice dock)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 
parseIntent(Intent intent)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 
createDialog(BluetoothDevice device, int state, int startId)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 
initBtSettings(BluetoothDevice device, int state, boolean firstTime)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
handleBtStateChange(Intent intent, int startId)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 
handleBluetoothStateOn(int startId)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 
handleUnexpectedDisconnect(BluetoothDevice disconnectedDevice, LocalBluetoothProfile profile, int startId)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 
connectIfEnabled(BluetoothDevice device)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 
applyBtSettings(BluetoothDevice device, int startId)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 
handleDocked(BluetoothDevice device, int state, int startId)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 
handleUndocked(BluetoothDevice device)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 
getCachedBluetoothDevice(BluetoothDevice device)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 
onServiceConnected()918     public synchronized void onServiceConnected() {
919         if (mRunnable != null) {
920             mRunnable.run();
921             mRunnable = null;
922             mProfileManager.removeServiceListener(this);
923         }
924     }
925 
onServiceDisconnected()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 
DockBluetoothCallback(Context context)933         public DockBluetoothCallback(Context context) {
934             mContext = context;
935         }
936 
onBluetoothStateChanged(int bluetoothState)937         public void onBluetoothStateChanged(int bluetoothState) { }
onDeviceAdded(CachedBluetoothDevice cachedDevice)938         public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { }
onDeviceDeleted(CachedBluetoothDevice cachedDevice)939         public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { }
onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state)940         public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { }
941 
942         @Override
onScanningStateChanged(boolean started)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
onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState)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.
getDockedDeviceAddress(Context context)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