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