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