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