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