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