CachedBluetoothDevice.java revision 436b29e68e6608bed9e8e7d54385b8f62d89208e
1/*
2 * Copyright (C) 2008 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.bluetooth.BluetoothClass;
20import android.bluetooth.BluetoothDevice;
21import android.bluetooth.BluetoothProfile;
22import android.os.ParcelUuid;
23import android.os.SystemClock;
24import android.text.TextUtils;
25import android.util.Log;
26
27import java.util.ArrayList;
28import java.util.Collection;
29import java.util.Collections;
30import java.util.List;
31
32/**
33 * CachedBluetoothDevice represents a remote Bluetooth device. It contains
34 * attributes of the device (such as the address, name, RSSI, etc.) and
35 * functionality that can be performed on the device (connect, pair, disconnect,
36 * etc.).
37 */
38final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
39    private static final String TAG = "CachedBluetoothDevice";
40    private static final boolean DEBUG = Utils.V;
41
42    private final LocalBluetoothAdapter mLocalAdapter;
43    private final LocalBluetoothProfileManager mProfileManager;
44    private final BluetoothDevice mDevice;
45    private String mName;
46    private short mRssi;
47    private BluetoothClass mBtClass;
48
49    private final List<LocalBluetoothProfile> mProfiles =
50            new ArrayList<LocalBluetoothProfile>();
51
52    private boolean mVisible;
53
54    private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
55
56    /**
57     * When we connect to multiple profiles, we only want to display a single
58     * error even if they all fail. This tracks that state.
59     */
60    private boolean mIsConnectingErrorPossible;
61
62    /**
63     * Last time a bt profile auto-connect was attempted.
64     * If an ACTION_UUID intent comes in within
65     * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
66     * again with the new UUIDs
67     */
68    private long mConnectAttempted;
69
70    // See mConnectAttempted
71    private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
72
73    /** Auto-connect after pairing only if locally initiated. */
74    private boolean mConnectAfterPairing;
75
76    /**
77     * Describes the current device and profile for logging.
78     *
79     * @param profile Profile to describe
80     * @return Description of the device and profile
81     */
82    private String describe(LocalBluetoothProfile profile) {
83        StringBuilder sb = new StringBuilder();
84        sb.append("Address:").append(mDevice);
85        if (profile != null) {
86            sb.append(" Profile:").append(profile);
87        }
88
89        return sb.toString();
90    }
91
92    void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
93        if (Utils.D) {
94            Log.d(TAG, "onProfileStateChanged: profile " + profile +
95                    " newProfileState " + newProfileState);
96        }
97
98        if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
99            if (!mProfiles.contains(profile)) {
100                mProfiles.add(profile);
101            }
102        }
103    }
104
105    CachedBluetoothDevice(LocalBluetoothAdapter adapter,
106            LocalBluetoothProfileManager profileManager,
107            BluetoothDevice device) {
108        mLocalAdapter = adapter;
109        mProfileManager = profileManager;
110        mDevice = device;
111        fillData();
112    }
113
114    void disconnect() {
115        for (LocalBluetoothProfile profile : mProfiles) {
116            disconnect(profile);
117        }
118    }
119
120    void disconnect(LocalBluetoothProfile profile) {
121        if (profile.disconnect(mDevice)) {
122            if (Utils.D) {
123                Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
124            }
125        }
126    }
127
128    void connect(boolean connectAllProfiles) {
129        if (!ensurePaired()) {
130            return;
131        }
132
133        mConnectAttempted = SystemClock.elapsedRealtime();
134        connectWithoutResettingTimer(connectAllProfiles);
135    }
136
137    void onBondingDockConnect() {
138        // Attempt to connect if UUIDs are available. Otherwise,
139        // we will connect when the ACTION_UUID intent arrives.
140        connect(false);
141    }
142
143    private void connectWithoutResettingTimer(boolean connectAllProfiles) {
144        // Try to initialize the profiles if they were not.
145        if (mProfiles.isEmpty()) {
146            if (!updateProfiles()) {
147                // If UUIDs are not available yet, connect will be happen
148                // upon arrival of the ACTION_UUID intent.
149                if (DEBUG) Log.d(TAG, "No profiles. Maybe we will connect later");
150                return;
151            }
152        }
153
154        // Reset the only-show-one-error-dialog tracking variable
155        mIsConnectingErrorPossible = true;
156
157        int preferredProfiles = 0;
158        for (LocalBluetoothProfile profile : mProfiles) {
159            if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
160                if (profile.isPreferred(mDevice)) {
161                    ++preferredProfiles;
162                    connectInt(profile);
163                }
164            }
165        }
166        if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
167
168        if (preferredProfiles == 0) {
169            connectAutoConnectableProfiles();
170        }
171    }
172
173    private void connectAutoConnectableProfiles() {
174        if (!ensurePaired()) {
175            return;
176        }
177        // Reset the only-show-one-error-dialog tracking variable
178        mIsConnectingErrorPossible = true;
179
180        for (LocalBluetoothProfile profile : mProfiles) {
181            if (profile.isAutoConnectable()) {
182                profile.setPreferred(mDevice, true);
183                connectInt(profile);
184            }
185        }
186    }
187
188    /**
189     * Connect this device to the specified profile.
190     *
191     * @param profile the profile to use with the remote device
192     */
193    void connectProfile(LocalBluetoothProfile profile) {
194        mConnectAttempted = SystemClock.elapsedRealtime();
195        // Reset the only-show-one-error-dialog tracking variable
196        mIsConnectingErrorPossible = true;
197        connectInt(profile);
198    }
199
200    private void connectInt(LocalBluetoothProfile profile) {
201        if (!ensurePaired()) {
202            return;
203        }
204        if (profile.connect(mDevice)) {
205            if (Utils.D) {
206                Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
207            }
208            return;
209        }
210        Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
211    }
212
213    private boolean ensurePaired() {
214        if (getBondState() == BluetoothDevice.BOND_NONE) {
215            startPairing();
216            return false;
217        } else {
218            return true;
219        }
220    }
221
222    boolean startPairing() {
223        // Pairing is unreliable while scanning, so cancel discovery
224        if (mLocalAdapter.isDiscovering()) {
225            mLocalAdapter.cancelDiscovery();
226        }
227
228        if (!mDevice.createBond()) {
229            return false;
230        }
231
232        mConnectAfterPairing = true;  // auto-connect after pairing
233        return true;
234    }
235
236    void unpair() {
237        disconnect();
238
239        int state = getBondState();
240
241        if (state == BluetoothDevice.BOND_BONDING) {
242            mDevice.cancelBondProcess();
243        }
244
245        if (state != BluetoothDevice.BOND_NONE) {
246            final BluetoothDevice dev = mDevice;
247            if (dev != null) {
248                final boolean successful = dev.removeBond();
249                if (successful) {
250                    if (Utils.D) {
251                        Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
252                    }
253                } else if (Utils.V) {
254                    Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
255                            describe(null));
256                }
257            }
258        }
259    }
260
261    // TODO: do any of these need to run async on a background thread?
262    private void fillData() {
263        fetchName();
264        fetchBtClass();
265        updateProfiles();
266
267        mVisible = false;
268
269        dispatchAttributesChanged();
270    }
271
272    BluetoothDevice getDevice() {
273        return mDevice;
274    }
275
276    String getName() {
277        return mName;
278    }
279
280    void setName(String name) {
281        if (!mName.equals(name)) {
282            if (TextUtils.isEmpty(name)) {
283                // TODO: use friendly name for unknown device (bug 1181856)
284                mName = mDevice.getAddress();
285            } else {
286                mName = name;
287            }
288            // TODO: save custom device name in preferences
289            dispatchAttributesChanged();
290        }
291    }
292
293    void refreshName() {
294        fetchName();
295        dispatchAttributesChanged();
296    }
297
298    private void fetchName() {
299        mName = mDevice.getName();
300
301        if (TextUtils.isEmpty(mName)) {
302            mName = mDevice.getAddress();
303            if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
304        }
305    }
306
307    void refresh() {
308        dispatchAttributesChanged();
309    }
310
311    boolean isVisible() {
312        return mVisible;
313    }
314
315    void setVisible(boolean visible) {
316        if (mVisible != visible) {
317            mVisible = visible;
318            dispatchAttributesChanged();
319        }
320    }
321
322    int getBondState() {
323        return mDevice.getBondState();
324    }
325
326    void setRssi(short rssi) {
327        if (mRssi != rssi) {
328            mRssi = rssi;
329            dispatchAttributesChanged();
330        }
331    }
332
333    /**
334     * Checks whether we are connected to this device (any profile counts).
335     *
336     * @return Whether it is connected.
337     */
338    boolean isConnected() {
339        for (LocalBluetoothProfile profile : mProfiles) {
340            int status = profile.getConnectionStatus(mDevice);
341            if (status == BluetoothProfile.STATE_CONNECTED) {
342                return true;
343            }
344        }
345
346        return false;
347    }
348
349    boolean isConnectedProfile(LocalBluetoothProfile profile) {
350        int status = profile.getConnectionStatus(mDevice);
351        return status == BluetoothProfile.STATE_CONNECTED;
352
353    }
354
355    boolean isBusy() {
356        for (LocalBluetoothProfile profile : mProfiles) {
357            int status = profile.getConnectionStatus(mDevice);
358            if (status == BluetoothProfile.STATE_CONNECTING
359                    || status == BluetoothProfile.STATE_DISCONNECTING) {
360                return true;
361            }
362        }
363        return getBondState() == BluetoothDevice.BOND_BONDING;
364    }
365
366    /**
367     * Fetches a new value for the cached BT class.
368     */
369    private void fetchBtClass() {
370        mBtClass = mDevice.getBluetoothClass();
371    }
372
373    private boolean updateProfiles() {
374        ParcelUuid[] uuids = mDevice.getUuids();
375        if (uuids == null) return false;
376
377        ParcelUuid[] localUuids = mLocalAdapter.getUuids();
378        if (localUuids == null) return false;
379
380        mProfileManager.updateProfiles(uuids, localUuids, mProfiles);
381
382        if (DEBUG) {
383            Log.e(TAG, "updating profiles for " + mDevice.getName());
384            BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
385
386            if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
387            Log.v(TAG, "UUID:");
388            for (ParcelUuid uuid : uuids) {
389                Log.v(TAG, "  " + uuid);
390            }
391        }
392        return true;
393    }
394
395    /**
396     * Refreshes the UI for the BT class, including fetching the latest value
397     * for the class.
398     */
399    void refreshBtClass() {
400        fetchBtClass();
401        dispatchAttributesChanged();
402    }
403
404    /**
405     * Refreshes the UI when framework alerts us of a UUID change.
406     */
407    void onUuidChanged() {
408        updateProfiles();
409
410        if (DEBUG) {
411            Log.e(TAG, "onUuidChanged: Time since last connect"
412                    + (SystemClock.elapsedRealtime() - mConnectAttempted));
413        }
414
415        /*
416         * If a connect was attempted earlier without any UUID, we will do the
417         * connect now.
418         */
419        if (!mProfiles.isEmpty()
420                && (mConnectAttempted + MAX_UUID_DELAY_FOR_AUTO_CONNECT) > SystemClock
421                        .elapsedRealtime()) {
422            connectWithoutResettingTimer(false);
423        }
424        dispatchAttributesChanged();
425    }
426
427    void onBondingStateChanged(int bondState) {
428        if (bondState == BluetoothDevice.BOND_NONE) {
429            mProfiles.clear();
430            mConnectAfterPairing = false;  // cancel auto-connect
431        }
432
433        refresh();
434
435        if (bondState == BluetoothDevice.BOND_BONDED) {
436            if (mDevice.isBluetoothDock()) {
437                onBondingDockConnect();
438            } else if (mConnectAfterPairing) {
439                connect(false);
440            }
441            mConnectAfterPairing = false;
442        }
443    }
444
445    void setBtClass(BluetoothClass btClass) {
446        if (btClass != null && mBtClass != btClass) {
447            mBtClass = btClass;
448            dispatchAttributesChanged();
449        }
450    }
451
452    BluetoothClass getBtClass() {
453        return mBtClass;
454    }
455
456    List<LocalBluetoothProfile> getProfiles() {
457        return Collections.unmodifiableList(mProfiles);
458    }
459
460    List<LocalBluetoothProfile> getConnectableProfiles() {
461        List<LocalBluetoothProfile> connectableProfiles =
462                new ArrayList<LocalBluetoothProfile>();
463        for (LocalBluetoothProfile profile : mProfiles) {
464            if (profile.isConnectable()) {
465                connectableProfiles.add(profile);
466            }
467        }
468        return connectableProfiles;
469    }
470
471    void registerCallback(Callback callback) {
472        synchronized (mCallbacks) {
473            mCallbacks.add(callback);
474        }
475    }
476
477    void unregisterCallback(Callback callback) {
478        synchronized (mCallbacks) {
479            mCallbacks.remove(callback);
480        }
481    }
482
483    private void dispatchAttributesChanged() {
484        synchronized (mCallbacks) {
485            for (Callback callback : mCallbacks) {
486                callback.onDeviceAttributesChanged();
487            }
488        }
489    }
490
491    @Override
492    public String toString() {
493        return mDevice.toString();
494    }
495
496    @Override
497    public boolean equals(Object o) {
498        if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
499            return false;
500        }
501        return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
502    }
503
504    @Override
505    public int hashCode() {
506        return mDevice.getAddress().hashCode();
507    }
508
509    // This comparison uses non-final fields so the sort order may change
510    // when device attributes change (such as bonding state). Settings
511    // will completely refresh the device list when this happens.
512    public int compareTo(CachedBluetoothDevice another) {
513        // Connected above not connected
514        int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
515        if (comparison != 0) return comparison;
516
517        // Paired above not paired
518        comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
519            (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
520        if (comparison != 0) return comparison;
521
522        // Visible above not visible
523        comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
524        if (comparison != 0) return comparison;
525
526        // Stronger signal above weaker signal
527        comparison = another.mRssi - mRssi;
528        if (comparison != 0) return comparison;
529
530        // Fallback on name
531        return mName.compareTo(another.mName);
532    }
533
534    public interface Callback {
535        void onDeviceAttributesChanged();
536    }
537}
538