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