DockService.java revision 845e740fc63657438b9085376c8e7d60d8334a72
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.content.Context;
27import android.content.DialogInterface;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.os.Handler;
31import android.os.HandlerThread;
32import android.os.IBinder;
33import android.os.Looper;
34import android.os.Message;
35import android.util.Log;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.WindowManager;
39import android.widget.CheckBox;
40import android.widget.CompoundButton;
41
42import com.android.settings.R;
43import com.android.settings.bluetooth.LocalBluetoothProfileManager.Profile;
44
45import java.util.List;
46
47public class DockService extends Service implements AlertDialog.OnMultiChoiceClickListener,
48        DialogInterface.OnClickListener, DialogInterface.OnDismissListener,
49        CompoundButton.OnCheckedChangeListener {
50
51    private static final String TAG = "DockService";
52
53    static final boolean DEBUG = false;
54
55    // Time allowed for the device to be undocked and redocked without severing
56    // the bluetooth connection
57    private static final long UNDOCKED_GRACE_PERIOD = 1000;
58
59    // Msg for user wanting the UI to setup the dock
60    private static final int MSG_TYPE_SHOW_UI = 111;
61
62    // Msg for device docked event
63    private static final int MSG_TYPE_DOCKED = 222;
64
65    // Msg for device undocked event
66    private static final int MSG_TYPE_UNDOCKED_TEMPORARY = 333;
67
68    // Msg for undocked command to be process after UNDOCKED_GRACE_PERIOD millis
69    // since MSG_TYPE_UNDOCKED_TEMPORARY
70    private static final int MSG_TYPE_UNDOCKED_PERMANENT = 444;
71
72    // Created in OnCreate()
73    private volatile Looper mServiceLooper;
74    private volatile ServiceHandler mServiceHandler;
75    private DockService mContext;
76    private LocalBluetoothManager mBtManager;
77
78    // Normally set after getting a docked event and unset when the connection
79    // is severed. One exception is that mDevice could be null if the service
80    // was started after the docked event.
81    private BluetoothDevice mDevice;
82
83    // Created and used for the duration of the dialog
84    private AlertDialog mDialog;
85    private Profile[] mProfiles;
86    private boolean[] mCheckedItems;
87    private int mStartIdAssociatedWithDialog;
88
89    // Set while BT is being enabled.
90    private BluetoothDevice mPendingDevice;
91    private int mPendingStartId;
92
93    private Object mBtSynchroObject = new Object();
94
95    @Override
96    public void onCreate() {
97        if (DEBUG) Log.d(TAG, "onCreate");
98
99        mBtManager = LocalBluetoothManager.getInstance(this);
100        mContext = this;
101
102        HandlerThread thread = new HandlerThread("DockService");
103        thread.start();
104
105        mServiceLooper = thread.getLooper();
106        mServiceHandler = new ServiceHandler(mServiceLooper);
107    }
108
109    @Override
110    public void onDestroy() {
111        if (DEBUG) Log.d(TAG, "onDestroy");
112        if (mDialog != null) {
113            mDialog.dismiss();
114            mDialog = null;
115        }
116        mServiceLooper.quit();
117    }
118
119    @Override
120    public IBinder onBind(Intent intent) {
121        // not supported
122        return null;
123    }
124
125    @Override
126    public int onStartCommand(Intent intent, int flags, int startId) {
127        if (DEBUG) Log.d(TAG, "onStartCommand startId:" + startId + " flags: " + flags);
128
129        if (intent == null) {
130            // Nothing to process, stop.
131            if (DEBUG) Log.d(TAG, "START_NOT_STICKY - intent is null.");
132
133            // NOTE: We MUST not call stopSelf() directly, since we need to
134            // make sure the wake lock acquired by the Receiver is released.
135            DockEventReceiver.finishStartingService(this, startId);
136            return START_NOT_STICKY;
137        }
138
139        if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) {
140            handleBtStateChange(intent);
141            DockEventReceiver.finishStartingService(this, startId);
142            return START_NOT_STICKY;
143        }
144
145        Message msg = parseIntent(intent);
146        if (msg == null) {
147            // Bad intent
148            if (DEBUG) Log.d(TAG, "START_NOT_STICKY - Bad intent.");
149            DockEventReceiver.finishStartingService(this, startId);
150            return START_NOT_STICKY;
151        }
152
153        msg.arg2 = startId;
154        processMessage(msg);
155
156        return START_NOT_STICKY;
157    }
158
159    private final class ServiceHandler extends Handler {
160        public ServiceHandler(Looper looper) {
161            super(looper);
162        }
163
164        @Override
165        public void handleMessage(Message msg) {
166            processMessage(msg);
167        }
168    }
169
170    // This method gets messages from both onStartCommand and mServiceHandler/mServiceLooper
171    private synchronized void processMessage(Message msg) {
172        int msgType = msg.what;
173        int state = msg.arg1;
174        int startId = msg.arg2;
175        BluetoothDevice device = (BluetoothDevice) msg.obj;
176
177        if(DEBUG) Log.d(TAG, "processMessage: " + msgType + " state: " + state + " device = "
178                + (msg.obj == null ? "null" : device.toString()));
179
180        switch (msgType) {
181            case MSG_TYPE_SHOW_UI:
182                if (mDialog != null) {
183                    // Shouldn't normally happen
184                    mDialog.dismiss();
185                    mDialog = null;
186                }
187                mDevice = device;
188                createDialog(mContext, mDevice, state, startId);
189                break;
190
191            case MSG_TYPE_DOCKED:
192                if (DEBUG) {
193                    // TODO figure out why hasMsg always returns false if device
194                    // is supplied
195                    Log.d(TAG, "1 Has undock perm msg = "
196                            + mServiceHandler.hasMessages(MSG_TYPE_UNDOCKED_PERMANENT, mDevice));
197                    Log.d(TAG, "2 Has undock perm msg = "
198                            + mServiceHandler.hasMessages(MSG_TYPE_UNDOCKED_PERMANENT, device));
199                }
200
201                mServiceHandler.removeMessages(MSG_TYPE_UNDOCKED_PERMANENT);
202
203                if (!device.equals(mDevice)) {
204                    if (mDevice != null) {
205                        // Not expected. Cleanup/undock existing
206                        handleUndocked(mContext, mBtManager, mDevice);
207                    }
208
209                    mDevice = device;
210                    if (mBtManager.getDockAutoConnectSetting(device.getAddress())) {
211                        // Setting == auto connect
212                        initBtSettings(mContext, device, state, false);
213                        applyBtSettings(mDevice, startId);
214                    } else {
215                        createDialog(mContext, mDevice, state, startId);
216                    }
217                }
218                break;
219
220            case MSG_TYPE_UNDOCKED_PERMANENT:
221                // Grace period passed. Disconnect.
222                handleUndocked(mContext, mBtManager, device);
223                break;
224
225            case MSG_TYPE_UNDOCKED_TEMPORARY:
226                // Undocked event received. Queue a delayed msg to sever connection
227                Message newMsg = mServiceHandler.obtainMessage(MSG_TYPE_UNDOCKED_PERMANENT, state,
228                        startId, device);
229                mServiceHandler.sendMessageDelayed(newMsg, UNDOCKED_GRACE_PERIOD);
230                break;
231        }
232
233        if (mDialog == null && mPendingDevice == null && msgType != MSG_TYPE_UNDOCKED_TEMPORARY) {
234            // NOTE: We MUST not call stopSelf() directly, since we need to
235            // make sure the wake lock acquired by the Receiver is released.
236            DockEventReceiver.finishStartingService(DockService.this, startId);
237        }
238    }
239
240    private Message parseIntent(Intent intent) {
241        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
242        int state = intent.getIntExtra(Intent.EXTRA_DOCK_STATE, -1234);
243
244        if (DEBUG) {
245            Log.d(TAG, "Action: " + intent.getAction() + " State:" + state
246                    + " Device: " + (device == null ? "null" : device.getName()));
247        }
248
249        if (device == null) {
250            Log.w(TAG, "device is null");
251            return null;
252        }
253
254        int msgType;
255        switch (state) {
256            case Intent.EXTRA_DOCK_STATE_UNDOCKED:
257                msgType = MSG_TYPE_UNDOCKED_TEMPORARY;
258                break;
259            case Intent.EXTRA_DOCK_STATE_DESK:
260            case Intent.EXTRA_DOCK_STATE_CAR:
261                if (DockEventReceiver.ACTION_DOCK_SHOW_UI.equals(intent.getAction())) {
262                    msgType = MSG_TYPE_SHOW_UI;
263                } else {
264                    msgType = MSG_TYPE_DOCKED;
265                }
266                break;
267            default:
268                return null;
269        }
270
271        return mServiceHandler.obtainMessage(msgType, state, 0, device);
272    }
273
274    private boolean createDialog(DockService service, BluetoothDevice device, int state,
275            int startId) {
276        switch (state) {
277            case Intent.EXTRA_DOCK_STATE_CAR:
278            case Intent.EXTRA_DOCK_STATE_DESK:
279                break;
280            default:
281                return false;
282        }
283
284        startForeground(0, new Notification());
285
286        // Device in a new dock.
287        boolean firstTime = !mBtManager.hasDockAutoConnectSetting(device.getAddress());
288
289        CharSequence[] items = initBtSettings(service, device, state, firstTime);
290
291        final AlertDialog.Builder ab = new AlertDialog.Builder(service);
292        ab.setTitle(service.getString(R.string.bluetooth_dock_settings_title));
293
294        // Profiles
295        ab.setMultiChoiceItems(items, mCheckedItems, service);
296
297        // Remember this settings
298        LayoutInflater inflater = (LayoutInflater) service
299                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
300        float pixelScaleFactor = service.getResources().getDisplayMetrics().density;
301        View view = inflater.inflate(R.layout.remember_dock_setting, null);
302        CheckBox rememberCheckbox = (CheckBox) view.findViewById(R.id.remember);
303
304        // check "Remember setting" by default if no value was saved
305        boolean checked = firstTime || mBtManager.getDockAutoConnectSetting(device.getAddress());
306        rememberCheckbox.setChecked(checked);
307        rememberCheckbox.setOnCheckedChangeListener(this);
308        int viewSpacingLeft = (int) (14 * pixelScaleFactor);
309        int viewSpacingRight = (int) (14 * pixelScaleFactor);
310        ab.setView(view, viewSpacingLeft, 0 /* top */, viewSpacingRight, 0 /* bottom */);
311        if (DEBUG) {
312            Log.d(TAG, "Auto connect = "
313                    + mBtManager.getDockAutoConnectSetting(device.getAddress()));
314        }
315
316        // Ok Button
317        ab.setPositiveButton(service.getString(android.R.string.ok), service);
318
319        mStartIdAssociatedWithDialog = startId;
320        mDialog = ab.create();
321        mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
322        mDialog.setOnDismissListener(service);
323        mDialog.show();
324        return true;
325    }
326
327    // Called when the individual bt profiles are clicked.
328    public void onClick(DialogInterface dialog, int which, boolean isChecked) {
329        if (DEBUG) Log.d(TAG, "Item " + which + " changed to " + isChecked);
330        mCheckedItems[which] = isChecked;
331    }
332
333    // Called when the "Remember" Checkbox is clicked
334    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
335        if (DEBUG) Log.d(TAG, "onCheckedChanged: Remember Settings = " + isChecked);
336        if (mDevice != null) {
337            mBtManager.saveDockAutoConnectSetting(mDevice.getAddress(), isChecked);
338        }
339    }
340
341    // Called when the dialog is dismissed
342    public void onDismiss(DialogInterface dialog) {
343        // NOTE: We MUST not call stopSelf() directly, since we need to
344        // make sure the wake lock acquired by the Receiver is released.
345        if (mPendingDevice == null) {
346            DockEventReceiver.finishStartingService(mContext, mStartIdAssociatedWithDialog);
347        }
348        mContext.stopForeground(true);
349    }
350
351    // Called when clicked on the OK button
352    public void onClick(DialogInterface dialog, int which) {
353        if (which == DialogInterface.BUTTON_POSITIVE && mDevice != null) {
354            if (!mBtManager.hasDockAutoConnectSetting(mDevice.getAddress())) {
355                mBtManager.saveDockAutoConnectSetting(mDevice.getAddress(), true);
356            }
357
358            applyBtSettings(mDevice, mStartIdAssociatedWithDialog);
359        }
360    }
361
362    private CharSequence[] initBtSettings(DockService service, BluetoothDevice device, int state,
363            boolean firstTime) {
364        // TODO Avoid hardcoding dock and profiles. Read from system properties
365        int numOfProfiles = 0;
366        switch (state) {
367            case Intent.EXTRA_DOCK_STATE_DESK:
368                numOfProfiles = 1;
369                break;
370            case Intent.EXTRA_DOCK_STATE_CAR:
371                numOfProfiles = 2;
372                break;
373            default:
374                return null;
375        }
376
377        mProfiles = new Profile[numOfProfiles];
378        mCheckedItems = new boolean[numOfProfiles];
379        CharSequence[] items = new CharSequence[numOfProfiles];
380
381        switch (state) {
382            case Intent.EXTRA_DOCK_STATE_CAR:
383                items[0] = service.getString(R.string.bluetooth_dock_settings_headset);
384                items[1] = service.getString(R.string.bluetooth_dock_settings_a2dp);
385                mProfiles[0] = Profile.HEADSET;
386                mProfiles[1] = Profile.A2DP;
387                if (firstTime) {
388                    // Enable by default for car dock
389                    mCheckedItems[0] = true;
390                    mCheckedItems[1] = true;
391                } else {
392                    mCheckedItems[0] = LocalBluetoothProfileManager.getProfileManager(mBtManager,
393                            Profile.HEADSET).isPreferred(device);
394                    mCheckedItems[1] = LocalBluetoothProfileManager.getProfileManager(mBtManager,
395                            Profile.A2DP).isPreferred(device);
396                }
397                break;
398
399            case Intent.EXTRA_DOCK_STATE_DESK:
400                items[0] = service.getString(R.string.bluetooth_dock_settings_a2dp);
401                mProfiles[0] = Profile.A2DP;
402                if (firstTime) {
403                    // Disable by default for desk dock
404                    mCheckedItems[0] = false;
405                } else {
406                    mCheckedItems[0] = LocalBluetoothProfileManager.getProfileManager(mBtManager,
407                            Profile.A2DP).isPreferred(device);
408                }
409                break;
410        }
411        return items;
412    }
413
414    public void handleBtStateChange(Intent intent) {
415        int btState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
416        if (btState == BluetoothAdapter.STATE_ON) {
417            synchronized (mBtSynchroObject) {
418                if (mPendingDevice != null) {
419                    if (mPendingDevice.equals(mDevice)) {
420                        if(DEBUG) Log.d(TAG, "applying settings");
421                        applyBtSettings(mPendingDevice, mPendingStartId);
422                    } else if(DEBUG) {
423                        Log.d(TAG, "mPendingDevice  (" + mPendingDevice + ") != mDevice ("
424                                + mDevice + ")");
425                    }
426
427                    mPendingDevice = null;
428                    DockEventReceiver.finishStartingService(mContext, mPendingStartId);
429                } else {
430                    // Reconnect if docked and bluetooth was enabled by user.
431                    Intent i = registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT));
432                    if (i != null) {
433                        int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE,
434                                Intent.EXTRA_DOCK_STATE_UNDOCKED);
435                        if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) {
436                            BluetoothDevice device = i
437                                    .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
438                            if (device != null) {
439                                connectIfEnabled(device);
440                            }
441                        }
442                    }
443                }
444            }
445        }
446    }
447
448    private synchronized void connectIfEnabled(BluetoothDevice device) {
449        CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice(mContext, mBtManager, device);
450        List<Profile> profiles = cachedDevice.getConnectableProfiles();
451        for (int i = 0; i < profiles.size(); i++) {
452            LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
453                    .getProfileManager(mBtManager, profiles.get(i));
454            int auto;
455            if (Profile.A2DP == profiles.get(i)) {
456                auto = BluetoothA2dp.PRIORITY_AUTO_CONNECT;
457            } else if (Profile.HEADSET == profiles.get(i)) {
458                auto = BluetoothHeadset.PRIORITY_AUTO_CONNECT;
459            } else {
460                continue;
461            }
462
463            if (profileManager.getPreferred(device) == auto) {
464                cachedDevice.connect();
465                break;
466            }
467        }
468    }
469
470    private synchronized void applyBtSettings(final BluetoothDevice device, int startId) {
471        if (device == null || mProfiles == null || mCheckedItems == null)
472            return;
473
474        // Turn on BT if something is enabled
475        synchronized (mBtSynchroObject) {
476            for (boolean enable : mCheckedItems) {
477                if (enable) {
478                    int btState = mBtManager.getBluetoothState();
479                    switch (btState) {
480                        case BluetoothAdapter.STATE_OFF:
481                        case BluetoothAdapter.STATE_TURNING_OFF:
482                        case BluetoothAdapter.STATE_TURNING_ON:
483                            if (mPendingDevice != null && mPendingDevice.equals(mDevice)) {
484                                return;
485                            }
486
487                            mPendingDevice = device;
488                            mPendingStartId = startId;
489                            if (btState != BluetoothAdapter.STATE_TURNING_ON) {
490                                // BT is off. Enable it
491                                mBtManager.getBluetoothAdapter().enable();
492                            }
493                            return;
494                    }
495                }
496            }
497        }
498
499        mPendingDevice = null;
500
501        boolean callConnect = false;
502        CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice(mContext, mBtManager,
503                device);
504        for (int i = 0; i < mProfiles.length; i++) {
505            LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
506                    .getProfileManager(mBtManager, mProfiles[i]);
507
508            if (DEBUG) Log.d(TAG, mProfiles[i].toString() + " = " + mCheckedItems[i]);
509
510            if (mCheckedItems[i]) {
511                // Checked but not connected
512                callConnect = true;
513            } else if (!mCheckedItems[i]) {
514                // Unchecked but connected
515                if (DEBUG) Log.d(TAG, "applyBtSettings - Disconnecting");
516                cachedDevice.disconnect(mProfiles[i]);
517            }
518            profileManager.setPreferred(device, mCheckedItems[i]);
519            if (DEBUG) {
520                if (mCheckedItems[i] != profileManager.isPreferred(device)) {
521                    Log.e(TAG, "Can't save prefered value");
522                }
523            }
524        }
525
526        if (callConnect) {
527            if (DEBUG) Log.d(TAG, "applyBtSettings - Connecting");
528            cachedDevice.connect();
529        }
530    }
531
532    private synchronized void handleUndocked(Context context, LocalBluetoothManager localManager,
533            BluetoothDevice device) {
534        if (mDialog != null) {
535            mDialog.dismiss();
536            mDialog = null;
537        }
538        mDevice = null;
539        mPendingDevice = null;
540        CachedBluetoothDevice cachedBluetoothDevice = getCachedBluetoothDevice(context,
541                localManager, device);
542        cachedBluetoothDevice.disconnect();
543    }
544
545    private static CachedBluetoothDevice getCachedBluetoothDevice(Context context,
546            LocalBluetoothManager localManager, BluetoothDevice device) {
547        CachedBluetoothDeviceManager cachedDeviceManager = localManager.getCachedDeviceManager();
548        CachedBluetoothDevice cachedBluetoothDevice = cachedDeviceManager.findDevice(device);
549        if (cachedBluetoothDevice == null) {
550            cachedBluetoothDevice = new CachedBluetoothDevice(context, device);
551        }
552        return cachedBluetoothDevice;
553    }
554}
555