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