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