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