• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.settings.bluetooth;
18 
19 import android.app.AlertDialog;
20 import android.bluetooth.BluetoothClass;
21 import android.bluetooth.BluetoothDevice;
22 import android.content.Context;
23 import android.content.DialogInterface;
24 import android.content.Intent;
25 import android.content.res.Resources;
26 import android.text.TextUtils;
27 import android.util.Log;
28 import android.view.ContextMenu;
29 import android.view.Menu;
30 import android.view.MenuItem;
31 
32 import com.android.settings.R;
33 import com.android.settings.bluetooth.LocalBluetoothProfileManager.Profile;
34 
35 import java.text.DateFormat;
36 import java.util.ArrayList;
37 import java.util.Date;
38 import java.util.Iterator;
39 import java.util.LinkedList;
40 import java.util.List;
41 
42 /**
43  * LocalBluetoothDevice 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  */
48 public class LocalBluetoothDevice implements Comparable<LocalBluetoothDevice> {
49     private static final String TAG = "LocalBluetoothDevice";
50     private static final boolean D = LocalBluetoothManager.D;
51     private static final boolean V = LocalBluetoothManager.V;
52 
53     private static final int CONTEXT_ITEM_CONNECT = Menu.FIRST + 1;
54     private static final int CONTEXT_ITEM_DISCONNECT = Menu.FIRST + 2;
55     private static final int CONTEXT_ITEM_UNPAIR = Menu.FIRST + 3;
56     private static final int CONTEXT_ITEM_CONNECT_ADVANCED = Menu.FIRST + 4;
57 
58     private final String mAddress;
59     private String mName;
60     private short mRssi;
61     private int mBtClass = BluetoothClass.ERROR;
62 
63     private List<Profile> mProfiles = new ArrayList<Profile>();
64 
65     private boolean mVisible;
66 
67     private final LocalBluetoothManager mLocalManager;
68 
69     private List<Callback> mCallbacks = new ArrayList<Callback>();
70 
71     /**
72      * When we connect to multiple profiles, we only want to display a single
73      * error even if they all fail. This tracks that state.
74      */
75     private boolean mIsConnectingErrorPossible;
76 
77     // Max time to hold the work queue if we don't get or missed a response
78     // from the bt framework.
79     private static final long MAX_WAIT_TIME_FOR_FRAMEWORK = 25 * 1000;
80 
81     private enum BluetoothCommand {
82         CONNECT, DISCONNECT,
83     }
84 
85     class BluetoothJob {
86         final BluetoothCommand command; // CONNECT, DISCONNECT
87         final LocalBluetoothDevice device;
88         final Profile profile; // HEADSET, A2DP, etc
89         // 0 means this command was not been sent to the bt framework.
90         long timeSent;
91 
BluetoothJob(BluetoothCommand command, LocalBluetoothDevice device, Profile profile)92         public BluetoothJob(BluetoothCommand command,
93                 LocalBluetoothDevice device, Profile profile) {
94             this.command = command;
95             this.device = device;
96             this.profile = profile;
97             this.timeSent = 0;
98         }
99 
100         @Override
toString()101         public String toString() {
102             StringBuilder sb = new StringBuilder();
103             sb.append(command.name());
104             sb.append(" Address:").append(device.mAddress);
105             sb.append(" Profile:").append(profile.name());
106             sb.append(" TimeSent:");
107             if (timeSent == 0) {
108                 sb.append("not yet");
109             } else {
110                 sb.append(DateFormat.getTimeInstance().format(new Date(timeSent)));
111             }
112             return sb.toString();
113         }
114     }
115 
116     /**
117      * We want to serialize connect and disconnect calls. http://b/170538
118      * This are some headsets that may have L2CAP resource limitation. We want
119      * to limit the bt bandwidth usage.
120      *
121      * A queue to keep track of asynchronous calls to the bt framework.  The
122      * first item, if exist, should be in progress i.e. went to the bt framework
123      * already, waiting for a notification to come back. The second item and
124      * beyond have not been sent to the bt framework yet.
125      */
126     private static LinkedList<BluetoothJob> workQueue = new LinkedList<BluetoothJob>();
127 
queueCommand(BluetoothJob job)128     private void queueCommand(BluetoothJob job) {
129         if (D) {
130             Log.d(TAG, workQueue.toString());
131         }
132         synchronized (workQueue) {
133             boolean processNow = pruneQueue(job);
134 
135             // Add job to queue
136             if (D) {
137                 Log.d(TAG, "Adding: " + job.toString());
138             }
139             workQueue.add(job);
140 
141             // if there's nothing pending from before, send the command to bt
142             // framework immediately.
143             if (workQueue.size() == 1 || processNow) {
144                 // If the failed to process, just drop it from the queue.
145                 // There will be no callback to remove this from the queue.
146                 processCommands();
147             }
148         }
149     }
150 
pruneQueue(BluetoothJob job)151     private boolean pruneQueue(BluetoothJob job) {
152         boolean removedStaleItems = false;
153         long now = System.currentTimeMillis();
154         Iterator<BluetoothJob> it = workQueue.iterator();
155         while (it.hasNext()) {
156             BluetoothJob existingJob = it.next();
157 
158             // Remove any pending CONNECTS when we receive a DISCONNECT
159             if (job != null && job.command == BluetoothCommand.DISCONNECT) {
160                 if (existingJob.timeSent == 0
161                         && existingJob.command == BluetoothCommand.CONNECT
162                         && existingJob.device.mAddress.equals(job.device.mAddress)
163                         && existingJob.profile == job.profile) {
164                     if (D) {
165                         Log.d(TAG, "Removed because of a pending disconnect. " + existingJob);
166                     }
167                     it.remove();
168                     continue;
169                 }
170             }
171 
172             // Defensive Code: Remove any job that older than a preset time.
173             // We never got a call back. It is better to have overlapping
174             // calls than to get stuck.
175             if (existingJob.timeSent != 0
176                     && (now - existingJob.timeSent) >= MAX_WAIT_TIME_FOR_FRAMEWORK) {
177                 Log.w(TAG, "Timeout. Removing Job:" + existingJob.toString());
178                 it.remove();
179                 removedStaleItems = true;
180                 continue;
181             }
182         }
183         return removedStaleItems;
184     }
185 
processCommand(BluetoothJob job)186     private boolean processCommand(BluetoothJob job) {
187         boolean successful = false;
188         if (job.timeSent == 0) {
189             job.timeSent = System.currentTimeMillis();
190             switch (job.command) {
191             case CONNECT:
192                 successful = connectInt(job.device, job.profile);
193                 break;
194             case DISCONNECT:
195                 successful = disconnectInt(job.device, job.profile);
196                 break;
197             }
198 
199             if (successful) {
200                 if (D) {
201                     Log.d(TAG, "Command sent successfully:" + job.toString());
202                 }
203             } else if (V) {
204                 Log.v(TAG, "Framework rejected command immediately:" + job.toString());
205             }
206         } else if (D) {
207             Log.d(TAG, "Job already has a sent time. Skip. " + job.toString());
208         }
209 
210         return successful;
211     }
212 
onProfileStateChanged(Profile profile, int newProfileState)213     public void onProfileStateChanged(Profile profile, int newProfileState) {
214         if (D) {
215             Log.d(TAG, "onProfileStateChanged:" + workQueue.toString());
216         }
217 
218         int newState = LocalBluetoothProfileManager.getProfileManager(mLocalManager,
219                 profile).convertState(newProfileState);
220 
221         if (newState == SettingsBtStatus.CONNECTION_STATUS_CONNECTED) {
222             if (!mProfiles.contains(profile)) {
223                 mProfiles.add(profile);
224             }
225         }
226 
227         /* Ignore the transient states e.g. connecting, disconnecting */
228         if (newState == SettingsBtStatus.CONNECTION_STATUS_CONNECTED ||
229                 newState == SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED) {
230             BluetoothJob job = workQueue.peek();
231             if (job == null) {
232                 return;
233             } else if (job.device.mAddress != mAddress) {
234                 // This can happen in 2 cases: 1) BT device initiated pairing and
235                 // 2) disconnects of one headset that's triggered by connects of
236                 // another.
237                 if (D) {
238                     Log.d(TAG, "mAddresses:" + mAddress + " != head:" + job.toString());
239                 }
240 
241                 // Check to see if we need to remove the stale items from the queue
242                 if (!pruneQueue(null)) {
243                     // nothing in the queue was modify. Just ignore the notification and return.
244                     return;
245                 }
246             } else {
247                 // Remove the first item and process the next one
248                 workQueue.poll();
249             }
250 
251             processCommands();
252         }
253     }
254 
255     /*
256      * This method is called in 2 places:
257      * 1) queryCommand() - when someone or something want to connect or
258      *    disconnect
259      * 2) onProfileStateChanged() - when the framework sends an intent
260      *    notification when it finishes processing a command
261      */
processCommands()262     private void processCommands() {
263         if (D) {
264             Log.d(TAG, "processCommands:" + workQueue.toString());
265         }
266         Iterator<BluetoothJob> it = workQueue.iterator();
267         while (it.hasNext()) {
268             BluetoothJob job = it.next();
269             if (processCommand(job)) {
270                 // Sent to bt framework. Done for now. Will remove this job
271                 // from queue when we get an event
272                 return;
273             } else {
274                 /*
275                  * If the command failed immediately, there will be no event
276                  * callbacks. So delete the job immediately and move on to the
277                  * next one
278                  */
279                 it.remove();
280             }
281         }
282     }
283 
LocalBluetoothDevice(Context context, String address)284     LocalBluetoothDevice(Context context, String address) {
285         mLocalManager = LocalBluetoothManager.getInstance(context);
286         if (mLocalManager == null) {
287             throw new IllegalStateException(
288                     "Cannot use LocalBluetoothDevice without Bluetooth hardware");
289         }
290 
291         mAddress = address;
292 
293         fillData();
294     }
295 
onClicked()296     public void onClicked() {
297         int bondState = getBondState();
298 
299         if (isConnected()) {
300             askDisconnect();
301         } else if (bondState == BluetoothDevice.BOND_BONDED) {
302             connect();
303         } else if (bondState == BluetoothDevice.BOND_NOT_BONDED) {
304             pair();
305         }
306     }
307 
disconnect()308     public void disconnect() {
309         for (Profile profile : mProfiles) {
310             disconnect(profile);
311         }
312     }
313 
disconnect(Profile profile)314     public void disconnect(Profile profile) {
315         queueCommand(new BluetoothJob(BluetoothCommand.DISCONNECT, this, profile));
316     }
317 
disconnectInt(LocalBluetoothDevice device, Profile profile)318     private boolean disconnectInt(LocalBluetoothDevice device, Profile profile) {
319         LocalBluetoothProfileManager profileManager =
320                 LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
321         int status = profileManager.getConnectionStatus(device.mAddress);
322         if (SettingsBtStatus.isConnectionStatusConnected(status)) {
323             if (profileManager.disconnect(device.mAddress) == BluetoothDevice.RESULT_SUCCESS) {
324                 return true;
325             }
326         }
327         return false;
328     }
329 
askDisconnect()330     public void askDisconnect() {
331         Context context = mLocalManager.getForegroundActivity();
332         if (context == null) {
333             // Cannot ask, since we need an activity context
334             disconnect();
335             return;
336         }
337 
338         Resources res = context.getResources();
339 
340         String name = getName();
341         if (TextUtils.isEmpty(name)) {
342             name = res.getString(R.string.bluetooth_device);
343         }
344         String message = res.getString(R.string.bluetooth_disconnect_blank, name);
345 
346         DialogInterface.OnClickListener disconnectListener = new DialogInterface.OnClickListener() {
347             public void onClick(DialogInterface dialog, int which) {
348                 disconnect();
349             }
350         };
351 
352         AlertDialog ad = new AlertDialog.Builder(context)
353                 .setTitle(getName())
354                 .setMessage(message)
355                 .setPositiveButton(android.R.string.ok, disconnectListener)
356                 .setNegativeButton(android.R.string.cancel, null)
357                 .show();
358     }
359 
connect()360     public void connect() {
361         if (!ensurePaired()) return;
362 
363         // Reset the only-show-one-error-dialog tracking variable
364         mIsConnectingErrorPossible = true;
365 
366         Context context = mLocalManager.getContext();
367         boolean hasAtLeastOnePreferredProfile = false;
368         for (Profile profile : mProfiles) {
369             LocalBluetoothProfileManager profileManager =
370                     LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
371             if (profileManager.isPreferred(mAddress)) {
372                 hasAtLeastOnePreferredProfile = true;
373                 queueCommand(new BluetoothJob(BluetoothCommand.CONNECT, this, profile));
374             }
375         }
376 
377         if (!hasAtLeastOnePreferredProfile) {
378             connectAndPreferAllProfiles();
379         }
380     }
381 
connectAndPreferAllProfiles()382     private void connectAndPreferAllProfiles() {
383         if (!ensurePaired()) return;
384 
385         // Reset the only-show-one-error-dialog tracking variable
386         mIsConnectingErrorPossible = true;
387 
388         Context context = mLocalManager.getContext();
389         for (Profile profile : mProfiles) {
390             LocalBluetoothProfileManager profileManager =
391                     LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
392             profileManager.setPreferred(mAddress, true);
393             queueCommand(new BluetoothJob(BluetoothCommand.CONNECT, this, profile));
394         }
395     }
396 
connect(Profile profile)397     public void connect(Profile profile) {
398         // Reset the only-show-one-error-dialog tracking variable
399         mIsConnectingErrorPossible = true;
400         queueCommand(new BluetoothJob(BluetoothCommand.CONNECT, this, profile));
401     }
402 
connectInt(LocalBluetoothDevice device, Profile profile)403     private boolean connectInt(LocalBluetoothDevice device, Profile profile) {
404         if (!device.ensurePaired()) return false;
405 
406         LocalBluetoothProfileManager profileManager =
407                 LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
408         int status = profileManager.getConnectionStatus(device.mAddress);
409         if (!SettingsBtStatus.isConnectionStatusConnected(status)) {
410             if (profileManager.connect(device.mAddress) == BluetoothDevice.RESULT_SUCCESS) {
411                 return true;
412             }
413             Log.i(TAG, "Failed to connect " + profile.toString() + " to " + device.mName);
414         }
415         Log.i(TAG, "Not connected");
416         return false;
417     }
418 
showConnectingError()419     public void showConnectingError() {
420         if (!mIsConnectingErrorPossible) return;
421         mIsConnectingErrorPossible = false;
422 
423         mLocalManager.showError(mAddress, R.string.bluetooth_error_title,
424                 R.string.bluetooth_connecting_error_message);
425     }
426 
ensurePaired()427     private boolean ensurePaired() {
428         if (getBondState() == BluetoothDevice.BOND_NOT_BONDED) {
429             pair();
430             return false;
431         } else {
432             return true;
433         }
434     }
435 
pair()436     public void pair() {
437         BluetoothDevice manager = mLocalManager.getBluetoothManager();
438 
439         // Pairing is unreliable while scanning, so cancel discovery
440         if (manager.isDiscovering()) {
441             manager.cancelDiscovery();
442         }
443 
444         if (!mLocalManager.getBluetoothManager().createBond(mAddress)) {
445             mLocalManager.showError(mAddress, R.string.bluetooth_error_title,
446                     R.string.bluetooth_pairing_error_message);
447         }
448     }
449 
unpair()450     public void unpair() {
451         synchronized (workQueue) {
452             // Remove any pending commands for this device
453             boolean processNow = false;
454             Iterator<BluetoothJob> it = workQueue.iterator();
455             while (it.hasNext()) {
456                 BluetoothJob job = it.next();
457                 if (job.device.mAddress.equals(this.mAddress)) {
458                     it.remove();
459                     if (job.timeSent != 0) {
460                         processNow = true;
461                     }
462                 }
463             }
464             if (processNow) {
465                 processCommands();
466             }
467         }
468 
469         BluetoothDevice manager = mLocalManager.getBluetoothManager();
470 
471         switch (getBondState()) {
472         case BluetoothDevice.BOND_BONDED:
473             manager.removeBond(mAddress);
474             break;
475 
476         case BluetoothDevice.BOND_BONDING:
477             manager.cancelBondProcess(mAddress);
478             break;
479         }
480     }
481 
fillData()482     private void fillData() {
483         BluetoothDevice manager = mLocalManager.getBluetoothManager();
484 
485         fetchName();
486         fetchBtClass();
487 
488         mVisible = false;
489 
490         dispatchAttributesChanged();
491     }
492 
getAddress()493     public String getAddress() {
494         return mAddress;
495     }
496 
getName()497     public String getName() {
498         return mName;
499     }
500 
refreshName()501     public void refreshName() {
502         fetchName();
503         dispatchAttributesChanged();
504     }
505 
fetchName()506     private void fetchName() {
507         mName = mLocalManager.getBluetoothManager().getRemoteName(mAddress);
508 
509         if (TextUtils.isEmpty(mName)) {
510             mName = mAddress;
511         }
512     }
513 
refresh()514     public void refresh() {
515         dispatchAttributesChanged();
516     }
517 
isVisible()518     public boolean isVisible() {
519         return mVisible;
520     }
521 
setVisible(boolean visible)522     void setVisible(boolean visible) {
523         if (mVisible != visible) {
524             mVisible = visible;
525             dispatchAttributesChanged();
526         }
527     }
528 
getBondState()529     public int getBondState() {
530         return mLocalManager.getBluetoothManager().getBondState(mAddress);
531     }
532 
setRssi(short rssi)533     void setRssi(short rssi) {
534         if (mRssi != rssi) {
535             mRssi = rssi;
536             dispatchAttributesChanged();
537         }
538     }
539 
540     /**
541      * Checks whether we are connected to this device (any profile counts).
542      *
543      * @return Whether it is connected.
544      */
isConnected()545     public boolean isConnected() {
546         for (Profile profile : mProfiles) {
547             int status = LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile)
548                     .getConnectionStatus(mAddress);
549             if (SettingsBtStatus.isConnectionStatusConnected(status)) {
550                 return true;
551             }
552         }
553 
554         return false;
555     }
556 
isBusy()557     public boolean isBusy() {
558         for (Profile profile : mProfiles) {
559             int status = LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile)
560                     .getConnectionStatus(mAddress);
561             if (SettingsBtStatus.isConnectionStatusBusy(status)) {
562                 return true;
563             }
564         }
565 
566         if (getBondState() == BluetoothDevice.BOND_BONDING) {
567             return true;
568         }
569 
570         return false;
571     }
572 
getBtClassDrawable()573     public int getBtClassDrawable() {
574 
575         // First try looking at profiles
576         if (mProfiles.contains(Profile.A2DP)) {
577             return R.drawable.ic_bt_headphones_a2dp;
578         } else if (mProfiles.contains(Profile.HEADSET)) {
579             return R.drawable.ic_bt_headset_hfp;
580         }
581 
582         // Fallback on class
583         switch (BluetoothClass.Device.Major.getDeviceMajor(mBtClass)) {
584         case BluetoothClass.Device.Major.COMPUTER:
585             return R.drawable.ic_bt_laptop;
586 
587         case BluetoothClass.Device.Major.PHONE:
588             return R.drawable.ic_bt_cellphone;
589 
590         default:
591             return 0;
592         }
593     }
594 
595     /**
596      * Fetches a new value for the cached BT class.
597      */
fetchBtClass()598     private void fetchBtClass() {
599         mBtClass = mLocalManager.getBluetoothManager().getRemoteClass(mAddress);
600         if (mBtClass != BluetoothClass.ERROR) {
601             LocalBluetoothProfileManager.fill(mBtClass, mProfiles);
602         }
603     }
604 
605     /**
606      * Refreshes the UI for the BT class, including fetching the latest value
607      * for the class.
608      */
refreshBtClass()609     public void refreshBtClass() {
610         fetchBtClass();
611         dispatchAttributesChanged();
612     }
613 
getSummary()614     public int getSummary() {
615         // TODO: clean up
616         int oneOffSummary = getOneOffSummary();
617         if (oneOffSummary != 0) {
618             return oneOffSummary;
619         }
620 
621         for (Profile profile : mProfiles) {
622             LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
623                     .getProfileManager(mLocalManager, profile);
624             int connectionStatus = profileManager.getConnectionStatus(mAddress);
625 
626             if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus) ||
627                     connectionStatus == SettingsBtStatus.CONNECTION_STATUS_CONNECTING ||
628                     connectionStatus == SettingsBtStatus.CONNECTION_STATUS_DISCONNECTING) {
629                 return SettingsBtStatus.getConnectionStatusSummary(connectionStatus);
630             }
631         }
632 
633         return SettingsBtStatus.getPairingStatusSummary(getBondState());
634     }
635 
636     /**
637      * We have special summaries when particular profiles are connected. This
638      * checks for those states and returns an applicable summary.
639      *
640      * @return A one-off summary that is applicable for the current state, or 0.
641      */
getOneOffSummary()642     private int getOneOffSummary() {
643         boolean isA2dpConnected = false, isHeadsetConnected = false, isConnecting = false;
644 
645         if (mProfiles.contains(Profile.A2DP)) {
646             LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
647                     .getProfileManager(mLocalManager, Profile.A2DP);
648             isConnecting = profileManager.getConnectionStatus(mAddress) ==
649                     SettingsBtStatus.CONNECTION_STATUS_CONNECTING;
650             isA2dpConnected = profileManager.isConnected(mAddress);
651         }
652 
653         if (mProfiles.contains(Profile.HEADSET)) {
654             LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
655                     .getProfileManager(mLocalManager, Profile.HEADSET);
656             isConnecting |= profileManager.getConnectionStatus(mAddress) ==
657                     SettingsBtStatus.CONNECTION_STATUS_CONNECTING;
658             isHeadsetConnected = profileManager.isConnected(mAddress);
659         }
660 
661         if (isConnecting) {
662             // If any of these important profiles is connecting, prefer that
663             return SettingsBtStatus.getConnectionStatusSummary(
664                     SettingsBtStatus.CONNECTION_STATUS_CONNECTING);
665         } else if (isA2dpConnected && isHeadsetConnected) {
666             return R.string.bluetooth_summary_connected_to_a2dp_headset;
667         } else if (isA2dpConnected) {
668             return R.string.bluetooth_summary_connected_to_a2dp;
669         } else if (isHeadsetConnected) {
670             return R.string.bluetooth_summary_connected_to_headset;
671         } else {
672             return 0;
673         }
674     }
675 
getProfiles()676     public List<Profile> getProfiles() {
677         return new ArrayList<Profile>(mProfiles);
678     }
679 
onCreateContextMenu(ContextMenu menu)680     public void onCreateContextMenu(ContextMenu menu) {
681         // No context menu if it is busy (none of these items are applicable if busy)
682         if (isBusy()) return;
683 
684         int bondState = getBondState();
685         boolean isConnected = isConnected();
686         boolean hasProfiles = mProfiles.size() > 0;
687 
688         menu.setHeaderTitle(getName());
689 
690         if (isConnected) {
691             menu.add(0, CONTEXT_ITEM_DISCONNECT, 0, R.string.bluetooth_device_context_disconnect);
692         } else if (hasProfiles) {
693             // For connection action, show either "Connect" or "Pair & connect"
694             int connectString = (bondState == BluetoothDevice.BOND_NOT_BONDED)
695                     ? R.string.bluetooth_device_context_pair_connect
696                     : R.string.bluetooth_device_context_connect;
697             menu.add(0, CONTEXT_ITEM_CONNECT, 0, connectString);
698         }
699 
700         if (bondState == BluetoothDevice.BOND_BONDED) {
701             // For unpair action, show either "Unpair" or "Disconnect & unpair"
702             int unpairString = isConnected
703                     ? R.string.bluetooth_device_context_disconnect_unpair
704                     : R.string.bluetooth_device_context_unpair;
705             menu.add(0, CONTEXT_ITEM_UNPAIR, 0, unpairString);
706 
707             // Show the connection options item
708             menu.add(0, CONTEXT_ITEM_CONNECT_ADVANCED, 0,
709                     R.string.bluetooth_device_context_connect_advanced);
710         }
711     }
712 
713     /**
714      * Called when a context menu item is clicked.
715      *
716      * @param item The item that was clicked.
717      */
onContextItemSelected(MenuItem item)718     public void onContextItemSelected(MenuItem item) {
719         switch (item.getItemId()) {
720             case CONTEXT_ITEM_DISCONNECT:
721                 disconnect();
722                 break;
723 
724             case CONTEXT_ITEM_CONNECT:
725                 connect();
726                 break;
727 
728             case CONTEXT_ITEM_UNPAIR:
729                 mLocalManager.getBluetoothManager().disconnectRemoteDeviceAcl(mAddress);
730                 unpair();
731                 break;
732 
733             case CONTEXT_ITEM_CONNECT_ADVANCED:
734                 Intent intent = new Intent();
735                 // Need an activity context to open this in our task
736                 Context context = mLocalManager.getForegroundActivity();
737                 if (context == null) {
738                     // Fallback on application context, and open in a new task
739                     context = mLocalManager.getContext();
740                     intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
741                 }
742                 intent.setClass(context, ConnectSpecificProfilesActivity.class);
743                 intent.putExtra(ConnectSpecificProfilesActivity.EXTRA_ADDRESS, mAddress);
744                 context.startActivity(intent);
745                 break;
746         }
747     }
748 
registerCallback(Callback callback)749     public void registerCallback(Callback callback) {
750         synchronized (mCallbacks) {
751             mCallbacks.add(callback);
752         }
753     }
754 
unregisterCallback(Callback callback)755     public void unregisterCallback(Callback callback) {
756         synchronized (mCallbacks) {
757             mCallbacks.remove(callback);
758         }
759     }
760 
dispatchAttributesChanged()761     private void dispatchAttributesChanged() {
762         synchronized (mCallbacks) {
763             for (Callback callback : mCallbacks) {
764                 callback.onDeviceAttributesChanged(this);
765             }
766         }
767     }
768 
769     @Override
toString()770     public String toString() {
771         return mAddress;
772     }
773 
774     @Override
equals(Object o)775     public boolean equals(Object o) {
776         if ((o == null) || !(o instanceof LocalBluetoothDevice)) {
777             throw new ClassCastException();
778         }
779 
780         return mAddress.equals(((LocalBluetoothDevice) o).mAddress);
781     }
782 
783     @Override
hashCode()784     public int hashCode() {
785         return mAddress.hashCode();
786     }
787 
compareTo(LocalBluetoothDevice another)788     public int compareTo(LocalBluetoothDevice another) {
789         int comparison;
790 
791         // Connected above not connected
792         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         // Visible above not visible
801         comparison = (another.mVisible ? 1 : 0) - (mVisible ? 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 getName().compareTo(another.getName());
810     }
811 
812     public interface Callback {
onDeviceAttributesChanged(LocalBluetoothDevice device)813         void onDeviceAttributesChanged(LocalBluetoothDevice device);
814     }
815 }
816