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