/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.bluetooth; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.content.Intent; import android.provider.DeviceConfig; import android.text.Editable; import android.util.Log; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import androidx.annotation.VisibleForTesting; import com.android.settings.R; import com.android.settings.bluetooth.BluetoothPairingDialogFragment.BluetoothPairingDialogListener; import com.android.settings.core.SettingsUIDeviceConfig; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfile; import java.util.Locale; /** * A controller used by {@link BluetoothPairingDialog} to manage connection state while we try to * pair with a bluetooth device. It includes methods that allow the * {@link BluetoothPairingDialogFragment} to interrogate the current state as well. */ public class BluetoothPairingController implements OnCheckedChangeListener, BluetoothPairingDialogListener { private static final String TAG = "BTPairingController"; // Different types of dialogs we can map to public static final int INVALID_DIALOG_TYPE = -1; public static final int USER_ENTRY_DIALOG = 0; public static final int CONFIRMATION_DIALOG = 1; public static final int DISPLAY_PASSKEY_DIALOG = 2; private static final int BLUETOOTH_PIN_MAX_LENGTH = 16; private static final int BLUETOOTH_PASSKEY_MAX_LENGTH = 6; // Bluetooth dependencies for the connection we are trying to establish LocalBluetoothManager mBluetoothManager; private BluetoothDevice mDevice; @VisibleForTesting int mType; private String mUserInput; private String mPasskeyFormatted; private int mPasskey; private int mInitiator; private String mDeviceName; private LocalBluetoothProfile mPbapClientProfile; private boolean mPbapAllowed; private boolean mIsCoordinatedSetMember; private boolean mIsLeAudio; private boolean mIsLeContactSharingEnabled; private boolean mIsLateBonding; /** * Creates an instance of a BluetoothPairingController. * * @param intent - must contain {@link BluetoothDevice#EXTRA_PAIRING_VARIANT}, {@link * BluetoothDevice#EXTRA_PAIRING_KEY}, and {@link BluetoothDevice#EXTRA_DEVICE}. Missing extra * will lead to undefined behavior. */ public BluetoothPairingController(Intent intent, Context context) { mBluetoothManager = Utils.getLocalBtManager(context); mDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); String message = ""; if (mBluetoothManager == null) { throw new IllegalStateException("Could not obtain LocalBluetoothManager"); } else if (mDevice == null) { throw new IllegalStateException("Could not find BluetoothDevice"); } mType = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR); mPasskey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR); mInitiator = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_INITIATOR, BluetoothDevice.ERROR); mDeviceName = mBluetoothManager.getCachedDeviceManager().getName(mDevice); mPbapClientProfile = mBluetoothManager.getProfileManager().getPbapClientProfile(); mPasskeyFormatted = formatKey(mPasskey); mIsLateBonding = mBluetoothManager.getCachedDeviceManager().isLateBonding(mDevice); final CachedBluetoothDevice cachedDevice = mBluetoothManager.getCachedDeviceManager().findDevice(mDevice); mIsCoordinatedSetMember = false; mIsLeAudio = false; mIsLeContactSharingEnabled = true; if (cachedDevice != null) { mIsCoordinatedSetMember = cachedDevice.isCoordinatedSetMemberDevice(); for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) { if (profile.getProfileId() == BluetoothProfile.LE_AUDIO) { mIsLeAudio = true; } } mIsLeContactSharingEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, SettingsUIDeviceConfig.BT_LE_AUDIO_CONTACT_SHARING_ENABLED, true); Log.d(TAG, "BT_LE_AUDIO_CONTACT_SHARING_ENABLED is " + mIsLeContactSharingEnabled + " isCooridnatedSetMember " + mIsCoordinatedSetMember); } } @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { mPbapAllowed = true; } else { mPbapAllowed = false; } } @Override public void onDialogPositiveClick(BluetoothPairingDialogFragment dialog) { if (mPbapAllowed) { mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); } else { mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); } if (getDialogType() == USER_ENTRY_DIALOG) { onPair(mUserInput); } else { onPair(null); } } @Override public void onDialogNegativeClick(BluetoothPairingDialogFragment dialog) { mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); onCancel(); } /** * A method for querying which bluetooth pairing dialog fragment variant this device requires. * * @return - The dialog view variant needed for this device. */ public int getDialogType() { switch (mType) { case BluetoothDevice.PAIRING_VARIANT_PIN: case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS: case BluetoothDevice.PAIRING_VARIANT_PASSKEY: return USER_ENTRY_DIALOG; case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: case BluetoothDevice.PAIRING_VARIANT_CONSENT: case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: return CONFIRMATION_DIALOG; case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: return DISPLAY_PASSKEY_DIALOG; default: return INVALID_DIALOG_TYPE; } } /** * @return - A string containing the name provided by the device. */ public String getDeviceName() { return mDeviceName; } /** * A method for querying if the bluetooth device is a LE coordinated set member device. * * @return - A boolean indicating if the device is a CSIP supported device. */ public boolean isCoordinatedSetMemberDevice() { return mIsCoordinatedSetMember; } /** * A method for querying if the bluetooth device from a coordinated set is bonding late. * * @return - A boolean indicating if the device is bonding late. */ public boolean isLateBonding() { return mIsLateBonding; } /** * A method for querying if the bluetooth device has a profile already set up on this device. * * @return - A boolean indicating if the device has previous knowledge of a profile for this * device. */ public boolean isProfileReady() { return mPbapClientProfile != null && mPbapClientProfile.isProfileReady(); } @VisibleForTesting boolean isLeAudio() { return mIsLeAudio; } @VisibleForTesting boolean isLeContactSharingEnabled() { return mIsLeContactSharingEnabled; } /** * A method whether the device allows to show the le audio's contact sharing. * * @return A boolean whether the device allows to show the contact sharing. */ public boolean isContactSharingVisible() { boolean isContactSharingVisible = !isProfileReady(); // If device do not support the ContactSharing of LE audio device, hiding ContactSharing UI if (isLeAudio() && !isLeContactSharingEnabled()) { isContactSharingVisible = false; } return isContactSharingVisible; } /** * A method for querying if the bluetooth device has access to contacts on the device. * * @return - A boolean indicating if the bluetooth device has permission to access the device * contacts */ public boolean getContactSharingState() { switch (mDevice.getPhonebookAccessPermission()) { case BluetoothDevice.ACCESS_ALLOWED: return true; case BluetoothDevice.ACCESS_REJECTED: return false; default: if (BluetoothUtils.isDeviceClassMatched( mDevice, BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE)) { return BluetoothDevice.EXTRA_PAIRING_INITIATOR_FOREGROUND == mInitiator; } return false; } } /** * Update Phone book permission * */ public void setContactSharingState() { final int permission = mDevice.getPhonebookAccessPermission(); if (permission == BluetoothDevice.ACCESS_ALLOWED || (permission == BluetoothDevice.ACCESS_UNKNOWN && BluetoothUtils.isDeviceClassMatched(mDevice, BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE))) { onCheckedChanged(null, true); } else { onCheckedChanged(null, false); } } /** * A method for querying if the provided editable is a valid passkey/pin format for this device. * * @param s - The passkey/pin * @return - A boolean indicating if the passkey/pin is of the correct format. */ public boolean isPasskeyValid(Editable s) { boolean requires16Digits = mType == BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS; return s.length() >= 16 && requires16Digits || s.length() > 0 && !requires16Digits; } /** * A method for querying what message should be shown to the user as additional text in the * dialog for this device. Returns -1 to indicate a device type that does not use this message. * * @return - The message ID to show the user. */ public int getDeviceVariantMessageId() { switch (mType) { case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS: case BluetoothDevice.PAIRING_VARIANT_PIN: return R.string.bluetooth_enter_pin_other_device; case BluetoothDevice.PAIRING_VARIANT_PASSKEY: return R.string.bluetooth_enter_passkey_other_device; default: return INVALID_DIALOG_TYPE; } } /** * A method for querying what message hint should be shown to the user as additional text in the * dialog for this device. Returns -1 to indicate a device type that does not use this message. * * @return - The message ID to show the user. */ public int getDeviceVariantMessageHintId() { switch (mType) { case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS: return R.string.bluetooth_pin_values_hint_16_digits; case BluetoothDevice.PAIRING_VARIANT_PIN: case BluetoothDevice.PAIRING_VARIANT_PASSKEY: return R.string.bluetooth_pin_values_hint; default: return INVALID_DIALOG_TYPE; } } /** * A method for querying the maximum passkey/pin length for this device. * * @return - An int indicating the maximum length */ public int getDeviceMaxPasskeyLength() { switch (mType) { case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS: case BluetoothDevice.PAIRING_VARIANT_PIN: return BLUETOOTH_PIN_MAX_LENGTH; case BluetoothDevice.PAIRING_VARIANT_PASSKEY: return BLUETOOTH_PASSKEY_MAX_LENGTH; default: return 0; } } /** * A method for querying if the device uses an alphanumeric passkey. * * @return - a boolean indicating if the passkey can be alphanumeric. */ public boolean pairingCodeIsAlphanumeric() { switch (mType) { case BluetoothDevice.PAIRING_VARIANT_PASSKEY: return false; default: return true; } } /** * A method used by the dialogfragment to notify the controller that the dialog has been * displayed for bluetooth device types that just care about it being displayed. */ protected void notifyDialogDisplayed() { // send an OK to the framework, indicating that the dialog has been displayed. if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) { mDevice.setPairingConfirmation(true); } else if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN) { mDevice.setPin(mPasskeyFormatted); } } /** * A method for querying if this bluetooth device type has a key it would like displayed * to the user. * * @return - A boolean indicating if a key exists which should be displayed to the user. */ public boolean isDisplayPairingKeyVariant() { switch (mType) { case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: return true; default: return false; } } /** * A method for querying if this bluetooth device type has other content it would like displayed * to the user. * * @return - A boolean indicating if content exists which should be displayed to the user. */ public boolean hasPairingContent() { switch (mType) { case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: return true; default: return false; } } /** * A method for obtaining any additional content this bluetooth device has for displaying to the * user. * * @return - A string containing the additional content, null if none exists. * @see {@link BluetoothPairingController#hasPairingContent()} */ public String getPairingContent() { if (hasPairingContent()) { return mPasskeyFormatted; } else { return null; } } /** * A method that exists to allow the fragment to update the controller with input the user has * provided in the fragment. * * @param input - A string containing the user input. */ protected void updateUserInput(String input) { mUserInput = input; } /** * Returns the provided passkey in a format that this device expects. Only works for numeric * passkeys/pins. * * @param passkey - An integer containing the passkey to format. * @return - A string containing the formatted passkey/pin */ private String formatKey(int passkey) { switch (mType) { case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: return String.format(Locale.getDefault(), "%06d", passkey); case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: return String.format("%04d", passkey); default: return null; } } /** * handles the necessary communication with the bluetooth device to establish a successful * pairing * * @param passkey - The passkey we will attempt to pair to the device with. */ private void onPair(String passkey) { Log.d(TAG, "Pairing dialog accepted"); switch (mType) { case BluetoothDevice.PAIRING_VARIANT_PIN: case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS: mDevice.setPin(passkey); break; case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: case BluetoothDevice.PAIRING_VARIANT_CONSENT: mDevice.setPairingConfirmation(true); break; case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: case BluetoothDevice.PAIRING_VARIANT_PASSKEY: // Do nothing. break; default: Log.e(TAG, "Incorrect pairing type received"); } } /** * A method for properly ending communication with the bluetooth device. Will be called by the * {@link BluetoothPairingDialogFragment} when it is dismissed. */ public void onCancel() { Log.d(TAG, "Pairing dialog canceled"); mDevice.cancelBondProcess(); } /** * A method for checking if this device is equal to another device. * * @param device - The other device being compared to this device. * @return - A boolean indicating if the devices were equal. */ public boolean deviceEquals(BluetoothDevice device) { return mDevice == device; } @VisibleForTesting void mockPbapClientProfile(LocalBluetoothProfile mockPbapClientProfile) { mPbapClientProfile = mockPbapClientProfile; } }