blob: 535e04086c04a15b2d1a2a8475a2e954d567ae81 [file] [log] [blame]
Salvador Martinezd98d0832016-10-03 17:52:51 -07001/*
2 * Copyright (C) 2016 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 */
16package com.android.settings.bluetooth;
17
18import android.bluetooth.BluetoothClass;
19import android.bluetooth.BluetoothDevice;
SongFerngWang51cabc52022-09-23 21:21:25 +080020import android.bluetooth.BluetoothProfile;
Salvador Martinezd98d0832016-10-03 17:52:51 -070021import android.content.Context;
22import android.content.Intent;
SongFerngWang51cabc52022-09-23 21:21:25 +080023import android.provider.DeviceConfig;
Salvador Martinezd98d0832016-10-03 17:52:51 -070024import android.text.Editable;
25import android.util.Log;
26import android.widget.CompoundButton;
27import android.widget.CompoundButton.OnCheckedChangeListener;
jackqdyulei7015e202018-06-19 16:52:07 -070028
Fan Zhang23f8d592018-08-28 15:11:40 -070029import androidx.annotation.VisibleForTesting;
30
Salvador Martinezd98d0832016-10-03 17:52:51 -070031import com.android.settings.R;
32import com.android.settings.bluetooth.BluetoothPairingDialogFragment.BluetoothPairingDialogListener;
SongFerngWang51cabc52022-09-23 21:21:25 +080033import com.android.settings.core.SettingsUIDeviceConfig;
Shen Linbdcface2023-01-03 14:16:58 +080034import com.android.settingslib.bluetooth.BluetoothUtils;
Alice Kuof57f7202021-01-29 15:37:39 +080035import com.android.settingslib.bluetooth.CachedBluetoothDevice;
Salvador Martinezd98d0832016-10-03 17:52:51 -070036import com.android.settingslib.bluetooth.LocalBluetoothManager;
37import com.android.settingslib.bluetooth.LocalBluetoothProfile;
jackqdyulei7015e202018-06-19 16:52:07 -070038
Salvador Martinezd98d0832016-10-03 17:52:51 -070039import java.util.Locale;
40
41/**
42 * A controller used by {@link BluetoothPairingDialog} to manage connection state while we try to
43 * pair with a bluetooth device. It includes methods that allow the
44 * {@link BluetoothPairingDialogFragment} to interrogate the current state as well.
45 */
46public class BluetoothPairingController implements OnCheckedChangeListener,
47 BluetoothPairingDialogListener {
48
49 private static final String TAG = "BTPairingController";
50
51 // Different types of dialogs we can map to
52 public static final int INVALID_DIALOG_TYPE = -1;
53 public static final int USER_ENTRY_DIALOG = 0;
54 public static final int CONFIRMATION_DIALOG = 1;
55 public static final int DISPLAY_PASSKEY_DIALOG = 2;
56
57 private static final int BLUETOOTH_PIN_MAX_LENGTH = 16;
58 private static final int BLUETOOTH_PASSKEY_MAX_LENGTH = 6;
59
60 // Bluetooth dependencies for the connection we are trying to establish
SongFerngWang51cabc52022-09-23 21:21:25 +080061 LocalBluetoothManager mBluetoothManager;
timhypengeb230c72018-09-19 15:54:13 +080062 private BluetoothDevice mDevice;
jackqdyulei7015e202018-06-19 16:52:07 -070063 @VisibleForTesting
64 int mType;
Salvador Martinezd98d0832016-10-03 17:52:51 -070065 private String mUserInput;
66 private String mPasskeyFormatted;
67 private int mPasskey;
Myles Watson0d31c372020-06-08 15:07:23 -070068 private int mInitiator;
Salvador Martinezd98d0832016-10-03 17:52:51 -070069 private String mDeviceName;
70 private LocalBluetoothProfile mPbapClientProfile;
Hansong Zhang500c83c2018-05-17 13:54:00 -070071 private boolean mPbapAllowed;
Alice Kuof57f7202021-01-29 15:37:39 +080072 private boolean mIsCoordinatedSetMember;
SongFerngWang51cabc52022-09-23 21:21:25 +080073 private boolean mIsLeAudio;
74 private boolean mIsLeContactSharingEnabled;
Salvador Martinezd98d0832016-10-03 17:52:51 -070075
76 /**
77 * Creates an instance of a BluetoothPairingController.
78 *
79 * @param intent - must contain {@link BluetoothDevice#EXTRA_PAIRING_VARIANT}, {@link
80 * BluetoothDevice#EXTRA_PAIRING_KEY}, and {@link BluetoothDevice#EXTRA_DEVICE}. Missing extra
81 * will lead to undefined behavior.
82 */
83 public BluetoothPairingController(Intent intent, Context context) {
84 mBluetoothManager = Utils.getLocalBtManager(context);
85 mDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
86
87 String message = "";
88 if (mBluetoothManager == null) {
89 throw new IllegalStateException("Could not obtain LocalBluetoothManager");
90 } else if (mDevice == null) {
91 throw new IllegalStateException("Could not find BluetoothDevice");
92 }
93
94 mType = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR);
95 mPasskey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR);
Myles Watson0d31c372020-06-08 15:07:23 -070096 mInitiator =
97 intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_INITIATOR, BluetoothDevice.ERROR);
Lei Yu499013b2018-09-17 18:22:57 +000098 mDeviceName = mBluetoothManager.getCachedDeviceManager().getName(mDevice);
Salvador Martinezd98d0832016-10-03 17:52:51 -070099 mPbapClientProfile = mBluetoothManager.getProfileManager().getPbapClientProfile();
100 mPasskeyFormatted = formatKey(mPasskey);
SongFerngWang51cabc52022-09-23 21:21:25 +0800101
Alice Kuof57f7202021-01-29 15:37:39 +0800102 final CachedBluetoothDevice cachedDevice =
103 mBluetoothManager.getCachedDeviceManager().findDevice(mDevice);
SongFerngWang51cabc52022-09-23 21:21:25 +0800104
105 mIsCoordinatedSetMember = false;
106 mIsLeAudio = false;
107 mIsLeContactSharingEnabled = true;
108 if (cachedDevice != null) {
109 mIsCoordinatedSetMember = cachedDevice.isCoordinatedSetMemberDevice();
110
111 for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
112 if (profile.getProfileId() == BluetoothProfile.LE_AUDIO) {
113 mIsLeAudio = true;
114 }
115 }
116
117 mIsLeContactSharingEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
118 SettingsUIDeviceConfig.BT_LE_AUDIO_CONTACT_SHARING_ENABLED, true);
119 Log.d(TAG, "BT_LE_AUDIO_CONTACT_SHARING_ENABLED is " + mIsLeContactSharingEnabled);
120 }
Salvador Martinezd98d0832016-10-03 17:52:51 -0700121 }
122
123 @Override
124 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
125 if (isChecked) {
Hansong Zhang500c83c2018-05-17 13:54:00 -0700126 mPbapAllowed = true;
Salvador Martinezd98d0832016-10-03 17:52:51 -0700127 } else {
Hansong Zhang500c83c2018-05-17 13:54:00 -0700128 mPbapAllowed = false;
Salvador Martinezd98d0832016-10-03 17:52:51 -0700129 }
130 }
131
132 @Override
133 public void onDialogPositiveClick(BluetoothPairingDialogFragment dialog) {
jackqdyulei7015e202018-06-19 16:52:07 -0700134 if (mPbapAllowed) {
135 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
136 } else {
137 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
138 }
139
Salvador Martinezd98d0832016-10-03 17:52:51 -0700140 if (getDialogType() == USER_ENTRY_DIALOG) {
141 onPair(mUserInput);
142 } else {
143 onPair(null);
144 }
145 }
146
147 @Override
148 public void onDialogNegativeClick(BluetoothPairingDialogFragment dialog) {
Hansong Zhang500c83c2018-05-17 13:54:00 -0700149 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
Salvador Martinezd98d0832016-10-03 17:52:51 -0700150 onCancel();
151 }
152
153 /**
154 * A method for querying which bluetooth pairing dialog fragment variant this device requires.
155 *
156 * @return - The dialog view variant needed for this device.
157 */
158 public int getDialogType() {
159 switch (mType) {
160 case BluetoothDevice.PAIRING_VARIANT_PIN:
161 case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
162 case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
163 return USER_ENTRY_DIALOG;
164
165 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
166 case BluetoothDevice.PAIRING_VARIANT_CONSENT:
167 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
168 return CONFIRMATION_DIALOG;
169
170 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
171 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
172 return DISPLAY_PASSKEY_DIALOG;
173
174 default:
175 return INVALID_DIALOG_TYPE;
176 }
177 }
178
179 /**
180 * @return - A string containing the name provided by the device.
181 */
182 public String getDeviceName() {
183 return mDeviceName;
184 }
185
186 /**
Alice Kuof57f7202021-01-29 15:37:39 +0800187 * A method for querying if the bluetooth device is a LE coordinated set member device.
188 *
189 * @return - A boolean indicating if the device is a CSIP supported device.
190 */
191 public boolean isCoordinatedSetMemberDevice() {
192 return mIsCoordinatedSetMember;
193 }
194
195 /**
Salvador Martinezd98d0832016-10-03 17:52:51 -0700196 * A method for querying if the bluetooth device has a profile already set up on this device.
197 *
198 * @return - A boolean indicating if the device has previous knowledge of a profile for this
199 * device.
200 */
201 public boolean isProfileReady() {
202 return mPbapClientProfile != null && mPbapClientProfile.isProfileReady();
203 }
204
SongFerngWang51cabc52022-09-23 21:21:25 +0800205 @VisibleForTesting
206 boolean isLeAudio() {
207 return mIsLeAudio;
208 }
209
210 @VisibleForTesting
211 boolean isLeContactSharingEnabled() {
212 return mIsLeContactSharingEnabled;
213 }
214
215 /**
216 * A method whether the device allows to show the le audio's contact sharing.
217 *
218 * @return A boolean whether the device allows to show the contact sharing.
219 */
220 public boolean isContactSharingVisible() {
221 boolean isContactSharingVisible = !isProfileReady();
222 // If device do not support the ContactSharing of LE audio device, hiding ContactSharing UI
223 if (isLeAudio() && !isLeContactSharingEnabled()) {
224 isContactSharingVisible = false;
225 }
226 return isContactSharingVisible;
227 }
228
Salvador Martinezd98d0832016-10-03 17:52:51 -0700229 /**
230 * A method for querying if the bluetooth device has access to contacts on the device.
231 *
232 * @return - A boolean indicating if the bluetooth device has permission to access the device
233 * contacts
234 */
235 public boolean getContactSharingState() {
236 switch (mDevice.getPhonebookAccessPermission()) {
237 case BluetoothDevice.ACCESS_ALLOWED:
238 return true;
239 case BluetoothDevice.ACCESS_REJECTED:
240 return false;
241 default:
Shen Linbdcface2023-01-03 14:16:58 +0800242 if (BluetoothUtils.isDeviceClassMatched(
243 mDevice, BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE)) {
Myles Watson0d31c372020-06-08 15:07:23 -0700244 return BluetoothDevice.EXTRA_PAIRING_INITIATOR_FOREGROUND == mInitiator;
Salvador Martinezd98d0832016-10-03 17:52:51 -0700245 }
246 return false;
247 }
248 }
249
250 /**
Hemant Gupta5b84cfa2017-09-22 12:24:14 +0530251 * Update Phone book permission
252 *
253 */
Shen Linbdcface2023-01-03 14:16:58 +0800254 public void setContactSharingState() {
timhypengeb230c72018-09-19 15:54:13 +0800255 final int permission = mDevice.getPhonebookAccessPermission();
256 if (permission == BluetoothDevice.ACCESS_ALLOWED
Shen Linbdcface2023-01-03 14:16:58 +0800257 || (permission == BluetoothDevice.ACCESS_UNKNOWN
258 && BluetoothUtils.isDeviceClassMatched(mDevice,
259 BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE))) {
timhypengeb230c72018-09-19 15:54:13 +0800260 onCheckedChanged(null, true);
261 } else {
262 onCheckedChanged(null, false);
263 }
264
265 }
Hemant Gupta5b84cfa2017-09-22 12:24:14 +0530266
267 /**
Salvador Martinezd98d0832016-10-03 17:52:51 -0700268 * A method for querying if the provided editable is a valid passkey/pin format for this device.
269 *
270 * @param s - The passkey/pin
271 * @return - A boolean indicating if the passkey/pin is of the correct format.
272 */
273 public boolean isPasskeyValid(Editable s) {
274 boolean requires16Digits = mType == BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS;
275 return s.length() >= 16 && requires16Digits || s.length() > 0 && !requires16Digits;
276 }
277
278 /**
279 * A method for querying what message should be shown to the user as additional text in the
280 * dialog for this device. Returns -1 to indicate a device type that does not use this message.
281 *
282 * @return - The message ID to show the user.
283 */
Salvador Martinezf492c282016-10-24 15:51:58 -0700284 public int getDeviceVariantMessageId() {
Salvador Martinezd98d0832016-10-03 17:52:51 -0700285 switch (mType) {
286 case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
287 case BluetoothDevice.PAIRING_VARIANT_PIN:
288 return R.string.bluetooth_enter_pin_other_device;
289
290 case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
291 return R.string.bluetooth_enter_passkey_other_device;
292
293 default:
Salvador Martinezf492c282016-10-24 15:51:58 -0700294 return INVALID_DIALOG_TYPE;
Salvador Martinezd98d0832016-10-03 17:52:51 -0700295 }
296 }
297
298 /**
299 * A method for querying what message hint should be shown to the user as additional text in the
300 * dialog for this device. Returns -1 to indicate a device type that does not use this message.
301 *
302 * @return - The message ID to show the user.
303 */
Salvador Martinezf492c282016-10-24 15:51:58 -0700304 public int getDeviceVariantMessageHintId() {
Salvador Martinezd98d0832016-10-03 17:52:51 -0700305 switch (mType) {
306 case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
307 return R.string.bluetooth_pin_values_hint_16_digits;
308
309 case BluetoothDevice.PAIRING_VARIANT_PIN:
310 case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
311 return R.string.bluetooth_pin_values_hint;
312
313 default:
Salvador Martinezf492c282016-10-24 15:51:58 -0700314 return INVALID_DIALOG_TYPE;
Salvador Martinezd98d0832016-10-03 17:52:51 -0700315 }
316 }
317
318 /**
319 * A method for querying the maximum passkey/pin length for this device.
320 *
321 * @return - An int indicating the maximum length
322 */
323 public int getDeviceMaxPasskeyLength() {
324 switch (mType) {
325 case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
326 case BluetoothDevice.PAIRING_VARIANT_PIN:
327 return BLUETOOTH_PIN_MAX_LENGTH;
328
329 case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
330 return BLUETOOTH_PASSKEY_MAX_LENGTH;
331
332 default:
333 return 0;
334 }
335
336 }
337
338 /**
339 * A method for querying if the device uses an alphanumeric passkey.
340 *
341 * @return - a boolean indicating if the passkey can be alphanumeric.
342 */
343 public boolean pairingCodeIsAlphanumeric() {
344 switch (mType) {
345 case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
346 return false;
347
348 default:
349 return true;
350 }
351 }
352
353 /**
354 * A method used by the dialogfragment to notify the controller that the dialog has been
355 * displayed for bluetooth device types that just care about it being displayed.
356 */
357 protected void notifyDialogDisplayed() {
358 // send an OK to the framework, indicating that the dialog has been displayed.
359 if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) {
360 mDevice.setPairingConfirmation(true);
361 } else if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN) {
Rahul Sabnis9348f312019-12-04 16:36:21 -0800362 mDevice.setPin(mPasskeyFormatted);
Salvador Martinezd98d0832016-10-03 17:52:51 -0700363 }
364 }
365
366 /**
367 * A method for querying if this bluetooth device type has a key it would like displayed
368 * to the user.
369 *
370 * @return - A boolean indicating if a key exists which should be displayed to the user.
371 */
372 public boolean isDisplayPairingKeyVariant() {
373 switch (mType) {
374 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
375 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
376 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
377 return true;
378 default:
379 return false;
380 }
381 }
382
383 /**
384 * A method for querying if this bluetooth device type has other content it would like displayed
385 * to the user.
386 *
387 * @return - A boolean indicating if content exists which should be displayed to the user.
388 */
389 public boolean hasPairingContent() {
390 switch (mType) {
391 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
392 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
393 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
394 return true;
395
396 default:
397 return false;
398 }
399 }
400
401 /**
402 * A method for obtaining any additional content this bluetooth device has for displaying to the
403 * user.
404 *
405 * @return - A string containing the additional content, null if none exists.
406 * @see {@link BluetoothPairingController#hasPairingContent()}
407 */
408 public String getPairingContent() {
409 if (hasPairingContent()) {
410 return mPasskeyFormatted;
411 } else {
412 return null;
413 }
414 }
415
416 /**
417 * A method that exists to allow the fragment to update the controller with input the user has
418 * provided in the fragment.
419 *
420 * @param input - A string containing the user input.
421 */
422 protected void updateUserInput(String input) {
423 mUserInput = input;
424 }
425
426 /**
427 * Returns the provided passkey in a format that this device expects. Only works for numeric
428 * passkeys/pins.
429 *
430 * @param passkey - An integer containing the passkey to format.
431 * @return - A string containing the formatted passkey/pin
432 */
433 private String formatKey(int passkey) {
434 switch (mType) {
435 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
436 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
437 return String.format(Locale.US, "%06d", passkey);
438
439 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
440 return String.format("%04d", passkey);
441
442 default:
443 return null;
444 }
445 }
446
447 /**
448 * handles the necessary communication with the bluetooth device to establish a successful
449 * pairing
450 *
451 * @param passkey - The passkey we will attempt to pair to the device with.
452 */
453 private void onPair(String passkey) {
454 Log.d(TAG, "Pairing dialog accepted");
455 switch (mType) {
456 case BluetoothDevice.PAIRING_VARIANT_PIN:
457 case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
Rahul Sabnis9348f312019-12-04 16:36:21 -0800458 mDevice.setPin(passkey);
Salvador Martinezd98d0832016-10-03 17:52:51 -0700459 break;
460
Salvador Martinezd98d0832016-10-03 17:52:51 -0700461
462 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
463 case BluetoothDevice.PAIRING_VARIANT_CONSENT:
464 mDevice.setPairingConfirmation(true);
465 break;
466
467 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
468 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
Salvador Martinezd98d0832016-10-03 17:52:51 -0700469 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
Rahul Sabnis9348f312019-12-04 16:36:21 -0800470 case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
471 // Do nothing.
Salvador Martinezd98d0832016-10-03 17:52:51 -0700472 break;
473
474 default:
475 Log.e(TAG, "Incorrect pairing type received");
476 }
477 }
478
479 /**
480 * A method for properly ending communication with the bluetooth device. Will be called by the
481 * {@link BluetoothPairingDialogFragment} when it is dismissed.
482 */
483 public void onCancel() {
484 Log.d(TAG, "Pairing dialog canceled");
William Escande4a5c1282022-02-08 15:33:16 +0100485 mDevice.cancelBondProcess();
Salvador Martinezd98d0832016-10-03 17:52:51 -0700486 }
487
488 /**
489 * A method for checking if this device is equal to another device.
490 *
491 * @param device - The other device being compared to this device.
492 * @return - A boolean indicating if the devices were equal.
493 */
494 public boolean deviceEquals(BluetoothDevice device) {
495 return mDevice == device;
496 }
SongFerngWang51cabc52022-09-23 21:21:25 +0800497
498 @VisibleForTesting
499 void mockPbapClientProfile(LocalBluetoothProfile mockPbapClientProfile) {
500 mPbapClientProfile = mockPbapClientProfile;
501 }
Salvador Martinezd98d0832016-10-03 17:52:51 -0700502}