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