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