/* * 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 cachedDevices = mBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy(); //TODO(b/198339129): Use BluetoothDeviceFilter.BONDED_DEVICE_FILTER Set bondedDevices = mBluetoothManager.getBluetoothAdapter() .getBondedDevices(); List 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 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 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; } } }