blob: 64b4452b9554204752391a8080a415454cc8be0d [file] [log] [blame]
Jason Monk7ce96b92015-02-02 11:27:58 -05001/*
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.BluetoothProfile;
22import android.bluetooth.BluetoothUuid;
23import android.content.Context;
24import android.content.SharedPreferences;
25import android.os.ParcelUuid;
26import android.os.SystemClock;
27import android.text.TextUtils;
28import android.util.Log;
29import android.bluetooth.BluetoothAdapter;
30
Jason Monkbe3c5db2015-02-04 13:00:55 -050031import com.android.settingslib.R;
32
Jason Monk7ce96b92015-02-02 11:27:58 -050033import java.util.ArrayList;
34import java.util.Collection;
35import java.util.Collections;
36import java.util.HashMap;
37import java.util.List;
38
39/**
40 * CachedBluetoothDevice represents a remote Bluetooth device. It contains
41 * attributes of the device (such as the address, name, RSSI, etc.) and
42 * functionality that can be performed on the device (connect, pair, disconnect,
43 * etc.).
44 */
45public final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
46 private static final String TAG = "CachedBluetoothDevice";
47 private static final boolean DEBUG = Utils.V;
48
49 private final Context mContext;
50 private final LocalBluetoothAdapter mLocalAdapter;
51 private final LocalBluetoothProfileManager mProfileManager;
52 private final BluetoothDevice mDevice;
53 private String mName;
54 private short mRssi;
55 private BluetoothClass mBtClass;
56 private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
57
58 private final List<LocalBluetoothProfile> mProfiles =
59 new ArrayList<LocalBluetoothProfile>();
60
61 // List of profiles that were previously in mProfiles, but have been removed
62 private final List<LocalBluetoothProfile> mRemovedProfiles =
63 new ArrayList<LocalBluetoothProfile>();
64
65 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
66 private boolean mLocalNapRoleConnected;
67
68 private boolean mVisible;
69
70 private int mPhonebookPermissionChoice;
71
72 private int mMessagePermissionChoice;
73
74 private int mMessageRejectionCount;
75
76 private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
77
78 // Following constants indicate the user's choices of Phone book/message access settings
79 // User hasn't made any choice or settings app has wiped out the memory
80 public final static int ACCESS_UNKNOWN = 0;
81 // User has accepted the connection and let Settings app remember the decision
82 public final static int ACCESS_ALLOWED = 1;
83 // User has rejected the connection and let Settings app remember the decision
84 public final static int ACCESS_REJECTED = 2;
85
86 // How many times user should reject the connection to make the choice persist.
87 private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
88
89 private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
90
91 /**
92 * When we connect to multiple profiles, we only want to display a single
93 * error even if they all fail. This tracks that state.
94 */
95 private boolean mIsConnectingErrorPossible;
96
97 /**
98 * Last time a bt profile auto-connect was attempted.
99 * If an ACTION_UUID intent comes in within
100 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
101 * again with the new UUIDs
102 */
103 private long mConnectAttempted;
104
105 // See mConnectAttempted
106 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
Etan Cohen50d47612015-03-31 12:45:23 -0700107 private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
Jason Monk7ce96b92015-02-02 11:27:58 -0500108
109 /** Auto-connect after pairing only if locally initiated. */
110 private boolean mConnectAfterPairing;
111
112 /**
113 * Describes the current device and profile for logging.
114 *
115 * @param profile Profile to describe
116 * @return Description of the device and profile
117 */
118 private String describe(LocalBluetoothProfile profile) {
119 StringBuilder sb = new StringBuilder();
120 sb.append("Address:").append(mDevice);
121 if (profile != null) {
122 sb.append(" Profile:").append(profile);
123 }
124
125 return sb.toString();
126 }
127
128 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
129 if (Utils.D) {
130 Log.d(TAG, "onProfileStateChanged: profile " + profile +
131 " newProfileState " + newProfileState);
132 }
133 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
134 {
135 if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
136 return;
137 }
138 mProfileConnectionState.put(profile, newProfileState);
139 if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
140 if (profile instanceof MapProfile) {
141 profile.setPreferred(mDevice, true);
142 } else if (!mProfiles.contains(profile)) {
143 mRemovedProfiles.remove(profile);
144 mProfiles.add(profile);
145 if (profile instanceof PanProfile &&
146 ((PanProfile) profile).isLocalRoleNap(mDevice)) {
147 // Device doesn't support NAP, so remove PanProfile on disconnect
148 mLocalNapRoleConnected = true;
149 }
150 }
151 } else if (profile instanceof MapProfile &&
152 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
153 profile.setPreferred(mDevice, false);
154 } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
155 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
156 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
157 Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
158 mProfiles.remove(profile);
159 mRemovedProfiles.add(profile);
160 mLocalNapRoleConnected = false;
161 }
162 }
163
164 CachedBluetoothDevice(Context context,
165 LocalBluetoothAdapter adapter,
166 LocalBluetoothProfileManager profileManager,
167 BluetoothDevice device) {
168 mContext = context;
169 mLocalAdapter = adapter;
170 mProfileManager = profileManager;
171 mDevice = device;
172 mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
173 fillData();
174 }
175
176 public void disconnect() {
177 for (LocalBluetoothProfile profile : mProfiles) {
178 disconnect(profile);
179 }
180 // Disconnect PBAP server in case its connected
181 // This is to ensure all the profiles are disconnected as some CK/Hs do not
182 // disconnect PBAP connection when HF connection is brought down
183 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
184 if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
185 {
186 PbapProfile.disconnect(mDevice);
187 }
188 }
189
190 public void disconnect(LocalBluetoothProfile profile) {
191 if (profile.disconnect(mDevice)) {
192 if (Utils.D) {
193 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
194 }
195 }
196 }
197
198 public void connect(boolean connectAllProfiles) {
199 if (!ensurePaired()) {
200 return;
201 }
202
203 mConnectAttempted = SystemClock.elapsedRealtime();
204 connectWithoutResettingTimer(connectAllProfiles);
205 }
206
207 void onBondingDockConnect() {
208 // Attempt to connect if UUIDs are available. Otherwise,
209 // we will connect when the ACTION_UUID intent arrives.
210 connect(false);
211 }
212
213 private void connectWithoutResettingTimer(boolean connectAllProfiles) {
214 // Try to initialize the profiles if they were not.
215 if (mProfiles.isEmpty()) {
216 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
217 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
218 // from bluetooth stack but ACTION.uuid is not sent yet.
219 // Eventually ACTION.uuid will be received which shall trigger the connection of the
220 // various profiles
221 // If UUIDs are not available yet, connect will be happen
222 // upon arrival of the ACTION_UUID intent.
223 Log.d(TAG, "No profiles. Maybe we will connect later");
224 return;
225 }
226
227 // Reset the only-show-one-error-dialog tracking variable
228 mIsConnectingErrorPossible = true;
229
230 int preferredProfiles = 0;
231 for (LocalBluetoothProfile profile : mProfiles) {
232 if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
233 if (profile.isPreferred(mDevice)) {
234 ++preferredProfiles;
235 connectInt(profile);
236 }
237 }
238 }
239 if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
240
241 if (preferredProfiles == 0) {
242 connectAutoConnectableProfiles();
243 }
244 }
245
246 private void connectAutoConnectableProfiles() {
247 if (!ensurePaired()) {
248 return;
249 }
250 // Reset the only-show-one-error-dialog tracking variable
251 mIsConnectingErrorPossible = true;
252
253 for (LocalBluetoothProfile profile : mProfiles) {
254 if (profile.isAutoConnectable()) {
255 profile.setPreferred(mDevice, true);
256 connectInt(profile);
257 }
258 }
259 }
260
261 /**
262 * Connect this device to the specified profile.
263 *
264 * @param profile the profile to use with the remote device
265 */
266 public void connectProfile(LocalBluetoothProfile profile) {
267 mConnectAttempted = SystemClock.elapsedRealtime();
268 // Reset the only-show-one-error-dialog tracking variable
269 mIsConnectingErrorPossible = true;
270 connectInt(profile);
271 // Refresh the UI based on profile.connect() call
272 refresh();
273 }
274
275 synchronized void connectInt(LocalBluetoothProfile profile) {
276 if (!ensurePaired()) {
277 return;
278 }
279 if (profile.connect(mDevice)) {
280 if (Utils.D) {
281 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
282 }
283 return;
284 }
285 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
286 }
287
288 private boolean ensurePaired() {
289 if (getBondState() == BluetoothDevice.BOND_NONE) {
290 startPairing();
291 return false;
292 } else {
293 return true;
294 }
295 }
296
297 public boolean startPairing() {
298 // Pairing is unreliable while scanning, so cancel discovery
299 if (mLocalAdapter.isDiscovering()) {
300 mLocalAdapter.cancelDiscovery();
301 }
302
303 if (!mDevice.createBond()) {
304 return false;
305 }
306
307 mConnectAfterPairing = true; // auto-connect after pairing
308 return true;
309 }
310
311 /**
312 * Return true if user initiated pairing on this device. The message text is
313 * slightly different for local vs. remote initiated pairing dialogs.
314 */
315 boolean isUserInitiatedPairing() {
316 return mConnectAfterPairing;
317 }
318
319 public void unpair() {
320 int state = getBondState();
321
322 if (state == BluetoothDevice.BOND_BONDING) {
323 mDevice.cancelBondProcess();
324 }
325
326 if (state != BluetoothDevice.BOND_NONE) {
327 final BluetoothDevice dev = mDevice;
328 if (dev != null) {
329 final boolean successful = dev.removeBond();
330 if (successful) {
331 if (Utils.D) {
332 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
333 }
334 } else if (Utils.V) {
335 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
336 describe(null));
337 }
338 }
339 }
340 }
341
342 public int getProfileConnectionState(LocalBluetoothProfile profile) {
343 if (mProfileConnectionState == null ||
344 mProfileConnectionState.get(profile) == null) {
345 // If cache is empty make the binder call to get the state
346 int state = profile.getConnectionStatus(mDevice);
347 mProfileConnectionState.put(profile, state);
348 }
349 return mProfileConnectionState.get(profile);
350 }
351
352 public void clearProfileConnectionState ()
353 {
354 if (Utils.D) {
355 Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
356 }
357 for (LocalBluetoothProfile profile :getProfiles()) {
358 mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
359 }
360 }
361
362 // TODO: do any of these need to run async on a background thread?
363 private void fillData() {
364 fetchName();
365 fetchBtClass();
366 updateProfiles();
367 migratePhonebookPermissionChoice();
368 migrateMessagePermissionChoice();
369 fetchMessageRejectionCount();
370
371 mVisible = false;
372 dispatchAttributesChanged();
373 }
374
375 public BluetoothDevice getDevice() {
376 return mDevice;
377 }
378
379 public String getName() {
380 return mName;
381 }
382
383 /**
384 * Populate name from BluetoothDevice.ACTION_FOUND intent
385 */
386 void setNewName(String name) {
387 if (mName == null) {
388 mName = name;
389 if (mName == null || TextUtils.isEmpty(mName)) {
390 mName = mDevice.getAddress();
391 }
392 dispatchAttributesChanged();
393 }
394 }
395
396 /**
397 * user changes the device name
398 */
399 public void setName(String name) {
400 if (!mName.equals(name)) {
401 mName = name;
402 mDevice.setAlias(name);
403 dispatchAttributesChanged();
404 }
405 }
406
407 void refreshName() {
408 fetchName();
409 dispatchAttributesChanged();
410 }
411
412 private void fetchName() {
413 mName = mDevice.getAliasName();
414
415 if (TextUtils.isEmpty(mName)) {
416 mName = mDevice.getAddress();
417 if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
418 }
419 }
420
421 void refresh() {
422 dispatchAttributesChanged();
423 }
424
425 public boolean isVisible() {
426 return mVisible;
427 }
428
429 public void setVisible(boolean visible) {
430 if (mVisible != visible) {
431 mVisible = visible;
432 dispatchAttributesChanged();
433 }
434 }
435
436 public int getBondState() {
437 return mDevice.getBondState();
438 }
439
440 void setRssi(short rssi) {
441 if (mRssi != rssi) {
442 mRssi = rssi;
443 dispatchAttributesChanged();
444 }
445 }
446
447 /**
448 * Checks whether we are connected to this device (any profile counts).
449 *
450 * @return Whether it is connected.
451 */
452 public boolean isConnected() {
453 for (LocalBluetoothProfile profile : mProfiles) {
454 int status = getProfileConnectionState(profile);
455 if (status == BluetoothProfile.STATE_CONNECTED) {
456 return true;
457 }
458 }
459
460 return false;
461 }
462
463 public boolean isConnectedProfile(LocalBluetoothProfile profile) {
464 int status = getProfileConnectionState(profile);
465 return status == BluetoothProfile.STATE_CONNECTED;
466
467 }
468
469 public boolean isBusy() {
470 for (LocalBluetoothProfile profile : mProfiles) {
471 int status = getProfileConnectionState(profile);
472 if (status == BluetoothProfile.STATE_CONNECTING
473 || status == BluetoothProfile.STATE_DISCONNECTING) {
474 return true;
475 }
476 }
477 return getBondState() == BluetoothDevice.BOND_BONDING;
478 }
479
480 /**
481 * Fetches a new value for the cached BT class.
482 */
483 private void fetchBtClass() {
484 mBtClass = mDevice.getBluetoothClass();
485 }
486
487 private boolean updateProfiles() {
488 ParcelUuid[] uuids = mDevice.getUuids();
489 if (uuids == null) return false;
490
491 ParcelUuid[] localUuids = mLocalAdapter.getUuids();
492 if (localUuids == null) return false;
493
494 /**
495 * Now we know if the device supports PBAP, update permissions...
496 */
497 processPhonebookAccess();
498
499 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
500 mLocalNapRoleConnected, mDevice);
501
502 if (DEBUG) {
503 Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
504 BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
505
506 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
507 Log.v(TAG, "UUID:");
508 for (ParcelUuid uuid : uuids) {
509 Log.v(TAG, " " + uuid);
510 }
511 }
512 return true;
513 }
514
515 /**
516 * Refreshes the UI for the BT class, including fetching the latest value
517 * for the class.
518 */
519 void refreshBtClass() {
520 fetchBtClass();
521 dispatchAttributesChanged();
522 }
523
524 /**
525 * Refreshes the UI when framework alerts us of a UUID change.
526 */
527 void onUuidChanged() {
528 updateProfiles();
Etan Cohen50d47612015-03-31 12:45:23 -0700529 ParcelUuid[] uuids = mDevice.getUuids();
530 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
Jason Monk7ce96b92015-02-02 11:27:58 -0500531
532 if (DEBUG) {
Etan Cohen50d47612015-03-31 12:45:23 -0700533 Log.d(TAG, "onUuidChanged: Time since last connect"
Jason Monk7ce96b92015-02-02 11:27:58 -0500534 + (SystemClock.elapsedRealtime() - mConnectAttempted));
535 }
536
537 /*
538 * If a connect was attempted earlier without any UUID, we will do the
539 * connect now.
540 */
Etan Cohen50d47612015-03-31 12:45:23 -0700541 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
542 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
543 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500544 if (!mProfiles.isEmpty()
Etan Cohen50d47612015-03-31 12:45:23 -0700545 && (mConnectAttempted + timeout) > SystemClock.elapsedRealtime()) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500546 connectWithoutResettingTimer(false);
547 }
548 dispatchAttributesChanged();
549 }
550
551 void onBondingStateChanged(int bondState) {
552 if (bondState == BluetoothDevice.BOND_NONE) {
553 mProfiles.clear();
554 mConnectAfterPairing = false; // cancel auto-connect
555 setPhonebookPermissionChoice(ACCESS_UNKNOWN);
556 setMessagePermissionChoice(ACCESS_UNKNOWN);
557 mMessageRejectionCount = 0;
558 saveMessageRejectionCount();
559 }
560
561 refresh();
562
563 if (bondState == BluetoothDevice.BOND_BONDED) {
564 if (mDevice.isBluetoothDock()) {
565 onBondingDockConnect();
566 } else if (mConnectAfterPairing) {
567 connect(false);
568 }
569 mConnectAfterPairing = false;
570 }
571 }
572
573 void setBtClass(BluetoothClass btClass) {
574 if (btClass != null && mBtClass != btClass) {
575 mBtClass = btClass;
576 dispatchAttributesChanged();
577 }
578 }
579
580 public BluetoothClass getBtClass() {
581 return mBtClass;
582 }
583
584 public List<LocalBluetoothProfile> getProfiles() {
585 return Collections.unmodifiableList(mProfiles);
586 }
587
588 public List<LocalBluetoothProfile> getConnectableProfiles() {
589 List<LocalBluetoothProfile> connectableProfiles =
590 new ArrayList<LocalBluetoothProfile>();
591 for (LocalBluetoothProfile profile : mProfiles) {
592 if (profile.isConnectable()) {
593 connectableProfiles.add(profile);
594 }
595 }
596 return connectableProfiles;
597 }
598
599 public List<LocalBluetoothProfile> getRemovedProfiles() {
600 return mRemovedProfiles;
601 }
602
603 public void registerCallback(Callback callback) {
604 synchronized (mCallbacks) {
605 mCallbacks.add(callback);
606 }
607 }
608
609 public void unregisterCallback(Callback callback) {
610 synchronized (mCallbacks) {
611 mCallbacks.remove(callback);
612 }
613 }
614
615 private void dispatchAttributesChanged() {
616 synchronized (mCallbacks) {
617 for (Callback callback : mCallbacks) {
618 callback.onDeviceAttributesChanged();
619 }
620 }
621 }
622
623 @Override
624 public String toString() {
625 return mDevice.toString();
626 }
627
628 @Override
629 public boolean equals(Object o) {
630 if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
631 return false;
632 }
633 return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
634 }
635
636 @Override
637 public int hashCode() {
638 return mDevice.getAddress().hashCode();
639 }
640
641 // This comparison uses non-final fields so the sort order may change
642 // when device attributes change (such as bonding state). Settings
643 // will completely refresh the device list when this happens.
644 public int compareTo(CachedBluetoothDevice another) {
645 // Connected above not connected
646 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
647 if (comparison != 0) return comparison;
648
649 // Paired above not paired
650 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
651 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
652 if (comparison != 0) return comparison;
653
654 // Visible above not visible
655 comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
656 if (comparison != 0) return comparison;
657
658 // Stronger signal above weaker signal
659 comparison = another.mRssi - mRssi;
660 if (comparison != 0) return comparison;
661
662 // Fallback on name
663 return mName.compareTo(another.mName);
664 }
665
666 public interface Callback {
667 void onDeviceAttributesChanged();
668 }
669
670 public int getPhonebookPermissionChoice() {
671 int permission = mDevice.getPhonebookAccessPermission();
672 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
673 return ACCESS_ALLOWED;
674 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
675 return ACCESS_REJECTED;
676 }
677 return ACCESS_UNKNOWN;
678 }
679
680 public void setPhonebookPermissionChoice(int permissionChoice) {
681 int permission = BluetoothDevice.ACCESS_UNKNOWN;
682 if (permissionChoice == ACCESS_ALLOWED) {
683 permission = BluetoothDevice.ACCESS_ALLOWED;
684 } else if (permissionChoice == ACCESS_REJECTED) {
685 permission = BluetoothDevice.ACCESS_REJECTED;
686 }
687 mDevice.setPhonebookAccessPermission(permission);
688 }
689
690 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
691 // app's shared preferences).
692 private void migratePhonebookPermissionChoice() {
693 SharedPreferences preferences = mContext.getSharedPreferences(
694 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
695 if (!preferences.contains(mDevice.getAddress())) {
696 return;
697 }
698
699 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
700 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
701 if (oldPermission == ACCESS_ALLOWED) {
702 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
703 } else if (oldPermission == ACCESS_REJECTED) {
704 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
705 }
706 }
707
708 SharedPreferences.Editor editor = preferences.edit();
709 editor.remove(mDevice.getAddress());
710 editor.commit();
711 }
712
713 public int getMessagePermissionChoice() {
714 int permission = mDevice.getMessageAccessPermission();
715 if (permission == BluetoothDevice.ACCESS_ALLOWED) {
716 return ACCESS_ALLOWED;
717 } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
718 return ACCESS_REJECTED;
719 }
720 return ACCESS_UNKNOWN;
721 }
722
723 public void setMessagePermissionChoice(int permissionChoice) {
724 int permission = BluetoothDevice.ACCESS_UNKNOWN;
725 if (permissionChoice == ACCESS_ALLOWED) {
726 permission = BluetoothDevice.ACCESS_ALLOWED;
727 } else if (permissionChoice == ACCESS_REJECTED) {
728 permission = BluetoothDevice.ACCESS_REJECTED;
729 }
730 mDevice.setMessageAccessPermission(permission);
731 }
732
733 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
734 // app's shared preferences).
735 private void migrateMessagePermissionChoice() {
736 SharedPreferences preferences = mContext.getSharedPreferences(
737 "bluetooth_message_permission", Context.MODE_PRIVATE);
738 if (!preferences.contains(mDevice.getAddress())) {
739 return;
740 }
741
742 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
743 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
744 if (oldPermission == ACCESS_ALLOWED) {
745 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
746 } else if (oldPermission == ACCESS_REJECTED) {
747 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
748 }
749 }
750
751 SharedPreferences.Editor editor = preferences.edit();
752 editor.remove(mDevice.getAddress());
753 editor.commit();
754 }
755
756 /**
757 * @return Whether this rejection should persist.
758 */
759 public boolean checkAndIncreaseMessageRejectionCount() {
760 if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
761 mMessageRejectionCount++;
762 saveMessageRejectionCount();
763 }
764 return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
765 }
766
767 private void fetchMessageRejectionCount() {
768 SharedPreferences preference = mContext.getSharedPreferences(
769 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
770 mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
771 }
772
773 private void saveMessageRejectionCount() {
774 SharedPreferences.Editor editor = mContext.getSharedPreferences(
775 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
776 if (mMessageRejectionCount == 0) {
777 editor.remove(mDevice.getAddress());
778 } else {
779 editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
780 }
781 editor.commit();
782 }
783
784 private void processPhonebookAccess() {
785 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
786
787 ParcelUuid[] uuids = mDevice.getUuids();
788 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
789 // The pairing dialog now warns of phone-book access for paired devices.
790 // No separate prompt is displayed after pairing.
791 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
792 }
793 }
Jason Monkbe3c5db2015-02-04 13:00:55 -0500794
795 public int getMaxConnectionState() {
796 int maxState = BluetoothProfile.STATE_DISCONNECTED;
797 for (LocalBluetoothProfile profile : getProfiles()) {
798 int connectionStatus = getProfileConnectionState(profile);
799 if (connectionStatus > maxState) {
800 maxState = connectionStatus;
801 }
802 }
803 return maxState;
804 }
805
806 /**
807 * @return resource for string that discribes the connection state of this device.
808 */
809 public int getConnectionSummary() {
810 boolean profileConnected = false; // at least one profile is connected
811 boolean a2dpNotConnected = false; // A2DP is preferred but not connected
812 boolean headsetNotConnected = false; // Headset is preferred but not connected
813
814 for (LocalBluetoothProfile profile : getProfiles()) {
815 int connectionStatus = getProfileConnectionState(profile);
816
817 switch (connectionStatus) {
818 case BluetoothProfile.STATE_CONNECTING:
819 case BluetoothProfile.STATE_DISCONNECTING:
820 return Utils.getConnectionStateSummary(connectionStatus);
821
822 case BluetoothProfile.STATE_CONNECTED:
823 profileConnected = true;
824 break;
825
826 case BluetoothProfile.STATE_DISCONNECTED:
827 if (profile.isProfileReady()) {
828 if (profile instanceof A2dpProfile) {
829 a2dpNotConnected = true;
830 } else if (profile instanceof HeadsetProfile) {
831 headsetNotConnected = true;
832 }
833 }
834 break;
835 }
836 }
837
838 if (profileConnected) {
839 if (a2dpNotConnected && headsetNotConnected) {
840 return R.string.bluetooth_connected_no_headset_no_a2dp;
841 } else if (a2dpNotConnected) {
842 return R.string.bluetooth_connected_no_a2dp;
843 } else if (headsetNotConnected) {
844 return R.string.bluetooth_connected_no_headset;
845 } else {
846 return R.string.bluetooth_connected;
847 }
848 }
849
850 return getBondState() == BluetoothDevice.BOND_BONDING ? R.string.bluetooth_pairing : 0;
851 }
Jason Monk7ce96b92015-02-02 11:27:58 -0500852}