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.app.AlertDialog;
20import android.bluetooth.BluetoothAdapter;
21import android.bluetooth.BluetoothClass;
22import android.bluetooth.BluetoothDevice;
23import android.content.Context;
24import android.content.DialogInterface;
25import android.content.Intent;
26import android.content.res.Resources;
27import android.os.ParcelUuid;
28import android.os.SystemClock;
29import android.text.TextUtils;
30import android.util.Log;
31import android.view.ContextMenu;
32import android.view.Menu;
33import android.view.MenuItem;
34
35import com.android.settings.R;
36import com.android.settings.bluetooth.LocalBluetoothProfileManager.Profile;
37
38import java.util.ArrayList;
39import java.util.List;
40import java.util.Set;
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 D = LocalBluetoothManager.D;
51    private static final boolean V = LocalBluetoothManager.V;
52    private static final boolean DEBUG = false;
53
54    private static final int CONTEXT_ITEM_CONNECT = Menu.FIRST + 1;
55    private static final int CONTEXT_ITEM_DISCONNECT = Menu.FIRST + 2;
56    private static final int CONTEXT_ITEM_UNPAIR = Menu.FIRST + 3;
57    private static final int CONTEXT_ITEM_CONNECT_ADVANCED = Menu.FIRST + 4;
58
59    private final BluetoothDevice mDevice;
60    private String mName;
61    private short mRssi;
62    private BluetoothClass mBtClass;
63
64    private List<Profile> mProfiles = new ArrayList<Profile>();
65
66    private boolean mVisible;
67
68    private final LocalBluetoothManager mLocalManager;
69
70    private List<Callback> mCallbacks = new ArrayList<Callback>();
71
72    /**
73     * When we connect to multiple profiles, we only want to display a single
74     * error even if they all fail. This tracks that state.
75     */
76    private boolean mIsConnectingErrorPossible;
77
78    /**
79     * Last time a bt profile auto-connect was attempted.
80     * If an ACTION_UUID intent comes in within
81     * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
82     * again with the new UUIDs
83     */
84    private long mConnectAttempted;
85
86    // See mConnectAttempted
87    private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
88
89    /** Auto-connect after pairing only if locally initiated. */
90    private boolean mConnectAfterPairing;
91
92    /**
93     * Describes the current device and profile for logging.
94     *
95     * @param profile Profile to describe
96     * @return Description of the device and profile
97     */
98    private String describe(CachedBluetoothDevice cachedDevice, Profile profile) {
99        StringBuilder sb = new StringBuilder();
100        sb.append("Address:").append(cachedDevice.mDevice);
101        if (profile != null) {
102            sb.append(" Profile:").append(profile.name());
103        }
104
105        return sb.toString();
106    }
107
108    private String describe(Profile profile) {
109        return describe(this, profile);
110    }
111
112    public void onProfileStateChanged(Profile profile, int newProfileState) {
113        if (D) {
114            Log.d(TAG, "onProfileStateChanged: profile " + profile.toString() +
115                    " newProfileState " + newProfileState);
116        }
117
118        int newState = LocalBluetoothProfileManager.getProfileManager(mLocalManager,
119                profile).convertState(newProfileState);
120
121        if (newState == SettingsBtStatus.CONNECTION_STATUS_CONNECTED) {
122            if (!mProfiles.contains(profile)) {
123                mProfiles.add(profile);
124            }
125        }
126    }
127
128    CachedBluetoothDevice(Context context, BluetoothDevice device) {
129        mLocalManager = LocalBluetoothManager.getInstance(context);
130        if (mLocalManager == null) {
131            throw new IllegalStateException(
132                    "Cannot use CachedBluetoothDevice without Bluetooth hardware");
133        }
134
135        mDevice = device;
136
137        fillData();
138    }
139
140    public void onClicked() {
141        int bondState = getBondState();
142
143        if (isConnected()) {
144            askDisconnect();
145        } else if (bondState == BluetoothDevice.BOND_BONDED) {
146            connect();
147        } else if (bondState == BluetoothDevice.BOND_NONE) {
148            pair();
149        }
150    }
151
152    public void disconnect() {
153        for (Profile profile : mProfiles) {
154            disconnect(profile);
155        }
156    }
157
158    public void disconnect(Profile profile) {
159        disconnectInt(this, profile);
160    }
161
162    private boolean disconnectInt(CachedBluetoothDevice cachedDevice, Profile profile) {
163        LocalBluetoothProfileManager profileManager =
164                LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
165        int status = profileManager.getConnectionStatus(cachedDevice.mDevice);
166        if (profileManager.disconnect(cachedDevice.mDevice)) {
167            if (D) {
168                Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
169            }
170            return true;
171        }
172        return false;
173    }
174
175    public void askDisconnect() {
176        Context context = mLocalManager.getForegroundActivity();
177        if (context == null) {
178            // Cannot ask, since we need an activity context
179            disconnect();
180            return;
181        }
182
183        Resources res = context.getResources();
184
185        String name = getName();
186        if (TextUtils.isEmpty(name)) {
187            name = res.getString(R.string.bluetooth_device);
188        }
189        String message = res.getString(R.string.bluetooth_disconnect_blank, name);
190
191        DialogInterface.OnClickListener disconnectListener = new DialogInterface.OnClickListener() {
192            public void onClick(DialogInterface dialog, int which) {
193                disconnect();
194            }
195        };
196
197        new AlertDialog.Builder(context)
198                .setTitle(getName())
199                .setMessage(message)
200                .setPositiveButton(android.R.string.ok, disconnectListener)
201                .setNegativeButton(android.R.string.cancel, null)
202                .show();
203    }
204
205    public void connect() {
206        if (!ensurePaired()) return;
207
208        mConnectAttempted = SystemClock.elapsedRealtime();
209
210        connectWithoutResettingTimer();
211    }
212
213    /*package*/ void onBondingDockConnect() {
214        // Attempt to connect if UUIDs are available. Otherwise,
215        // we will connect when the ACTION_UUID intent arrives.
216        connect();
217    }
218
219    private void connectWithoutResettingTimer() {
220        // Try to initialize the profiles if there were not.
221        if (mProfiles.size() == 0) {
222            if (!updateProfiles()) {
223                // If UUIDs are not available yet, connect will be happen
224                // upon arrival of the ACTION_UUID intent.
225                if (DEBUG) Log.d(TAG, "No profiles. Maybe we will connect later");
226                return;
227            }
228        }
229
230        // Reset the only-show-one-error-dialog tracking variable
231        mIsConnectingErrorPossible = true;
232
233        int preferredProfiles = 0;
234        for (Profile profile : mProfiles) {
235            if (isConnectableProfile(profile)) {
236                LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
237                        .getProfileManager(mLocalManager, profile);
238                if (profileManager.isPreferred(mDevice)) {
239                    ++preferredProfiles;
240                    disconnectConnected(this, profile);
241                    connectInt(this, profile);
242                }
243            }
244        }
245        if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
246
247        if (preferredProfiles == 0) {
248            connectAllProfiles();
249        }
250    }
251
252    private void connectAllProfiles() {
253        if (!ensurePaired()) return;
254
255        // Reset the only-show-one-error-dialog tracking variable
256        mIsConnectingErrorPossible = true;
257
258        for (Profile profile : mProfiles) {
259            if (isConnectableProfile(profile)) {
260                LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
261                        .getProfileManager(mLocalManager, profile);
262                profileManager.setPreferred(mDevice, false);
263                disconnectConnected(this, profile);
264                connectInt(this, profile);
265            }
266        }
267    }
268
269    public void connect(Profile profile) {
270        mConnectAttempted = SystemClock.elapsedRealtime();
271        // Reset the only-show-one-error-dialog tracking variable
272        mIsConnectingErrorPossible = true;
273        disconnectConnected(this, profile);
274        connectInt(this, profile);
275    }
276
277    private void disconnectConnected(CachedBluetoothDevice device, Profile profile) {
278        LocalBluetoothProfileManager profileManager =
279            LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
280        CachedBluetoothDeviceManager cachedDeviceManager = mLocalManager.getCachedDeviceManager();
281        Set<BluetoothDevice> devices = profileManager.getConnectedDevices();
282        if (devices == null) return;
283        for (BluetoothDevice btDevice : devices) {
284            CachedBluetoothDevice cachedDevice = cachedDeviceManager.findDevice(btDevice);
285
286            if (cachedDevice != null && !cachedDevice.equals(device)) {
287                disconnectInt(cachedDevice, profile);
288            }
289        }
290    }
291
292    private boolean connectInt(CachedBluetoothDevice cachedDevice, Profile profile) {
293        if (!cachedDevice.ensurePaired()) return false;
294
295        LocalBluetoothProfileManager profileManager =
296                LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
297        int status = profileManager.getConnectionStatus(cachedDevice.mDevice);
298        if (profileManager.connect(cachedDevice.mDevice)) {
299            if (D) {
300                Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
301            }
302            return true;
303        }
304        Log.i(TAG, "Failed to connect " + profile.toString() + " to " + cachedDevice.mName);
305
306        return false;
307    }
308
309    public void showConnectingError() {
310        if (!mIsConnectingErrorPossible) return;
311        mIsConnectingErrorPossible = false;
312
313        mLocalManager.showError(mDevice, R.string.bluetooth_error_title,
314                R.string.bluetooth_connecting_error_message);
315    }
316
317    private boolean ensurePaired() {
318        if (getBondState() == BluetoothDevice.BOND_NONE) {
319            pair();
320            return false;
321        } else {
322            return true;
323        }
324    }
325
326    public void pair() {
327        BluetoothAdapter adapter = mLocalManager.getBluetoothAdapter();
328
329        // Pairing is unreliable while scanning, so cancel discovery
330        if (adapter.isDiscovering()) {
331            adapter.cancelDiscovery();
332        }
333
334        if (!mDevice.createBond()) {
335            mLocalManager.showError(mDevice, R.string.bluetooth_error_title,
336                    R.string.bluetooth_pairing_error_message);
337            return;
338        }
339
340        mConnectAfterPairing = true;  // auto-connect after pairing
341    }
342
343    public void unpair() {
344        disconnect();
345
346        int state = getBondState();
347
348        if (state == BluetoothDevice.BOND_BONDING) {
349            mDevice.cancelBondProcess();
350        }
351
352        if (state != BluetoothDevice.BOND_NONE) {
353            final BluetoothDevice dev = getDevice();
354            if (dev != null) {
355                final boolean successful = dev.removeBond();
356                if (successful) {
357                    if (D) {
358                        Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
359                    }
360                } else if (V) {
361                    Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
362                            describe(null));
363                }
364            }
365        }
366    }
367
368    private void fillData() {
369        fetchName();
370        fetchBtClass();
371        updateProfiles();
372
373        mVisible = false;
374
375        dispatchAttributesChanged();
376    }
377
378    public BluetoothDevice getDevice() {
379        return mDevice;
380    }
381
382    public String getName() {
383        return mName;
384    }
385
386    public void setName(String name) {
387        if (!mName.equals(name)) {
388            if (TextUtils.isEmpty(name)) {
389                mName = mDevice.getAddress();
390            } else {
391                mName = name;
392            }
393            dispatchAttributesChanged();
394        }
395    }
396
397    public void refreshName() {
398        fetchName();
399        dispatchAttributesChanged();
400    }
401
402    private void fetchName() {
403        mName = mDevice.getName();
404
405        if (TextUtils.isEmpty(mName)) {
406            mName = mDevice.getAddress();
407            if (DEBUG) Log.d(TAG, "Default to address. Device has no name (yet) " + mName);
408        }
409    }
410
411    public void refresh() {
412        dispatchAttributesChanged();
413    }
414
415    public boolean isVisible() {
416        return mVisible;
417    }
418
419    void setVisible(boolean visible) {
420        if (mVisible != visible) {
421            mVisible = visible;
422            dispatchAttributesChanged();
423        }
424    }
425
426    public int getBondState() {
427        return mDevice.getBondState();
428    }
429
430    void setRssi(short rssi) {
431        if (mRssi != rssi) {
432            mRssi = rssi;
433            dispatchAttributesChanged();
434        }
435    }
436
437    /**
438     * Checks whether we are connected to this device (any profile counts).
439     *
440     * @return Whether it is connected.
441     */
442    public boolean isConnected() {
443        for (Profile profile : mProfiles) {
444            int status = LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile)
445                    .getConnectionStatus(mDevice);
446            if (SettingsBtStatus.isConnectionStatusConnected(status)) {
447                return true;
448            }
449        }
450
451        return false;
452    }
453
454    public boolean isBusy() {
455        for (Profile profile : mProfiles) {
456            int status = LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile)
457                    .getConnectionStatus(mDevice);
458            if (SettingsBtStatus.isConnectionStatusBusy(status)) {
459                return true;
460            }
461        }
462
463        if (getBondState() == BluetoothDevice.BOND_BONDING) {
464            return true;
465        }
466
467        return false;
468    }
469
470    public int getBtClassDrawable() {
471        if (mBtClass != null) {
472            switch (mBtClass.getMajorDeviceClass()) {
473            case BluetoothClass.Device.Major.COMPUTER:
474                return R.drawable.ic_bt_laptop;
475
476            case BluetoothClass.Device.Major.PHONE:
477                return R.drawable.ic_bt_cellphone;
478            }
479        } else {
480            Log.w(TAG, "mBtClass is null");
481        }
482
483        if (mProfiles.size() > 0) {
484            if (mProfiles.contains(Profile.A2DP)) {
485                return R.drawable.ic_bt_headphones_a2dp;
486            } else if (mProfiles.contains(Profile.HEADSET)) {
487                return R.drawable.ic_bt_headset_hfp;
488            }
489        } else if (mBtClass != null) {
490            if (mBtClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
491                return R.drawable.ic_bt_headphones_a2dp;
492
493            }
494            if (mBtClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
495                return R.drawable.ic_bt_headset_hfp;
496            }
497        }
498        return 0;
499    }
500
501    /**
502     * Fetches a new value for the cached BT class.
503     */
504    private void fetchBtClass() {
505        mBtClass = mDevice.getBluetoothClass();
506    }
507
508    private boolean updateProfiles() {
509        ParcelUuid[] uuids = mDevice.getUuids();
510        if (uuids == null) return false;
511
512        LocalBluetoothProfileManager.updateProfiles(uuids, mProfiles);
513
514        if (DEBUG) {
515            Log.e(TAG, "updating profiles for " + mDevice.getName());
516
517            boolean printUuids = true;
518            BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
519
520            if (bluetoothClass != null) {
521                if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET) !=
522                    mProfiles.contains(Profile.HEADSET)) {
523                    Log.v(TAG, "headset classbits != uuid");
524                    printUuids = true;
525                }
526
527                if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_A2DP) !=
528                    mProfiles.contains(Profile.A2DP)) {
529                    Log.v(TAG, "a2dp classbits != uuid");
530                    printUuids = true;
531                }
532
533                if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_OPP) !=
534                    mProfiles.contains(Profile.OPP)) {
535                    Log.v(TAG, "opp classbits != uuid");
536                    printUuids = true;
537                }
538            }
539
540            if (printUuids) {
541                if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
542                Log.v(TAG, "UUID:");
543                for (int i = 0; i < uuids.length; i++) {
544                    Log.v(TAG, "  " + uuids[i]);
545                }
546            }
547        }
548        return true;
549    }
550
551    /**
552     * Refreshes the UI for the BT class, including fetching the latest value
553     * for the class.
554     */
555    public void refreshBtClass() {
556        fetchBtClass();
557        dispatchAttributesChanged();
558    }
559
560    /**
561     * Refreshes the UI when framework alerts us of a UUID change.
562     */
563    public void onUuidChanged() {
564        updateProfiles();
565
566        if (DEBUG) {
567            Log.e(TAG, "onUuidChanged: Time since last connect"
568                    + (SystemClock.elapsedRealtime() - mConnectAttempted));
569        }
570
571        /*
572         * If a connect was attempted earlier without any UUID, we will do the
573         * connect now.
574         */
575        if (mProfiles.size() > 0
576                && (mConnectAttempted + MAX_UUID_DELAY_FOR_AUTO_CONNECT) > SystemClock
577                        .elapsedRealtime()) {
578            connectWithoutResettingTimer();
579        }
580        dispatchAttributesChanged();
581    }
582
583    public void onBondingStateChanged(int bondState) {
584        if (bondState == BluetoothDevice.BOND_NONE) {
585            mProfiles.clear();
586            mConnectAfterPairing = false;  // cancel auto-connect
587        }
588
589        refresh();
590
591        if (bondState == BluetoothDevice.BOND_BONDED) {
592            if (mDevice.isBluetoothDock()) {
593                onBondingDockConnect();
594            } else if (mConnectAfterPairing) {
595                connect();
596            }
597            mConnectAfterPairing = false;
598        }
599    }
600
601    public void setBtClass(BluetoothClass btClass) {
602        if (btClass != null && mBtClass != btClass) {
603            mBtClass = btClass;
604            dispatchAttributesChanged();
605        }
606    }
607
608    public int getSummary() {
609        // TODO: clean up
610        int oneOffSummary = getOneOffSummary();
611        if (oneOffSummary != 0) {
612            return oneOffSummary;
613        }
614
615        for (Profile profile : mProfiles) {
616            LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
617                    .getProfileManager(mLocalManager, profile);
618            int connectionStatus = profileManager.getConnectionStatus(mDevice);
619
620            if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus) ||
621                    connectionStatus == SettingsBtStatus.CONNECTION_STATUS_CONNECTING ||
622                    connectionStatus == SettingsBtStatus.CONNECTION_STATUS_DISCONNECTING) {
623                return SettingsBtStatus.getConnectionStatusSummary(connectionStatus);
624            }
625        }
626
627        return SettingsBtStatus.getPairingStatusSummary(getBondState());
628    }
629
630    /**
631     * We have special summaries when particular profiles are connected. This
632     * checks for those states and returns an applicable summary.
633     *
634     * @return A one-off summary that is applicable for the current state, or 0.
635     */
636    private int getOneOffSummary() {
637        boolean isA2dpConnected = false, isHeadsetConnected = false, isConnecting = false;
638
639        if (mProfiles.contains(Profile.A2DP)) {
640            LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
641                    .getProfileManager(mLocalManager, Profile.A2DP);
642            isConnecting = profileManager.getConnectionStatus(mDevice) ==
643                    SettingsBtStatus.CONNECTION_STATUS_CONNECTING;
644            isA2dpConnected = profileManager.isConnected(mDevice);
645        }
646
647        if (mProfiles.contains(Profile.HEADSET)) {
648            LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
649                    .getProfileManager(mLocalManager, Profile.HEADSET);
650            isConnecting |= profileManager.getConnectionStatus(mDevice) ==
651                    SettingsBtStatus.CONNECTION_STATUS_CONNECTING;
652            isHeadsetConnected = profileManager.isConnected(mDevice);
653        }
654
655        if (isConnecting) {
656            // If any of these important profiles is connecting, prefer that
657            return SettingsBtStatus.getConnectionStatusSummary(
658                    SettingsBtStatus.CONNECTION_STATUS_CONNECTING);
659        } else if (isA2dpConnected && isHeadsetConnected) {
660            return R.string.bluetooth_summary_connected_to_a2dp_headset;
661        } else if (isA2dpConnected) {
662            return R.string.bluetooth_summary_connected_to_a2dp;
663        } else if (isHeadsetConnected) {
664            return R.string.bluetooth_summary_connected_to_headset;
665        } else {
666            return 0;
667        }
668    }
669
670    public List<Profile> getConnectableProfiles() {
671        ArrayList<Profile> connectableProfiles = new ArrayList<Profile>();
672        for (Profile profile : mProfiles) {
673            if (isConnectableProfile(profile)) {
674                connectableProfiles.add(profile);
675            }
676        }
677        return connectableProfiles;
678    }
679
680    private boolean isConnectableProfile(Profile profile) {
681        return profile.equals(Profile.HEADSET) || profile.equals(Profile.A2DP);
682    }
683
684    public void onCreateContextMenu(ContextMenu menu) {
685        // No context menu if it is busy (none of these items are applicable if busy)
686        if (mLocalManager.getBluetoothState() != BluetoothAdapter.STATE_ON || isBusy()) {
687            return;
688        }
689
690        int bondState = getBondState();
691        boolean isConnected = isConnected();
692        boolean hasConnectableProfiles = false;
693
694        for (Profile profile : mProfiles) {
695            if (isConnectableProfile(profile)) {
696                hasConnectableProfiles = true;
697                break;
698            }
699        }
700
701        menu.setHeaderTitle(getName());
702
703        if (bondState == BluetoothDevice.BOND_NONE) { // Not paired and not connected
704            menu.add(0, CONTEXT_ITEM_CONNECT, 0, R.string.bluetooth_device_context_pair_connect);
705        } else { // Paired
706            if (isConnected) { // Paired and connected
707                menu.add(0, CONTEXT_ITEM_DISCONNECT, 0,
708                        R.string.bluetooth_device_context_disconnect);
709                menu.add(0, CONTEXT_ITEM_UNPAIR, 0,
710                        R.string.bluetooth_device_context_disconnect_unpair);
711            } else { // Paired but not connected
712                if (hasConnectableProfiles) {
713                    menu.add(0, CONTEXT_ITEM_CONNECT, 0, R.string.bluetooth_device_context_connect);
714                }
715                menu.add(0, CONTEXT_ITEM_UNPAIR, 0, R.string.bluetooth_device_context_unpair);
716            }
717
718            // Show the connection options item
719            if (hasConnectableProfiles) {
720                menu.add(0, CONTEXT_ITEM_CONNECT_ADVANCED, 0,
721                        R.string.bluetooth_device_context_connect_advanced);
722            }
723        }
724    }
725
726    /**
727     * Called when a context menu item is clicked.
728     *
729     * @param item The item that was clicked.
730     */
731    public void onContextItemSelected(MenuItem item) {
732        switch (item.getItemId()) {
733            case CONTEXT_ITEM_DISCONNECT:
734                disconnect();
735                break;
736
737            case CONTEXT_ITEM_CONNECT:
738                connect();
739                break;
740
741            case CONTEXT_ITEM_UNPAIR:
742                unpair();
743                break;
744
745            case CONTEXT_ITEM_CONNECT_ADVANCED:
746                Intent intent = new Intent();
747                // Need an activity context to open this in our task
748                Context context = mLocalManager.getForegroundActivity();
749                if (context == null) {
750                    // Fallback on application context, and open in a new task
751                    context = mLocalManager.getContext();
752                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
753                }
754                intent.setClass(context, ConnectSpecificProfilesActivity.class);
755                intent.putExtra(ConnectSpecificProfilesActivity.EXTRA_DEVICE, mDevice);
756                context.startActivity(intent);
757                break;
758        }
759    }
760
761    public void registerCallback(Callback callback) {
762        synchronized (mCallbacks) {
763            mCallbacks.add(callback);
764        }
765    }
766
767    public void unregisterCallback(Callback callback) {
768        synchronized (mCallbacks) {
769            mCallbacks.remove(callback);
770        }
771    }
772
773    private void dispatchAttributesChanged() {
774        synchronized (mCallbacks) {
775            for (Callback callback : mCallbacks) {
776                callback.onDeviceAttributesChanged(this);
777            }
778        }
779    }
780
781    @Override
782    public String toString() {
783        return mDevice.toString();
784    }
785
786    @Override
787    public boolean equals(Object o) {
788        if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
789            throw new ClassCastException();
790        }
791
792        return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
793    }
794
795    @Override
796    public int hashCode() {
797        return mDevice.getAddress().hashCode();
798    }
799
800    public int compareTo(CachedBluetoothDevice another) {
801        int comparison;
802
803        // Connected above not connected
804        comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
805        if (comparison != 0) return comparison;
806
807        // Paired above not paired
808        comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
809            (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
810        if (comparison != 0) return comparison;
811
812        // Visible above not visible
813        comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
814        if (comparison != 0) return comparison;
815
816        // Stronger signal above weaker signal
817        comparison = another.mRssi - mRssi;
818        if (comparison != 0) return comparison;
819
820        // Fallback on name
821        return getName().compareTo(another.getName());
822    }
823
824    public interface Callback {
825        void onDeviceAttributesChanged(CachedBluetoothDevice cachedDevice);
826    }
827}
828