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.settingslib.bluetooth;
18
19import android.bluetooth.BluetoothClass;
20import android.bluetooth.BluetoothDevice;
21import android.bluetooth.BluetoothHearingAid;
22import android.bluetooth.BluetoothProfile;
23import android.bluetooth.BluetoothUuid;
24import android.content.Context;
25import android.content.SharedPreferences;
26import android.media.AudioManager;
27import android.os.ParcelUuid;
28import android.os.SystemClock;
29import android.text.TextUtils;
30import android.util.Log;
31import android.bluetooth.BluetoothAdapter;
32import android.support.annotation.VisibleForTesting;
33
34import com.android.settingslib.R;
35
36import java.util.ArrayList;
37import java.util.Collection;
38import java.util.Collections;
39import java.util.HashMap;
40import java.util.List;
41
42/**
43 * CachedBluetoothDevice represents a remote Bluetooth device. It contains
44 * attributes of the device (such as the address, name, RSSI, etc.) and
45 * functionality that can be performed on the device (connect, pair, disconnect,
46 * etc.).
47 */
48public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
49    private static final String TAG = "CachedBluetoothDevice";
50    private static final boolean DEBUG = Utils.V;
51
52    private final Context mContext;
53    private final LocalBluetoothAdapter mLocalAdapter;
54    private final LocalBluetoothProfileManager mProfileManager;
55    private final AudioManager mAudioManager;
56    private final BluetoothDevice mDevice;
57    //TODO: consider remove, BluetoothDevice.getName() is already cached
58    private String mName;
59    private long mHiSyncId;
60    // Need this since there is no method for getting RSSI
61    private short mRssi;
62    //TODO: consider remove, BluetoothDevice.getBluetoothClass() is already cached
63    private BluetoothClass mBtClass;
64    private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
65
66    private final List<LocalBluetoothProfile> mProfiles =
67            new ArrayList<LocalBluetoothProfile>();
68
69    // List of profiles that were previously in mProfiles, but have been removed
70    private final List<LocalBluetoothProfile> mRemovedProfiles =
71            new ArrayList<LocalBluetoothProfile>();
72
73    // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
74    private boolean mLocalNapRoleConnected;
75
76    private boolean mJustDiscovered;
77
78    private int mMessageRejectionCount;
79
80    private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
81
82    // Following constants indicate the user's choices of Phone book/message access settings
83    // User hasn't made any choice or settings app has wiped out the memory
84    public final static int ACCESS_UNKNOWN = 0;
85    // User has accepted the connection and let Settings app remember the decision
86    public final static int ACCESS_ALLOWED = 1;
87    // User has rejected the connection and let Settings app remember the decision
88    public final static int ACCESS_REJECTED = 2;
89
90    // How many times user should reject the connection to make the choice persist.
91    private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
92
93    private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
94
95    /**
96     * When we connect to multiple profiles, we only want to display a single
97     * error even if they all fail. This tracks that state.
98     */
99    private boolean mIsConnectingErrorPossible;
100
101    public long getHiSyncId() {
102        return mHiSyncId;
103    }
104
105    public void setHiSyncId(long id) {
106        if (Utils.D) {
107            Log.d(TAG, "setHiSyncId: mDevice " + mDevice + ", id " + id);
108        }
109        mHiSyncId = id;
110    }
111
112    /**
113     * Last time a bt profile auto-connect was attempted.
114     * If an ACTION_UUID intent comes in within
115     * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
116     * again with the new UUIDs
117     */
118    private long mConnectAttempted;
119
120    // See mConnectAttempted
121    private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
122    private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
123
124    // Active device state
125    private boolean mIsActiveDeviceA2dp = false;
126    private boolean mIsActiveDeviceHeadset = false;
127    private boolean mIsActiveDeviceHearingAid = false;
128    /**
129     * Describes the current device and profile for logging.
130     *
131     * @param profile Profile to describe
132     * @return Description of the device and profile
133     */
134    private String describe(LocalBluetoothProfile profile) {
135        StringBuilder sb = new StringBuilder();
136        sb.append("Address:").append(mDevice);
137        if (profile != null) {
138            sb.append(" Profile:").append(profile);
139        }
140
141        return sb.toString();
142    }
143
144    void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
145        if (Utils.D) {
146            Log.d(TAG, "onProfileStateChanged: profile " + profile +
147                    " newProfileState " + newProfileState);
148        }
149        if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
150        {
151            if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
152            return;
153        }
154        mProfileConnectionState.put(profile, newProfileState);
155        if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
156            if (profile instanceof MapProfile) {
157                profile.setPreferred(mDevice, true);
158            }
159            if (!mProfiles.contains(profile)) {
160                mRemovedProfiles.remove(profile);
161                mProfiles.add(profile);
162                if (profile instanceof PanProfile &&
163                        ((PanProfile) profile).isLocalRoleNap(mDevice)) {
164                    // Device doesn't support NAP, so remove PanProfile on disconnect
165                    mLocalNapRoleConnected = true;
166                }
167            }
168        } else if (profile instanceof MapProfile &&
169                newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
170            profile.setPreferred(mDevice, false);
171        } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
172                ((PanProfile) profile).isLocalRoleNap(mDevice) &&
173                newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
174            Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
175            mProfiles.remove(profile);
176            mRemovedProfiles.add(profile);
177            mLocalNapRoleConnected = false;
178        }
179        fetchActiveDevices();
180    }
181
182    CachedBluetoothDevice(Context context,
183                          LocalBluetoothAdapter adapter,
184                          LocalBluetoothProfileManager profileManager,
185                          BluetoothDevice device) {
186        mContext = context;
187        mLocalAdapter = adapter;
188        mProfileManager = profileManager;
189        mAudioManager = context.getSystemService(AudioManager.class);
190        mDevice = device;
191        mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
192        fillData();
193        mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
194    }
195
196    public void disconnect() {
197        for (LocalBluetoothProfile profile : mProfiles) {
198            disconnect(profile);
199        }
200        // Disconnect  PBAP server in case its connected
201        // This is to ensure all the profiles are disconnected as some CK/Hs do not
202        // disconnect  PBAP connection when HF connection is brought down
203        PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
204        if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
205        {
206            PbapProfile.disconnect(mDevice);
207        }
208    }
209
210    public void disconnect(LocalBluetoothProfile profile) {
211        if (profile.disconnect(mDevice)) {
212            if (Utils.D) {
213                Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
214            }
215        }
216    }
217
218    public void connect(boolean connectAllProfiles) {
219        if (!ensurePaired()) {
220            return;
221        }
222
223        mConnectAttempted = SystemClock.elapsedRealtime();
224        connectWithoutResettingTimer(connectAllProfiles);
225    }
226
227    void onBondingDockConnect() {
228        // Attempt to connect if UUIDs are available. Otherwise,
229        // we will connect when the ACTION_UUID intent arrives.
230        connect(false);
231    }
232
233    private void connectWithoutResettingTimer(boolean connectAllProfiles) {
234        // Try to initialize the profiles if they were not.
235        if (mProfiles.isEmpty()) {
236            // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
237            // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
238            // from bluetooth stack but ACTION.uuid is not sent yet.
239            // Eventually ACTION.uuid will be received which shall trigger the connection of the
240            // various profiles
241            // If UUIDs are not available yet, connect will be happen
242            // upon arrival of the ACTION_UUID intent.
243            Log.d(TAG, "No profiles. Maybe we will connect later");
244            return;
245        }
246
247        // Reset the only-show-one-error-dialog tracking variable
248        mIsConnectingErrorPossible = true;
249
250        int preferredProfiles = 0;
251        for (LocalBluetoothProfile profile : mProfiles) {
252            if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
253                if (profile.isPreferred(mDevice)) {
254                    ++preferredProfiles;
255                    connectInt(profile);
256                }
257            }
258        }
259        if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
260
261        if (preferredProfiles == 0) {
262            connectAutoConnectableProfiles();
263        }
264    }
265
266    private void connectAutoConnectableProfiles() {
267        if (!ensurePaired()) {
268            return;
269        }
270        // Reset the only-show-one-error-dialog tracking variable
271        mIsConnectingErrorPossible = true;
272
273        for (LocalBluetoothProfile profile : mProfiles) {
274            if (profile.isAutoConnectable()) {
275                profile.setPreferred(mDevice, true);
276                connectInt(profile);
277            }
278        }
279    }
280
281    /**
282     * Connect this device to the specified profile.
283     *
284     * @param profile the profile to use with the remote device
285     */
286    public void connectProfile(LocalBluetoothProfile profile) {
287        mConnectAttempted = SystemClock.elapsedRealtime();
288        // Reset the only-show-one-error-dialog tracking variable
289        mIsConnectingErrorPossible = true;
290        connectInt(profile);
291        // Refresh the UI based on profile.connect() call
292        refresh();
293    }
294
295    synchronized void connectInt(LocalBluetoothProfile profile) {
296        if (!ensurePaired()) {
297            return;
298        }
299        if (profile.connect(mDevice)) {
300            if (Utils.D) {
301                Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
302            }
303            return;
304        }
305        Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
306    }
307
308    private boolean ensurePaired() {
309        if (getBondState() == BluetoothDevice.BOND_NONE) {
310            startPairing();
311            return false;
312        } else {
313            return true;
314        }
315    }
316
317    public boolean startPairing() {
318        // Pairing is unreliable while scanning, so cancel discovery
319        if (mLocalAdapter.isDiscovering()) {
320            mLocalAdapter.cancelDiscovery();
321        }
322
323        if (!mDevice.createBond()) {
324            return false;
325        }
326
327        return true;
328    }
329
330    /**
331     * Return true if user initiated pairing on this device. The message text is
332     * slightly different for local vs. remote initiated pairing dialogs.
333     */
334    boolean isUserInitiatedPairing() {
335        return mDevice.isBondingInitiatedLocally();
336    }
337
338    public void unpair() {
339        int state = getBondState();
340
341        if (state == BluetoothDevice.BOND_BONDING) {
342            mDevice.cancelBondProcess();
343        }
344
345        if (state != BluetoothDevice.BOND_NONE) {
346            final BluetoothDevice dev = mDevice;
347            if (dev != null) {
348                final boolean successful = dev.removeBond();
349                if (successful) {
350                    if (Utils.D) {
351                        Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
352                    }
353                } else if (Utils.V) {
354                    Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
355                        describe(null));
356                }
357            }
358        }
359    }
360
361    public int getProfileConnectionState(LocalBluetoothProfile profile) {
362        if (mProfileConnectionState.get(profile) == null) {
363            // If cache is empty make the binder call to get the state
364            int state = profile.getConnectionStatus(mDevice);
365            mProfileConnectionState.put(profile, state);
366        }
367        return mProfileConnectionState.get(profile);
368    }
369
370    public void clearProfileConnectionState ()
371    {
372        if (Utils.D) {
373            Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
374        }
375        for (LocalBluetoothProfile profile :getProfiles()) {
376            mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
377        }
378    }
379
380    // TODO: do any of these need to run async on a background thread?
381    private void fillData() {
382        fetchName();
383        fetchBtClass();
384        updateProfiles();
385        fetchActiveDevices();
386        migratePhonebookPermissionChoice();
387        migrateMessagePermissionChoice();
388        fetchMessageRejectionCount();
389
390        dispatchAttributesChanged();
391    }
392
393    public BluetoothDevice getDevice() {
394        return mDevice;
395    }
396
397    /**
398     * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
399     * causes problems in tests since BluetoothDevice is final and cannot be mocked.
400     * @return the address of this device
401     */
402    public String getAddress() {
403        return mDevice.getAddress();
404    }
405
406    public String getName() {
407        return mName;
408    }
409
410    /**
411     * Populate name from BluetoothDevice.ACTION_FOUND intent
412     */
413    void setNewName(String name) {
414        if (mName == null) {
415            mName = name;
416            if (mName == null || TextUtils.isEmpty(mName)) {
417                mName = mDevice.getAddress();
418            }
419            dispatchAttributesChanged();
420        }
421    }
422
423    /**
424     * User changes the device name
425     * @param name new alias name to be set, should never be null
426     */
427    public void setName(String name) {
428        // Prevent mName to be set to null if setName(null) is called
429        if (name != null && !TextUtils.equals(name, mName)) {
430            mName = name;
431            mDevice.setAlias(name);
432            dispatchAttributesChanged();
433        }
434    }
435
436    /**
437     * Set this device as active device
438     * @return true if at least one profile on this device is set to active, false otherwise
439     */
440    public boolean setActive() {
441        boolean result = false;
442        A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
443        if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) {
444            if (a2dpProfile.setActiveDevice(getDevice())) {
445                Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this);
446                result = true;
447            }
448        }
449        HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
450        if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) {
451            if (headsetProfile.setActiveDevice(getDevice())) {
452                Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this);
453                result = true;
454            }
455        }
456        HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
457        if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) {
458            if (hearingAidProfile.setActiveDevice(getDevice())) {
459                Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this);
460                result = true;
461            }
462        }
463        return result;
464    }
465
466    void refreshName() {
467        fetchName();
468        dispatchAttributesChanged();
469    }
470
471    private void fetchName() {
472        mName = mDevice.getAliasName();
473
474        if (TextUtils.isEmpty(mName)) {
475            mName = mDevice.getAddress();
476            if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
477        }
478    }
479
480    /**
481     * Checks if device has a human readable name besides MAC address
482     * @return true if device's alias name is not null nor empty, false otherwise
483     */
484    public boolean hasHumanReadableName() {
485        return !TextUtils.isEmpty(mDevice.getAliasName());
486    }
487
488    /**
489     * Get battery level from remote device
490     * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
491     */
492    public int getBatteryLevel() {
493        return mDevice.getBatteryLevel();
494    }
495
496    void refresh() {
497        dispatchAttributesChanged();
498    }
499
500    public void setJustDiscovered(boolean justDiscovered) {
501        if (mJustDiscovered != justDiscovered) {
502            mJustDiscovered = justDiscovered;
503            dispatchAttributesChanged();
504        }
505    }
506
507    public int getBondState() {
508        return mDevice.getBondState();
509    }
510
511    /**
512     * Update the device status as active or non-active per Bluetooth profile.
513     *
514     * @param isActive true if the device is active
515     * @param bluetoothProfile the Bluetooth profile
516     */
517    public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) {
518        boolean changed = false;
519        switch (bluetoothProfile) {
520        case BluetoothProfile.A2DP:
521            changed = (mIsActiveDeviceA2dp != isActive);
522            mIsActiveDeviceA2dp = isActive;
523            break;
524        case BluetoothProfile.HEADSET:
525            changed = (mIsActiveDeviceHeadset != isActive);
526            mIsActiveDeviceHeadset = isActive;
527            break;
528        case BluetoothProfile.HEARING_AID:
529            changed = (mIsActiveDeviceHearingAid != isActive);
530            mIsActiveDeviceHearingAid = isActive;
531            break;
532        default:
533            Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
534                    " isActive " + isActive);
535            break;
536        }
537        if (changed) {
538            dispatchAttributesChanged();
539        }
540    }
541
542    /**
543     * Update the profile audio state.
544     */
545    void onAudioModeChanged() {
546        dispatchAttributesChanged();
547    }
548    /**
549     * Get the device status as active or non-active per Bluetooth profile.
550     *
551     * @param bluetoothProfile the Bluetooth profile
552     * @return true if the device is active
553     */
554    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
555    public boolean isActiveDevice(int bluetoothProfile) {
556        switch (bluetoothProfile) {
557            case BluetoothProfile.A2DP:
558                return mIsActiveDeviceA2dp;
559            case BluetoothProfile.HEADSET:
560                return mIsActiveDeviceHeadset;
561            case BluetoothProfile.HEARING_AID:
562                return mIsActiveDeviceHearingAid;
563            default:
564                Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
565                break;
566        }
567        return false;
568    }
569
570    void setRssi(short rssi) {
571        if (mRssi != rssi) {
572            mRssi = rssi;
573            dispatchAttributesChanged();
574        }
575    }
576
577    /**
578     * Checks whether we are connected to this device (any profile counts).
579     *
580     * @return Whether it is connected.
581     */
582    public boolean isConnected() {
583        for (LocalBluetoothProfile profile : mProfiles) {
584            int status = getProfileConnectionState(profile);
585            if (status == BluetoothProfile.STATE_CONNECTED) {
586                return true;
587            }
588        }
589
590        return false;
591    }
592
593    public boolean isConnectedProfile(LocalBluetoothProfile profile) {
594        int status = getProfileConnectionState(profile);
595        return status == BluetoothProfile.STATE_CONNECTED;
596
597    }
598
599    public boolean isBusy() {
600        for (LocalBluetoothProfile profile : mProfiles) {
601            int status = getProfileConnectionState(profile);
602            if (status == BluetoothProfile.STATE_CONNECTING
603                    || status == BluetoothProfile.STATE_DISCONNECTING) {
604                return true;
605            }
606        }
607        return getBondState() == BluetoothDevice.BOND_BONDING;
608    }
609
610    /**
611     * Fetches a new value for the cached BT class.
612     */
613    private void fetchBtClass() {
614        mBtClass = mDevice.getBluetoothClass();
615    }
616
617    private boolean updateProfiles() {
618        ParcelUuid[] uuids = mDevice.getUuids();
619        if (uuids == null) return false;
620
621        ParcelUuid[] localUuids = mLocalAdapter.getUuids();
622        if (localUuids == null) return false;
623
624        /*
625         * Now we know if the device supports PBAP, update permissions...
626         */
627        processPhonebookAccess();
628
629        mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
630                                       mLocalNapRoleConnected, mDevice);
631
632        if (DEBUG) {
633            Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
634            BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
635
636            if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
637            Log.v(TAG, "UUID:");
638            for (ParcelUuid uuid : uuids) {
639                Log.v(TAG, "  " + uuid);
640            }
641        }
642        return true;
643    }
644
645    private void fetchActiveDevices() {
646        A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
647        if (a2dpProfile != null) {
648            mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
649        }
650        HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
651        if (headsetProfile != null) {
652            mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
653        }
654        HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
655        if (hearingAidProfile != null) {
656            mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
657        }
658    }
659
660    /**
661     * Refreshes the UI for the BT class, including fetching the latest value
662     * for the class.
663     */
664    void refreshBtClass() {
665        fetchBtClass();
666        dispatchAttributesChanged();
667    }
668
669    /**
670     * Refreshes the UI when framework alerts us of a UUID change.
671     */
672    void onUuidChanged() {
673        updateProfiles();
674        ParcelUuid[] uuids = mDevice.getUuids();
675
676        long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
677        if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
678            timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
679        }
680
681        if (DEBUG) {
682            Log.d(TAG, "onUuidChanged: Time since last connect"
683                    + (SystemClock.elapsedRealtime() - mConnectAttempted));
684        }
685
686        /*
687         * If a connect was attempted earlier without any UUID, we will do the connect now.
688         * Otherwise, allow the connect on UUID change.
689         */
690        if (!mProfiles.isEmpty()
691                && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
692            connectWithoutResettingTimer(false);
693        }
694
695        dispatchAttributesChanged();
696    }
697
698    void onBondingStateChanged(int bondState) {
699        if (bondState == BluetoothDevice.BOND_NONE) {
700            mProfiles.clear();
701            setPhonebookPermissionChoice(ACCESS_UNKNOWN);
702            setMessagePermissionChoice(ACCESS_UNKNOWN);
703            setSimPermissionChoice(ACCESS_UNKNOWN);
704            mMessageRejectionCount = 0;
705            saveMessageRejectionCount();
706        }
707
708        refresh();
709
710        if (bondState == BluetoothDevice.BOND_BONDED) {
711            if (mDevice.isBluetoothDock()) {
712                onBondingDockConnect();
713            } else if (mDevice.isBondingInitiatedLocally()) {
714                connect(false);
715            }
716        }
717    }
718
719    void setBtClass(BluetoothClass btClass) {
720        if (btClass != null && mBtClass != btClass) {
721            mBtClass = btClass;
722            dispatchAttributesChanged();
723        }
724    }
725
726    public BluetoothClass getBtClass() {
727        return mBtClass;
728    }
729
730    public List<LocalBluetoothProfile> getProfiles() {
731        return Collections.unmodifiableList(mProfiles);
732    }
733
734    public List<LocalBluetoothProfile> getConnectableProfiles() {
735        List<LocalBluetoothProfile> connectableProfiles =
736                new ArrayList<LocalBluetoothProfile>();
737        for (LocalBluetoothProfile profile : mProfiles) {
738            if (profile.isConnectable()) {
739                connectableProfiles.add(profile);
740            }
741        }
742        return connectableProfiles;
743    }
744
745    public List<LocalBluetoothProfile> getRemovedProfiles() {
746        return mRemovedProfiles;
747    }
748
749    public void registerCallback(Callback callback) {
750        synchronized (mCallbacks) {
751            mCallbacks.add(callback);
752        }
753    }
754
755    public void unregisterCallback(Callback callback) {
756        synchronized (mCallbacks) {
757            mCallbacks.remove(callback);
758        }
759    }
760
761    private void dispatchAttributesChanged() {
762        synchronized (mCallbacks) {
763            for (Callback callback : mCallbacks) {
764                callback.onDeviceAttributesChanged();
765            }
766        }
767    }
768
769    @Override
770    public String toString() {
771        return mDevice.toString();
772    }
773
774    @Override
775    public boolean equals(Object o) {
776        if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
777            return false;
778        }
779        return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
780    }
781
782    @Override
783    public int hashCode() {
784        return mDevice.getAddress().hashCode();
785    }
786
787    // This comparison uses non-final fields so the sort order may change
788    // when device attributes change (such as bonding state). Settings
789    // will completely refresh the device list when this happens.
790    public int compareTo(CachedBluetoothDevice another) {
791        // Connected above not connected
792        int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
793        if (comparison != 0) return comparison;
794
795        // Paired above not paired
796        comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
797            (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
798        if (comparison != 0) return comparison;
799
800        // Just discovered above discovered in the past
801        comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
802        if (comparison != 0) return comparison;
803
804        // Stronger signal above weaker signal
805        comparison = another.mRssi - mRssi;
806        if (comparison != 0) return comparison;
807
808        // Fallback on name
809        return mName.compareTo(another.mName);
810    }
811
812    public interface Callback {
813        void onDeviceAttributesChanged();
814    }
815
816    public int getPhonebookPermissionChoice() {
817        int permission = mDevice.getPhonebookAccessPermission();
818        if (permission == BluetoothDevice.ACCESS_ALLOWED) {
819            return ACCESS_ALLOWED;
820        } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
821            return ACCESS_REJECTED;
822        }
823        return ACCESS_UNKNOWN;
824    }
825
826    public void setPhonebookPermissionChoice(int permissionChoice) {
827        int permission = BluetoothDevice.ACCESS_UNKNOWN;
828        if (permissionChoice == ACCESS_ALLOWED) {
829            permission = BluetoothDevice.ACCESS_ALLOWED;
830        } else if (permissionChoice == ACCESS_REJECTED) {
831            permission = BluetoothDevice.ACCESS_REJECTED;
832        }
833        mDevice.setPhonebookAccessPermission(permission);
834    }
835
836    // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
837    // app's shared preferences).
838    private void migratePhonebookPermissionChoice() {
839        SharedPreferences preferences = mContext.getSharedPreferences(
840                "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
841        if (!preferences.contains(mDevice.getAddress())) {
842            return;
843        }
844
845        if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
846            int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
847            if (oldPermission == ACCESS_ALLOWED) {
848                mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
849            } else if (oldPermission == ACCESS_REJECTED) {
850                mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
851            }
852        }
853
854        SharedPreferences.Editor editor = preferences.edit();
855        editor.remove(mDevice.getAddress());
856        editor.commit();
857    }
858
859    public int getMessagePermissionChoice() {
860        int permission = mDevice.getMessageAccessPermission();
861        if (permission == BluetoothDevice.ACCESS_ALLOWED) {
862            return ACCESS_ALLOWED;
863        } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
864            return ACCESS_REJECTED;
865        }
866        return ACCESS_UNKNOWN;
867    }
868
869    public void setMessagePermissionChoice(int permissionChoice) {
870        int permission = BluetoothDevice.ACCESS_UNKNOWN;
871        if (permissionChoice == ACCESS_ALLOWED) {
872            permission = BluetoothDevice.ACCESS_ALLOWED;
873        } else if (permissionChoice == ACCESS_REJECTED) {
874            permission = BluetoothDevice.ACCESS_REJECTED;
875        }
876        mDevice.setMessageAccessPermission(permission);
877    }
878
879    public int getSimPermissionChoice() {
880        int permission = mDevice.getSimAccessPermission();
881        if (permission == BluetoothDevice.ACCESS_ALLOWED) {
882            return ACCESS_ALLOWED;
883        } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
884            return ACCESS_REJECTED;
885        }
886        return ACCESS_UNKNOWN;
887    }
888
889    void setSimPermissionChoice(int permissionChoice) {
890        int permission = BluetoothDevice.ACCESS_UNKNOWN;
891        if (permissionChoice == ACCESS_ALLOWED) {
892            permission = BluetoothDevice.ACCESS_ALLOWED;
893        } else if (permissionChoice == ACCESS_REJECTED) {
894            permission = BluetoothDevice.ACCESS_REJECTED;
895        }
896        mDevice.setSimAccessPermission(permission);
897    }
898
899    // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
900    // app's shared preferences).
901    private void migrateMessagePermissionChoice() {
902        SharedPreferences preferences = mContext.getSharedPreferences(
903                "bluetooth_message_permission", Context.MODE_PRIVATE);
904        if (!preferences.contains(mDevice.getAddress())) {
905            return;
906        }
907
908        if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
909            int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
910            if (oldPermission == ACCESS_ALLOWED) {
911                mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
912            } else if (oldPermission == ACCESS_REJECTED) {
913                mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
914            }
915        }
916
917        SharedPreferences.Editor editor = preferences.edit();
918        editor.remove(mDevice.getAddress());
919        editor.commit();
920    }
921
922    /**
923     * @return Whether this rejection should persist.
924     */
925    public boolean checkAndIncreaseMessageRejectionCount() {
926        if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
927            mMessageRejectionCount++;
928            saveMessageRejectionCount();
929        }
930        return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
931    }
932
933    private void fetchMessageRejectionCount() {
934        SharedPreferences preference = mContext.getSharedPreferences(
935                MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
936        mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
937    }
938
939    private void saveMessageRejectionCount() {
940        SharedPreferences.Editor editor = mContext.getSharedPreferences(
941                MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
942        if (mMessageRejectionCount == 0) {
943            editor.remove(mDevice.getAddress());
944        } else {
945            editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
946        }
947        editor.commit();
948    }
949
950    private void processPhonebookAccess() {
951        if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
952
953        ParcelUuid[] uuids = mDevice.getUuids();
954        if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
955            // The pairing dialog now warns of phone-book access for paired devices.
956            // No separate prompt is displayed after pairing.
957            if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
958                if (mDevice.getBluetoothClass().getDeviceClass()
959                        == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
960                    mDevice.getBluetoothClass().getDeviceClass()
961                        == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
962                    setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
963                } else {
964                    setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
965                }
966            }
967        }
968    }
969
970    public int getMaxConnectionState() {
971        int maxState = BluetoothProfile.STATE_DISCONNECTED;
972        for (LocalBluetoothProfile profile : getProfiles()) {
973            int connectionStatus = getProfileConnectionState(profile);
974            if (connectionStatus > maxState) {
975                maxState = connectionStatus;
976            }
977        }
978        return maxState;
979    }
980
981    /**
982     * @return resource for string that discribes the connection state of this device.
983     * case 1: idle or playing media, show "Active" on the only one A2DP active device.
984     * case 2: in phone call, show "Active" on the only one HFP active device
985     */
986    public String getConnectionSummary() {
987        boolean profileConnected = false;    // Updated as long as BluetoothProfile is connected
988        boolean a2dpConnected = true;        // A2DP is connected
989        boolean hfpConnected = true;         // HFP is connected
990        boolean hearingAidConnected = true;  // Hearing Aid is connected
991
992        for (LocalBluetoothProfile profile : getProfiles()) {
993            int connectionStatus = getProfileConnectionState(profile);
994
995            switch (connectionStatus) {
996                case BluetoothProfile.STATE_CONNECTING:
997                case BluetoothProfile.STATE_DISCONNECTING:
998                    return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
999
1000                case BluetoothProfile.STATE_CONNECTED:
1001                    profileConnected = true;
1002                    break;
1003
1004                case BluetoothProfile.STATE_DISCONNECTED:
1005                    if (profile.isProfileReady()) {
1006                        if ((profile instanceof A2dpProfile) ||
1007                                (profile instanceof A2dpSinkProfile)) {
1008                            a2dpConnected = false;
1009                        } else if ((profile instanceof HeadsetProfile) ||
1010                                (profile instanceof HfpClientProfile)) {
1011                            hfpConnected = false;
1012                        } else if (profile instanceof HearingAidProfile) {
1013                            hearingAidConnected = false;
1014                        }
1015                    }
1016                    break;
1017            }
1018        }
1019
1020        String batteryLevelPercentageString = null;
1021        // Android framework should only set mBatteryLevel to valid range [0-100] or
1022        // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
1023        // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
1024        // be valid
1025        final int batteryLevel = getBatteryLevel();
1026        if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
1027            // TODO: name com.android.settingslib.bluetooth.Utils something different
1028            batteryLevelPercentageString =
1029                    com.android.settingslib.Utils.formatPercentage(batteryLevel);
1030        }
1031
1032        int stringRes = R.string.bluetooth_pairing;
1033        //when profile is connected, information would be available
1034        if (profileConnected) {
1035            if (a2dpConnected || hfpConnected || hearingAidConnected) {
1036                //contain battery information
1037                if (batteryLevelPercentageString != null) {
1038                    //device is in phone call
1039                    if (com.android.settingslib.Utils.isAudioModeOngoingCall(mContext)) {
1040                        if (mIsActiveDeviceHeadset) {
1041                            stringRes = R.string.bluetooth_active_battery_level;
1042                        } else {
1043                            stringRes = R.string.bluetooth_battery_level;
1044                        }
1045                    } else {//device is not in phone call(ex. idle or playing media)
1046                        //need to check if A2DP and HearingAid are exclusive
1047                        if (mIsActiveDeviceHearingAid || mIsActiveDeviceA2dp) {
1048                            stringRes = R.string.bluetooth_active_battery_level;
1049                        } else {
1050                            stringRes = R.string.bluetooth_battery_level;
1051                        }
1052                    }
1053                } else {
1054                    //no battery information
1055                    if (com.android.settingslib.Utils.isAudioModeOngoingCall(mContext)) {
1056                        if (mIsActiveDeviceHeadset) {
1057                            stringRes = R.string.bluetooth_active_no_battery_level;
1058                        }
1059                    } else {
1060                        if (mIsActiveDeviceHearingAid || mIsActiveDeviceA2dp) {
1061                            stringRes = R.string.bluetooth_active_no_battery_level;
1062                        }
1063                    }
1064                }
1065            } else {//unknown profile with battery information
1066                if (batteryLevelPercentageString != null) {
1067                    stringRes = R.string.bluetooth_battery_level;
1068                }
1069            }
1070        }
1071
1072        return (stringRes != R.string.bluetooth_pairing
1073                || getBondState() == BluetoothDevice.BOND_BONDING)
1074                ? mContext.getString(stringRes, batteryLevelPercentageString)
1075                : null;
1076    }
1077
1078    /**
1079     * @return resource for android auto string that describes the connection state of this device.
1080     */
1081    public String getCarConnectionSummary() {
1082        boolean profileConnected = false;       // at least one profile is connected
1083        boolean a2dpNotConnected = false;       // A2DP is preferred but not connected
1084        boolean hfpNotConnected = false;        // HFP is preferred but not connected
1085        boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected
1086
1087        for (LocalBluetoothProfile profile : getProfiles()) {
1088            int connectionStatus = getProfileConnectionState(profile);
1089
1090            switch (connectionStatus) {
1091                case BluetoothProfile.STATE_CONNECTING:
1092                case BluetoothProfile.STATE_DISCONNECTING:
1093                    return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
1094
1095                case BluetoothProfile.STATE_CONNECTED:
1096                    profileConnected = true;
1097                    break;
1098
1099                case BluetoothProfile.STATE_DISCONNECTED:
1100                    if (profile.isProfileReady()) {
1101                        if ((profile instanceof A2dpProfile) ||
1102                                (profile instanceof A2dpSinkProfile)){
1103                            a2dpNotConnected = true;
1104                        } else if ((profile instanceof HeadsetProfile) ||
1105                                (profile instanceof HfpClientProfile)) {
1106                            hfpNotConnected = true;
1107                        } else if (profile instanceof  HearingAidProfile) {
1108                            hearingAidNotConnected = true;
1109                        }
1110                    }
1111                    break;
1112            }
1113        }
1114
1115        String batteryLevelPercentageString = null;
1116        // Android framework should only set mBatteryLevel to valid range [0-100] or
1117        // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
1118        // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
1119        // be valid
1120        final int batteryLevel = getBatteryLevel();
1121        if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
1122            // TODO: name com.android.settingslib.bluetooth.Utils something different
1123            batteryLevelPercentageString =
1124                    com.android.settingslib.Utils.formatPercentage(batteryLevel);
1125        }
1126
1127        // Prepare the string for the Active Device summary
1128        String[] activeDeviceStringsArray = mContext.getResources().getStringArray(
1129                R.array.bluetooth_audio_active_device_summaries);
1130        String activeDeviceString = activeDeviceStringsArray[0];  // Default value: not active
1131        if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) {
1132            activeDeviceString = activeDeviceStringsArray[1];     // Active for Media and Phone
1133        } else {
1134            if (mIsActiveDeviceA2dp) {
1135                activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only
1136            }
1137            if (mIsActiveDeviceHeadset) {
1138                activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only
1139            }
1140        }
1141        if (!hearingAidNotConnected && mIsActiveDeviceHearingAid) {
1142            activeDeviceString = activeDeviceStringsArray[1];
1143            return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1144        }
1145
1146        if (profileConnected) {
1147            if (a2dpNotConnected && hfpNotConnected) {
1148                if (batteryLevelPercentageString != null) {
1149                    return mContext.getString(
1150                            R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
1151                            batteryLevelPercentageString, activeDeviceString);
1152                } else {
1153                    return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp,
1154                            activeDeviceString);
1155                }
1156
1157            } else if (a2dpNotConnected) {
1158                if (batteryLevelPercentageString != null) {
1159                    return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
1160                            batteryLevelPercentageString, activeDeviceString);
1161                } else {
1162                    return mContext.getString(R.string.bluetooth_connected_no_a2dp,
1163                            activeDeviceString);
1164                }
1165
1166            } else if (hfpNotConnected) {
1167                if (batteryLevelPercentageString != null) {
1168                    return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
1169                            batteryLevelPercentageString, activeDeviceString);
1170                } else {
1171                    return mContext.getString(R.string.bluetooth_connected_no_headset,
1172                            activeDeviceString);
1173                }
1174            } else {
1175                if (batteryLevelPercentageString != null) {
1176                    return mContext.getString(R.string.bluetooth_connected_battery_level,
1177                            batteryLevelPercentageString, activeDeviceString);
1178                } else {
1179                    return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
1180                }
1181            }
1182        }
1183
1184        return getBondState() == BluetoothDevice.BOND_BONDING ?
1185                mContext.getString(R.string.bluetooth_pairing) : null;
1186    }
1187
1188    /**
1189     * @return {@code true} if {@code cachedBluetoothDevice} is a2dp device
1190     */
1191    public boolean isA2dpDevice() {
1192        return mProfileManager.getA2dpProfile().getConnectionStatus(mDevice) ==
1193                BluetoothProfile.STATE_CONNECTED;
1194    }
1195
1196    /**
1197     * @return {@code true} if {@code cachedBluetoothDevice} is HFP device
1198     */
1199    public boolean isHfpDevice() {
1200        return mProfileManager.getHeadsetProfile().getConnectionStatus(mDevice) ==
1201                BluetoothProfile.STATE_CONNECTED;
1202    }
1203}
1204