blob: f76aa353f9664273f2c2bdbfb4ddf276c5005822 [file] [log] [blame]
/*
* Copyright (C) 2021 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.qc;
import static android.os.UserManager.DISALLOW_BLUETOOTH;
import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
import static com.android.car.qc.QCItem.QC_ACTION_TOGGLE_STATE;
import static com.android.car.qc.QCItem.QC_TYPE_ACTION_TOGGLE;
import static com.android.car.settings.qc.QCUtils.getActionDisabledDialogIntent;
import static com.android.car.settings.qc.QCUtils.getAvailabilityStatusForZoneFromXml;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemProperties;
import androidx.annotation.DrawableRes;
import androidx.annotation.VisibleForTesting;
import com.android.car.qc.QCActionItem;
import com.android.car.qc.QCItem;
import com.android.car.qc.QCList;
import com.android.car.qc.QCRow;
import com.android.car.settings.R;
import com.android.car.settings.bluetooth.BluetoothUtils;
import com.android.car.settings.common.Logger;
import com.android.car.settings.enterprise.EnterpriseUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.HidProfile;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfile;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
/**
* QCItem for showing paired bluetooth devices.
*/
public class PairedBluetoothDevices extends SettingsQCItem {
@VisibleForTesting
static final String EXTRA_DEVICE_KEY = "BT_EXTRA_DEVICE_KEY";
@VisibleForTesting
static final String EXTRA_BUTTON_TYPE = "BT_EXTRA_BUTTON_TYPE";
@VisibleForTesting
static final String BLUETOOTH_BUTTON = "BLUETOOTH_BUTTON";
@VisibleForTesting
static final String PHONE_BUTTON = "PHONE_BUTTON";
@VisibleForTesting
static final String MEDIA_BUTTON = "MEDIA_BUTTON";
private static final Logger LOG = new Logger(PairedBluetoothDevices.class);
private final LocalBluetoothManager mBluetoothManager;
private final int mDeviceLimit;
private final boolean mShowDevicesWithoutNames;
public PairedBluetoothDevices(Context context) {
super(context);
setAvailabilityStatusForZone(getAvailabilityStatusForZoneFromXml(context,
R.xml.bluetooth_settings_fragment, R.string.pk_bluetooth_paired_devices));
mBluetoothManager = LocalBluetoothManager.getInstance(context, /* onInitCallback= */ null);
mDeviceLimit = context.getResources().getInteger(
R.integer.config_qc_bluetooth_device_limit);
mShowDevicesWithoutNames = SystemProperties.getBoolean(
BluetoothUtils.BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false);
}
@Override
QCItem getQCItem() {
if (!getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)
|| EnterpriseUtils.hasUserRestrictionByDpm(getContext(), DISALLOW_BLUETOOTH)
|| mDeviceLimit == 0 || isHiddenForZone()) {
return null;
}
QCList.Builder listBuilder = new QCList.Builder();
if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) {
listBuilder.addRow(new QCRow.Builder()
.setIcon(Icon.createWithResource(getContext(),
R.drawable.ic_settings_bluetooth_disabled))
.setTitle(getContext().getString(R.string.qc_bluetooth_off_devices_info))
.build());
return listBuilder.build();
}
Collection<CachedBluetoothDevice> cachedDevices =
mBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy();
//TODO(b/198339129): Use BluetoothDeviceFilter.BONDED_DEVICE_FILTER
Set<BluetoothDevice> bondedDevices = mBluetoothManager.getBluetoothAdapter()
.getBondedDevices();
List<CachedBluetoothDevice> filteredDevices = new ArrayList<>();
for (CachedBluetoothDevice cachedDevice : cachedDevices) {
if (bondedDevices != null && bondedDevices.contains(cachedDevice.getDevice())) {
filteredDevices.add(cachedDevice);
}
}
filteredDevices.sort(Comparator.naturalOrder());
if (filteredDevices.isEmpty()) {
listBuilder.addRow(new QCRow.Builder()
.setIcon(Icon.createWithResource(getContext(),
R.drawable.ic_add))
.setTitle(getContext().getString(R.string.qc_bluetooth_on_no_devices_info))
.build());
return listBuilder.build();
}
int i = 0;
int deviceLimit = mDeviceLimit >= 0 ? Math.min(mDeviceLimit, filteredDevices.size())
: filteredDevices.size();
for (int j = 0; j < deviceLimit; j++) {
CachedBluetoothDevice cachedDevice = filteredDevices.get(j);
if (mShowDevicesWithoutNames || cachedDevice.hasHumanReadableName()) {
listBuilder.addRow(new QCRow.Builder()
.setTitle(cachedDevice.getName())
.setSubtitle(cachedDevice.getCarConnectionSummary(/* shortSummary= */ true))
.setIcon(Icon.createWithResource(getContext(), getIconRes(cachedDevice)))
.addEndItem(createBluetoothButton(cachedDevice, i++))
.addEndItem(createPhoneButton(cachedDevice, i++))
.addEndItem(createMediaButton(cachedDevice, i++))
.build()
);
}
}
return listBuilder.build();
}
@Override
Uri getUri() {
return SettingsQCRegistry.PAIRED_BLUETOOTH_DEVICES_URI;
}
@Override
void onNotifyChange(Intent intent) {
String deviceKey = intent.getStringExtra(EXTRA_DEVICE_KEY);
if (deviceKey == null) {
return;
}
CachedBluetoothDevice device = null;
Collection<CachedBluetoothDevice> cachedDevices =
mBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy();
for (CachedBluetoothDevice cachedDevice : cachedDevices) {
if (cachedDevice.getAddress().equals(deviceKey)) {
device = cachedDevice;
break;
}
}
if (device == null) {
return;
}
String buttonType = intent.getStringExtra(EXTRA_BUTTON_TYPE);
boolean newState = intent.getBooleanExtra(QC_ACTION_TOGGLE_STATE, true);
if (BLUETOOTH_BUTTON.equals(buttonType)) {
if (newState) {
LocalBluetoothProfile phoneProfile = getProfile(device,
BluetoothProfile.HEADSET_CLIENT);
LocalBluetoothProfile mediaProfile = getProfile(device, BluetoothProfile.A2DP_SINK);
// If trying to connect and both phone and media are disabled, connecting will
// always fail. In this case force both profiles on.
if (phoneProfile != null && mediaProfile != null
&& !phoneProfile.isEnabled(device.getDevice())
&& !mediaProfile.isEnabled(device.getDevice())) {
phoneProfile.setEnabled(device.getDevice(), true);
mediaProfile.setEnabled(device.getDevice(), true);
}
device.connect();
} else if (device.isConnected()) {
device.disconnect();
}
} else if (PHONE_BUTTON.equals(buttonType)) {
LocalBluetoothProfile profile = getProfile(device, BluetoothProfile.HEADSET_CLIENT);
if (profile != null) {
profile.setEnabled(device.getDevice(), newState);
}
} else if (MEDIA_BUTTON.equals(buttonType)) {
LocalBluetoothProfile profile = getProfile(device, BluetoothProfile.A2DP_SINK);
if (profile != null) {
profile.setEnabled(device.getDevice(), newState);
}
} else {
LOG.d("Unknown button type: " + buttonType);
}
}
@Override
Class getBackgroundWorkerClass() {
return PairedBluetoothDevicesWorker.class;
}
@DrawableRes
private int getIconRes(CachedBluetoothDevice device) {
BluetoothClass btClass = device.getBtClass();
if (btClass != null) {
switch (btClass.getMajorDeviceClass()) {
case BluetoothClass.Device.Major.COMPUTER:
return com.android.internal.R.drawable.ic_bt_laptop;
case BluetoothClass.Device.Major.PHONE:
return com.android.internal.R.drawable.ic_phone;
case BluetoothClass.Device.Major.PERIPHERAL:
return HidProfile.getHidClassDrawable(btClass);
case BluetoothClass.Device.Major.IMAGING:
return com.android.internal.R.drawable.ic_settings_print;
default:
// unrecognized device class; continue
}
}
List<LocalBluetoothProfile> profiles = device.getProfiles();
for (LocalBluetoothProfile profile : profiles) {
int resId = profile.getDrawableResource(btClass);
if (resId != 0) {
return resId;
}
}
if (btClass != null) {
if (btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
return com.android.internal.R.drawable.ic_bt_headset_hfp;
}
if (btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
return com.android.internal.R.drawable.ic_bt_headphones_a2dp;
}
}
return com.android.internal.R.drawable.ic_settings_bluetooth;
}
private QCActionItem createBluetoothButton(CachedBluetoothDevice device, int requestCode) {
return createBluetoothDeviceToggle(device, requestCode, BLUETOOTH_BUTTON,
Icon.createWithResource(getContext(), R.drawable.ic_qc_bluetooth),
getContext().getString(
R.string.bluetooth_bonded_bluetooth_toggle_content_description),
true, !device.isBusy(), false, device.isConnected());
}
private QCActionItem createPhoneButton(CachedBluetoothDevice device, int requestCode) {
BluetoothProfileToggleState phoneState = getBluetoothProfileToggleState(device,
BluetoothProfile.HEADSET_CLIENT);
int iconRes = phoneState.mIsAvailable ? R.drawable.ic_qc_bluetooth_phone
: R.drawable.ic_qc_bluetooth_phone_unavailable;
return createBluetoothDeviceToggle(device, requestCode, PHONE_BUTTON,
Icon.createWithResource(getContext(), iconRes),
getContext().getString(R.string.bluetooth_bonded_phone_toggle_content_description),
phoneState.mIsAvailable, phoneState.mIsEnabled,
phoneState.mIsClickableWhileDisabled, phoneState.mIsChecked);
}
private QCActionItem createMediaButton(CachedBluetoothDevice device, int requestCode) {
BluetoothProfileToggleState mediaState = getBluetoothProfileToggleState(device,
BluetoothProfile.A2DP_SINK);
int iconRes = mediaState.mIsAvailable ? R.drawable.ic_qc_bluetooth_media
: R.drawable.ic_qc_bluetooth_media_unavailable;
return createBluetoothDeviceToggle(device, requestCode, MEDIA_BUTTON,
Icon.createWithResource(getContext(), iconRes),
getContext().getString(R.string.bluetooth_bonded_media_toggle_content_description),
mediaState.mIsAvailable, mediaState.mIsEnabled,
mediaState.mIsClickableWhileDisabled, mediaState.mIsChecked);
}
private QCActionItem createBluetoothDeviceToggle(CachedBluetoothDevice device, int requestCode,
String buttonType, Icon icon, String contentDescription, boolean available,
boolean enabled,
boolean clickableWhileDisabled, boolean checked) {
Bundle extras = new Bundle();
extras.putString(EXTRA_BUTTON_TYPE, buttonType);
extras.putString(EXTRA_DEVICE_KEY, device.getAddress());
PendingIntent action = getBroadcastIntent(extras, requestCode);
boolean isReadOnlyForZone = isReadOnlyForZone();
PendingIntent disabledPendingIntent = isReadOnlyForZone
? QCUtils.getDisabledToastBroadcastIntent(getContext())
: getActionDisabledDialogIntent(getContext(), DISALLOW_CONFIG_BLUETOOTH);
return new QCActionItem.Builder(QC_TYPE_ACTION_TOGGLE)
.setAvailable(available)
.setChecked(checked)
.setEnabled(enabled && isWritableForZone())
.setClickableWhileDisabled(clickableWhileDisabled | isReadOnlyForZone)
.setAction(action)
.setDisabledClickAction(disabledPendingIntent)
.setIcon(icon)
.setContentDescription(contentDescription)
.build();
}
private LocalBluetoothProfile getProfile(CachedBluetoothDevice device, int profileId) {
for (LocalBluetoothProfile profile : device.getProfiles()) {
if (profile.getProfileId() == profileId) {
return profile;
}
}
return null;
}
private BluetoothProfileToggleState getBluetoothProfileToggleState(CachedBluetoothDevice device,
int profileId) {
LocalBluetoothProfile profile = getProfile(device, profileId);
if (!device.isConnected() || profile == null) {
return new BluetoothProfileToggleState(false, false, false, false);
}
boolean hasUmRestrictions = EnterpriseUtils.hasUserRestrictionByUm(getContext(),
DISALLOW_CONFIG_BLUETOOTH);
boolean hasDpmRestrictions = EnterpriseUtils.hasUserRestrictionByDpm(getContext(),
DISALLOW_CONFIG_BLUETOOTH);
return new BluetoothProfileToggleState(true, !hasDpmRestrictions && !hasUmRestrictions
&& !device.isBusy(), hasDpmRestrictions, profile.isEnabled(device.getDevice()));
}
private static class BluetoothProfileToggleState {
final boolean mIsAvailable;
final boolean mIsEnabled;
final boolean mIsClickableWhileDisabled;
final boolean mIsChecked;
BluetoothProfileToggleState(boolean isAvailable, boolean isEnabled,
boolean isClickableWhileDisabled, boolean isChecked) {
mIsAvailable = isAvailable;
mIsEnabled = isEnabled;
mIsClickableWhileDisabled = isClickableWhileDisabled;
mIsChecked = isChecked;
}
}
}