1 /* 2 * Copyright (C) 2021 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 */ 16 17 package com.android.car.settings.qc; 18 19 import static android.os.UserManager.DISALLOW_BLUETOOTH; 20 21 import static com.android.car.qc.QCItem.QC_ACTION_TOGGLE_STATE; 22 import static com.android.car.qc.QCItem.QC_TYPE_ACTION_TOGGLE; 23 24 import android.app.PendingIntent; 25 import android.bluetooth.BluetoothAdapter; 26 import android.bluetooth.BluetoothClass; 27 import android.bluetooth.BluetoothDevice; 28 import android.bluetooth.BluetoothProfile; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.pm.PackageManager; 32 import android.graphics.drawable.Icon; 33 import android.net.Uri; 34 import android.os.Bundle; 35 import android.os.UserManager; 36 37 import androidx.annotation.DrawableRes; 38 import androidx.annotation.VisibleForTesting; 39 40 import com.android.car.qc.QCActionItem; 41 import com.android.car.qc.QCItem; 42 import com.android.car.qc.QCList; 43 import com.android.car.qc.QCRow; 44 import com.android.car.settings.R; 45 import com.android.car.settings.common.Logger; 46 import com.android.settingslib.bluetooth.BluetoothUtils; 47 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 48 import com.android.settingslib.bluetooth.HidProfile; 49 import com.android.settingslib.bluetooth.LocalBluetoothManager; 50 import com.android.settingslib.bluetooth.LocalBluetoothProfile; 51 52 import java.util.ArrayList; 53 import java.util.Collection; 54 import java.util.Comparator; 55 import java.util.List; 56 import java.util.Set; 57 58 /** 59 * QCItem for showing paired bluetooth devices. 60 */ 61 public class PairedBluetoothDevices extends SettingsQCItem { 62 @VisibleForTesting 63 static final String EXTRA_DEVICE_KEY = "BT_EXTRA_DEVICE_KEY"; 64 @VisibleForTesting 65 static final String EXTRA_BUTTON_TYPE = "BT_EXTRA_BUTTON_TYPE"; 66 @VisibleForTesting 67 static final String BLUETOOTH_BUTTON = "BLUETOOTH_BUTTON"; 68 @VisibleForTesting 69 static final String PHONE_BUTTON = "PHONE_BUTTON"; 70 @VisibleForTesting 71 static final String MEDIA_BUTTON = "MEDIA_BUTTON"; 72 private static final Logger LOG = new Logger(PairedBluetoothDevices.class); 73 74 private final LocalBluetoothManager mBluetoothManager; 75 private final UserManager mUserManager; 76 private final int mDeviceLimit; 77 PairedBluetoothDevices(Context context)78 public PairedBluetoothDevices(Context context) { 79 super(context); 80 mBluetoothManager = LocalBluetoothManager.getInstance(context, /* onInitCallback= */ null); 81 mUserManager = UserManager.get(context); 82 mDeviceLimit = context.getResources().getInteger( 83 R.integer.config_qc_bluetooth_device_limit); 84 } 85 86 @Override getQCItem()87 QCItem getQCItem() { 88 if (!getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) 89 || mUserManager.hasUserRestriction(DISALLOW_BLUETOOTH) 90 || mDeviceLimit == 0) { 91 return null; 92 } 93 94 QCList.Builder listBuilder = new QCList.Builder(); 95 96 if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { 97 listBuilder.addRow(new QCRow.Builder() 98 .setIcon(Icon.createWithResource(getContext(), 99 R.drawable.ic_settings_bluetooth_disabled)) 100 .setTitle(getContext().getString(R.string.qc_bluetooth_off_devices_info)) 101 .build()); 102 return listBuilder.build(); 103 } 104 105 Collection<CachedBluetoothDevice> cachedDevices = 106 mBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy(); 107 108 //TODO(b/198339129): Use BluetoothDeviceFilter.BONDED_DEVICE_FILTER 109 Set<BluetoothDevice> bondedDevices = mBluetoothManager.getBluetoothAdapter() 110 .getBondedDevices(); 111 112 List<CachedBluetoothDevice> filteredDevices = new ArrayList<>(); 113 for (CachedBluetoothDevice cachedDevice : cachedDevices) { 114 if (bondedDevices != null && bondedDevices.contains(cachedDevice.getDevice())) { 115 filteredDevices.add(cachedDevice); 116 } 117 } 118 filteredDevices.sort(Comparator.naturalOrder()); 119 120 if (filteredDevices.isEmpty()) { 121 listBuilder.addRow(new QCRow.Builder() 122 .setIcon(Icon.createWithResource(getContext(), 123 R.drawable.ic_settings_bluetooth)) 124 .setTitle(getContext().getString(R.string.qc_bluetooth_on_no_devices_info)) 125 .build()); 126 return listBuilder.build(); 127 } 128 129 int i = 0; 130 int deviceLimit = mDeviceLimit >= 0 ? Math.min(mDeviceLimit, filteredDevices.size()) 131 : filteredDevices.size(); 132 for (int j = 0; j < deviceLimit; j++) { 133 CachedBluetoothDevice cachedDevice = filteredDevices.get(j); 134 listBuilder.addRow(new QCRow.Builder() 135 .setTitle(cachedDevice.getName()) 136 .setSubtitle(getSubtitle(cachedDevice)) 137 .setIcon(Icon.createWithResource(getContext(), getIconRes(cachedDevice))) 138 .addEndItem(createBluetoothButton(cachedDevice, i++)) 139 .addEndItem(createPhoneButton(cachedDevice, i++)) 140 .addEndItem(createMediaButton(cachedDevice, i++)) 141 .build() 142 ); 143 } 144 145 return listBuilder.build(); 146 } 147 148 @Override getUri()149 Uri getUri() { 150 return SettingsQCRegistry.PAIRED_BLUETOOTH_DEVICES_URI; 151 } 152 153 154 @Override onNotifyChange(Intent intent)155 void onNotifyChange(Intent intent) { 156 String deviceKey = intent.getStringExtra(EXTRA_DEVICE_KEY); 157 if (deviceKey == null) { 158 return; 159 } 160 CachedBluetoothDevice device = null; 161 Collection<CachedBluetoothDevice> cachedDevices = 162 mBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy(); 163 for (CachedBluetoothDevice cachedDevice : cachedDevices) { 164 if (cachedDevice.getAddress().equals(deviceKey)) { 165 device = cachedDevice; 166 break; 167 } 168 } 169 if (device == null) { 170 return; 171 } 172 173 String buttonType = intent.getStringExtra(EXTRA_BUTTON_TYPE); 174 boolean newState = intent.getBooleanExtra(QC_ACTION_TOGGLE_STATE, true); 175 if (BLUETOOTH_BUTTON.equals(buttonType)) { 176 if (newState) { 177 LocalBluetoothProfile phoneProfile = getProfile(device, 178 BluetoothProfile.HEADSET_CLIENT); 179 LocalBluetoothProfile mediaProfile = getProfile(device, BluetoothProfile.A2DP_SINK); 180 // If trying to connect and both phone and media are disabled, connecting will 181 // always fail. In this case force both profiles on. 182 if (phoneProfile != null && mediaProfile != null 183 && !phoneProfile.isEnabled(device.getDevice()) 184 && !mediaProfile.isEnabled(device.getDevice())) { 185 phoneProfile.setEnabled(device.getDevice(), true); 186 mediaProfile.setEnabled(device.getDevice(), true); 187 } 188 device.connect(); 189 } else if (device.isConnected()) { 190 device.disconnect(); 191 } 192 } else if (PHONE_BUTTON.equals(buttonType)) { 193 LocalBluetoothProfile profile = getProfile(device, BluetoothProfile.HEADSET_CLIENT); 194 if (profile != null) { 195 profile.setEnabled(device.getDevice(), newState); 196 } 197 } else if (MEDIA_BUTTON.equals(buttonType)) { 198 LocalBluetoothProfile profile = getProfile(device, BluetoothProfile.A2DP_SINK); 199 if (profile != null) { 200 profile.setEnabled(device.getDevice(), newState); 201 } 202 } else { 203 LOG.d("Unknown button type: " + buttonType); 204 } 205 } 206 207 @Override getBackgroundWorkerClass()208 Class getBackgroundWorkerClass() { 209 return PairedBluetoothDevicesWorker.class; 210 } 211 getSubtitle(CachedBluetoothDevice device)212 private String getSubtitle(CachedBluetoothDevice device) { 213 if (device.isConnected()) { 214 return getContext().getString(BluetoothUtils 215 .getConnectionStateSummary(BluetoothProfile.STATE_CONNECTED), 216 /* appended text= */ ""); 217 } 218 return device.getCarConnectionSummary(); 219 } 220 221 @DrawableRes getIconRes(CachedBluetoothDevice device)222 private int getIconRes(CachedBluetoothDevice device) { 223 BluetoothClass btClass = device.getBtClass(); 224 if (btClass != null) { 225 switch (btClass.getMajorDeviceClass()) { 226 case BluetoothClass.Device.Major.COMPUTER: 227 return com.android.internal.R.drawable.ic_bt_laptop; 228 case BluetoothClass.Device.Major.PHONE: 229 return com.android.internal.R.drawable.ic_phone; 230 case BluetoothClass.Device.Major.PERIPHERAL: 231 return HidProfile.getHidClassDrawable(btClass); 232 case BluetoothClass.Device.Major.IMAGING: 233 return com.android.internal.R.drawable.ic_settings_print; 234 default: 235 // unrecognized device class; continue 236 } 237 } 238 239 List<LocalBluetoothProfile> profiles = device.getProfiles(); 240 for (LocalBluetoothProfile profile : profiles) { 241 int resId = profile.getDrawableResource(btClass); 242 if (resId != 0) { 243 return resId; 244 } 245 } 246 if (btClass != null) { 247 if (btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) { 248 return com.android.internal.R.drawable.ic_bt_headset_hfp; 249 } 250 if (btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) { 251 return com.android.internal.R.drawable.ic_bt_headphones_a2dp; 252 } 253 } 254 return com.android.internal.R.drawable.ic_settings_bluetooth; 255 } 256 createBluetoothButton(CachedBluetoothDevice device, int requestCode)257 private QCActionItem createBluetoothButton(CachedBluetoothDevice device, int requestCode) { 258 return createBluetoothDeviceToggle(device, requestCode, BLUETOOTH_BUTTON, 259 Icon.createWithResource(getContext(), R.drawable.ic_qc_bluetooth), true, 260 !device.isBusy(), device.isConnected()); 261 } 262 createPhoneButton(CachedBluetoothDevice device, int requestCode)263 private QCActionItem createPhoneButton(CachedBluetoothDevice device, int requestCode) { 264 BluetoothProfileToggleState phoneState = getBluetoothProfileToggleState(device, 265 BluetoothProfile.HEADSET_CLIENT); 266 int iconRes = phoneState.mIsAvailable ? R.drawable.ic_qc_bluetooth_phone 267 : R.drawable.ic_qc_bluetooth_phone_unavailable; 268 return createBluetoothDeviceToggle(device, requestCode, PHONE_BUTTON, 269 Icon.createWithResource(getContext(), iconRes), 270 phoneState.mIsAvailable, phoneState.mIsEnabled, phoneState.mIsChecked); 271 } 272 createMediaButton(CachedBluetoothDevice device, int requestCode)273 private QCActionItem createMediaButton(CachedBluetoothDevice device, int requestCode) { 274 BluetoothProfileToggleState mediaState = getBluetoothProfileToggleState(device, 275 BluetoothProfile.A2DP_SINK); 276 int iconRes = mediaState.mIsAvailable ? R.drawable.ic_qc_bluetooth_media 277 : R.drawable.ic_qc_bluetooth_media_unavailable; 278 return createBluetoothDeviceToggle(device, requestCode, MEDIA_BUTTON, 279 Icon.createWithResource(getContext(), iconRes), 280 mediaState.mIsAvailable, mediaState.mIsEnabled, mediaState.mIsChecked); 281 } 282 createBluetoothDeviceToggle(CachedBluetoothDevice device, int requestCode, String buttonType, Icon icon, boolean available, boolean enabled, boolean checked)283 private QCActionItem createBluetoothDeviceToggle(CachedBluetoothDevice device, int requestCode, 284 String buttonType, Icon icon, boolean available, boolean enabled, boolean checked) { 285 Bundle extras = new Bundle(); 286 extras.putString(EXTRA_BUTTON_TYPE, buttonType); 287 extras.putString(EXTRA_DEVICE_KEY, device.getAddress()); 288 PendingIntent action = getBroadcastIntent(extras, requestCode); 289 290 return new QCActionItem.Builder(QC_TYPE_ACTION_TOGGLE) 291 .setAvailable(available) 292 .setChecked(checked) 293 .setEnabled(enabled) 294 .setAction(action) 295 .setIcon(icon) 296 .build(); 297 } 298 getProfile(CachedBluetoothDevice device, int profileId)299 private LocalBluetoothProfile getProfile(CachedBluetoothDevice device, int profileId) { 300 for (LocalBluetoothProfile profile : device.getProfiles()) { 301 if (profile.getProfileId() == profileId) { 302 return profile; 303 } 304 } 305 return null; 306 } 307 getBluetoothProfileToggleState(CachedBluetoothDevice device, int profileId)308 private BluetoothProfileToggleState getBluetoothProfileToggleState(CachedBluetoothDevice device, 309 int profileId) { 310 LocalBluetoothProfile profile = getProfile(device, profileId); 311 if (!device.isConnected() || profile == null) { 312 return new BluetoothProfileToggleState(false, false, false); 313 } 314 return new BluetoothProfileToggleState(true, !device.isBusy(), 315 profile.isEnabled(device.getDevice())); 316 } 317 318 private static class BluetoothProfileToggleState { 319 final boolean mIsAvailable; 320 final boolean mIsEnabled; 321 final boolean mIsChecked; 322 BluetoothProfileToggleState(boolean isAvailable, boolean isEnabled, boolean isChecked)323 BluetoothProfileToggleState(boolean isAvailable, boolean isEnabled, boolean isChecked) { 324 mIsAvailable = isAvailable; 325 mIsEnabled = isEnabled; 326 mIsChecked = isChecked; 327 } 328 } 329 } 330