/* * Copyright 2018 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.car.settings.bluetooth; import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.car.drivingstate.CarUxRestrictions; import android.content.Context; import android.os.UserManager; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceGroup; import com.android.car.settings.R; import com.android.car.settings.common.CarUxRestrictionsHelper; import com.android.car.settings.common.FragmentController; import com.android.car.settings.common.MultiActionPreference; import com.android.car.settings.common.ToggleButtonActionItem; import com.android.settingslib.bluetooth.BluetoothDeviceFilter; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfile; import java.util.Set; /** * Displays a list of bonded (paired) Bluetooth devices. Clicking on a device launch the device * details page. Additional buttons to will connect/disconnect from the device, toggle phone calls, * and toggle media audio. * *
* Moreover, these buttons' availability and enable/disable status are controlled by UX restriction * and user restriction. Specifically, *
* Note: when button is disabled, it will still be shown as available. When the button is disabled * because of {@link UserManager.DISALLOW_CONFIG_BLUETOOTH} is set by DevicePolicyManager, click * on the button will show action disabled by admin dialog. * *
* Device detail page will not be launched when UX retriction is set. It can still be launched
* when there is {@link UserManager.DISALLOW_CONFIG_BLUETOOTH} restriction. However, individual
* profile's toggle switch will be disabled - when clicked, shows action disabled by admin dialog.
*/
public class BluetoothBondedDevicesPreferenceController extends
BluetoothDevicesGroupPreferenceController implements
BluetoothDevicePreference.UpdateToggleButtonListener {
private static final MultiActionPreference.ActionItem BLUETOOTH_BUTTON =
MultiActionPreference.ActionItem.ACTION_ITEM1;
private static final MultiActionPreference.ActionItem PHONE_BUTTON =
MultiActionPreference.ActionItem.ACTION_ITEM2;
private static final MultiActionPreference.ActionItem MEDIA_BUTTON =
MultiActionPreference.ActionItem.ACTION_ITEM3;
private final BluetoothDeviceFilter.Filter mBondedDeviceTypeFilter =
new BondedDeviceTypeFilter();
private boolean mShowDeviceDetails = true;
private boolean mHasUxRestriction;
public BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
@VisibleForTesting
BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions,
LocalBluetoothManager localBluetoothManager, UserManager userManager) {
super(context, preferenceKey, fragmentController, uxRestrictions, localBluetoothManager,
userManager);
}
@Override
protected BluetoothDeviceFilter.Filter getDeviceFilter() {
return mBondedDeviceTypeFilter;
}
@Override
protected BluetoothDevicePreference createDevicePreference(CachedBluetoothDevice cachedDevice) {
BluetoothDevicePreference pref = super.createDevicePreference(cachedDevice);
ToggleButtonActionItem bluetoothItem = pref.getActionItem(BLUETOOTH_BUTTON);
ToggleButtonActionItem phoneItem = pref.getActionItem(PHONE_BUTTON);
ToggleButtonActionItem mediaItem = pref.getActionItem(MEDIA_BUTTON);
bluetoothItem.setVisible(true);
phoneItem.setVisible(true);
mediaItem.setVisible(true);
bluetoothItem.setContentDescription(getContext(),
R.string.bluetooth_bonded_bluetooth_toggle_content_description);
phoneItem.setContentDescription(getContext(),
R.string.bluetooth_bonded_phone_toggle_content_description);
mediaItem.setContentDescription(getContext(),
R.string.bluetooth_bonded_media_toggle_content_description);
pref.setToggleButtonUpdateListener(this);
mHasUxRestriction = hasNoSetupUxRestriction();
setButtonsCheckedAndListeners(pref);
return pref;
}
@Override
protected void onDeviceClicked(CachedBluetoothDevice cachedDevice) {
if (mShowDeviceDetails) {
getFragmentController().launchFragment(
BluetoothDeviceDetailsFragment.newInstance(cachedDevice));
}
}
@Override
public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
refreshUi();
}
@Override
protected void updateState(PreferenceGroup preferenceGroup) {
super.updateState(preferenceGroup);
updateActionAvailability(preferenceGroup);
}
@Override
public void updateToggleButtonState(BluetoothDevicePreference preference) {
updateActionAvailability(preference);
}
private void updateActionAvailability(PreferenceGroup group) {
for (int i = 0; i < group.getPreferenceCount(); i++) {
BluetoothDevicePreference preference =
(BluetoothDevicePreference) group.getPreference(i);
updateActionAvailability(preference);
}
}
private void updateActionAvailability(BluetoothDevicePreference preference) {
mHasUxRestriction = hasNoSetupUxRestriction();
if (!mHasUxRestriction) {
setButtonsCheckedAndListeners(preference);
} else {
updatePhoneActionItemAvailability(preference, /* isAvailable= */ false);
updateMediaActionItemAvailability(preference, /* isAvailable= */ false);
}
mShowDeviceDetails = !mHasUxRestriction;
}
private void toggleBluetoothConnectivity(boolean connect, CachedBluetoothDevice cachedDevice) {
if (connect) {
cachedDevice.connect();
} else if (cachedDevice.isConnected()) {
cachedDevice.disconnect();
}
}
private void setButtonsCheckedAndListeners(BluetoothDevicePreference preference) {
CachedBluetoothDevice cachedDevice = preference.getCachedDevice();
// If device is currently attempting to connect/disconnect, disable further actions
if (cachedDevice.isBusy()) {
disableAllActionItems(preference);
// There is a case where on creation the cached device will try to automatically connect
// but does not report itself as busy yet. This ensures that the bluetooth button state
// is correct (should be checked in either connecting or disconnecting states).
preference.getActionItem(BLUETOOTH_BUTTON).setChecked(true);
return;
}
LocalBluetoothProfile phoneProfile = null;
LocalBluetoothProfile mediaProfile = null;
for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
if (profile.getProfileId() == BluetoothProfile.HEADSET_CLIENT) {
phoneProfile = profile;
} else if (profile.getProfileId() == BluetoothProfile.A2DP_SINK) {
mediaProfile = profile;
}
}
LocalBluetoothProfile finalPhoneProfile = phoneProfile;
LocalBluetoothProfile finalMediaProfile = mediaProfile;
boolean isConnected = cachedDevice.isConnected();
// Setup up bluetooth button
updateBluetoothActionItemAvailability(preference);
ToggleButtonActionItem bluetoothItem = preference.getActionItem(BLUETOOTH_BUTTON);
bluetoothItem.setChecked(isConnected);
bluetoothItem.setOnClickListener(
isChecked -> {
if (cachedDevice.isBusy()) {
return;
}
// If trying to connect and both phone and media are disabled, connecting will
// always fail. In this case force both profiles on.
if (isChecked && finalPhoneProfile != null && finalMediaProfile != null
&& !finalPhoneProfile.isEnabled(cachedDevice.getDevice())
&& !finalMediaProfile.isEnabled(cachedDevice.getDevice())) {
finalPhoneProfile.setEnabled(cachedDevice.getDevice(), true);
finalMediaProfile.setEnabled(cachedDevice.getDevice(), true);
}
toggleBluetoothConnectivity(isChecked, cachedDevice);
});
if (phoneProfile == null || !isConnected || mHasUxRestriction) {
// Disable phone button
updatePhoneActionItemAvailability(preference, /* isAvailable= */ false);
} else {
// Enable phone button
ToggleButtonActionItem phoneItem = preference.getActionItem(PHONE_BUTTON);
updatePhoneActionItemAvailability(preference, /* isAvailable= */ true);
boolean phoneEnabled = phoneProfile.isEnabled(cachedDevice.getDevice());
if (hasDisallowConfigRestriction()) {
phoneItem.setOnClickWhileDisabledListener(p -> BluetoothUtils
.onClickWhileDisabled(getContext(), getFragmentController()));
}
phoneItem.setOnClickListener(isChecked ->
finalPhoneProfile.setEnabled(cachedDevice.getDevice(), isChecked));
phoneItem.setChecked(phoneEnabled);
}
if (mediaProfile == null || !isConnected || mHasUxRestriction) {
// Disable media button
updateMediaActionItemAvailability(preference, /* isAvailable= */ false);
} else {
// Enable media button
ToggleButtonActionItem mediaItem = preference.getActionItem(MEDIA_BUTTON);
updateMediaActionItemAvailability(preference, /* isAvailable= */ true);
boolean mediaEnabled = mediaProfile.isEnabled(cachedDevice.getDevice());
if (hasDisallowConfigRestriction()) {
mediaItem.setOnClickWhileDisabledListener(p -> BluetoothUtils
.onClickWhileDisabled(getContext(), getFragmentController()));
}
mediaItem.setOnClickListener(isChecked ->
finalMediaProfile.setEnabled(cachedDevice.getDevice(), isChecked));
mediaItem.setChecked(mediaEnabled);
}
}
private void updateBluetoothActionItemAvailability(BluetoothDevicePreference preference) {
// Run on main thread because recyclerview may still be computing layout
getContext().getMainExecutor().execute(() -> {
ToggleButtonActionItem bluetoothItem = preference.getActionItem(BLUETOOTH_BUTTON);
bluetoothItem.setEnabled(true);
bluetoothItem.setDrawable(getContext(), R.drawable.ic_bluetooth_button);
});
}
private void updatePhoneActionItemAvailability(BluetoothDevicePreference preference,
boolean isAvailable) {
// Run on main thread because recyclerview may still be computing layout
getContext().getMainExecutor().execute(() -> {
ToggleButtonActionItem phoneItem = preference.getActionItem(PHONE_BUTTON);
phoneItem.setEnabled(isAvailable && !hasDisallowConfigRestriction());
phoneItem.setDrawable(getContext(), isAvailable
? R.drawable.ic_bluetooth_phone : R.drawable.ic_bluetooth_phone_unavailable);
phoneItem.setRestricted(!isAvailable && mHasUxRestriction);
});
}
private void updateMediaActionItemAvailability(BluetoothDevicePreference preference,
boolean isAvailable) {
// Run on main thread because recyclerview may still be computing layout
getContext().getMainExecutor().execute(() -> {
ToggleButtonActionItem mediaItem = preference.getActionItem(MEDIA_BUTTON);
mediaItem.setEnabled(isAvailable && !hasDisallowConfigRestriction());
mediaItem.setDrawable(getContext(), isAvailable
? R.drawable.ic_bluetooth_media : R.drawable.ic_bluetooth_media_unavailable);
mediaItem.setRestricted(!isAvailable && mHasUxRestriction);
});
}
private void disableAllActionItems(BluetoothDevicePreference preference) {
// Run on main thread because recyclerview may still be computing layout
getContext().getMainExecutor().execute(() -> {
preference.getActionItem(BLUETOOTH_BUTTON).setEnabled(false);
preference.getActionItem(PHONE_BUTTON).setEnabled(false);
preference.getActionItem(MEDIA_BUTTON).setEnabled(false);
});
}
private boolean hasDisallowConfigRestriction() {
return getUserManager().hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH);
}
private boolean hasNoSetupUxRestriction() {
return CarUxRestrictionsHelper.isNoSetup(getUxRestrictions());
}
/** Filter that matches only bonded devices with specific device types. */
//TODO(b/198339129): Use BluetoothDeviceFilter.BONDED_DEVICE_FILTER
private class BondedDeviceTypeFilter implements BluetoothDeviceFilter.Filter {
@Override
public boolean matches(BluetoothDevice device) {
Set