1 /* 2 * Copyright 2018 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.bluetooth; 18 19 import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; 20 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothProfile; 23 import android.car.drivingstate.CarUxRestrictions; 24 import android.content.Context; 25 import android.os.UserManager; 26 27 import androidx.annotation.VisibleForTesting; 28 import androidx.preference.PreferenceGroup; 29 30 import com.android.car.settings.R; 31 import com.android.car.settings.common.CarUxRestrictionsHelper; 32 import com.android.car.settings.common.FragmentController; 33 import com.android.car.settings.common.MultiActionPreference; 34 import com.android.car.settings.common.ToggleButtonActionItem; 35 import com.android.settingslib.bluetooth.BluetoothDeviceFilter; 36 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 37 import com.android.settingslib.bluetooth.LocalBluetoothManager; 38 import com.android.settingslib.bluetooth.LocalBluetoothProfile; 39 40 import java.util.Set; 41 42 /** 43 * Displays a list of bonded (paired) Bluetooth devices. Clicking on a device launch the device 44 * details page. Additional buttons to will connect/disconnect from the device, toggle phone calls, 45 * and toggle media audio. 46 * 47 * <p> 48 * Moreover, these buttons' availability and enable/disable status are controlled by UX restriction 49 * and user restriction. Specifically, 50 * <ul> 51 * <li>{@code BLUETOOTH_BUTTON}: always available and enabled. 52 * <li>{@code PHONE_BUTTON}: available when the device has {@code BluetoothProfile.HEADSET_CLIENT} 53 * and {@code CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP} is not set. Disabled but clickable when 54 * {@link UserManager.DISALLOW_CONFIG_BLUETOOTH} is set. 55 * <li>{@code MEDIA_BUTTON}: available when the device has {@code BluetoothProfile.A2DP_SINK} and 56 * {@code CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP} is not set; Disabled but clickable when 57 * {@link UserManager.DISALLOW_CONFIG_BLUETOOTH} is set. 58 * </ul> 59 * 60 * <p> 61 * Note: when button is disabled, it will still be shown as available. When the button is disabled 62 * because of {@link UserManager.DISALLOW_CONFIG_BLUETOOTH} is set by DevicePolicyManager, click 63 * on the button will show action disabled by admin dialog. 64 * 65 * <p> 66 * Device detail page will not be launched when UX retriction is set. It can still be launched 67 * when there is {@link UserManager.DISALLOW_CONFIG_BLUETOOTH} restriction. However, individual 68 * profile's toggle switch will be disabled - when clicked, shows action disabled by admin dialog. 69 */ 70 public class BluetoothBondedDevicesPreferenceController extends 71 BluetoothDevicesGroupPreferenceController implements 72 BluetoothDevicePreference.UpdateToggleButtonListener { 73 74 private static final MultiActionPreference.ActionItem BLUETOOTH_BUTTON = 75 MultiActionPreference.ActionItem.ACTION_ITEM1; 76 private static final MultiActionPreference.ActionItem PHONE_BUTTON = 77 MultiActionPreference.ActionItem.ACTION_ITEM2; 78 private static final MultiActionPreference.ActionItem MEDIA_BUTTON = 79 MultiActionPreference.ActionItem.ACTION_ITEM3; 80 81 private final BluetoothDeviceFilter.Filter mBondedDeviceTypeFilter = 82 new BondedDeviceTypeFilter(); 83 private boolean mShowDeviceDetails = true; 84 private boolean mHasUxRestriction; 85 BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)86 public BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey, 87 FragmentController fragmentController, CarUxRestrictions uxRestrictions) { 88 super(context, preferenceKey, fragmentController, uxRestrictions); 89 } 90 91 @VisibleForTesting BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions, LocalBluetoothManager localBluetoothManager, UserManager userManager)92 BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey, 93 FragmentController fragmentController, CarUxRestrictions uxRestrictions, 94 LocalBluetoothManager localBluetoothManager, UserManager userManager) { 95 super(context, preferenceKey, fragmentController, uxRestrictions, localBluetoothManager, 96 userManager); 97 } 98 99 @Override getDeviceFilter()100 protected BluetoothDeviceFilter.Filter getDeviceFilter() { 101 return mBondedDeviceTypeFilter; 102 } 103 104 @Override createDevicePreference(CachedBluetoothDevice cachedDevice)105 protected BluetoothDevicePreference createDevicePreference(CachedBluetoothDevice cachedDevice) { 106 BluetoothDevicePreference pref = super.createDevicePreference(cachedDevice); 107 ToggleButtonActionItem bluetoothItem = pref.getActionItem(BLUETOOTH_BUTTON); 108 ToggleButtonActionItem phoneItem = pref.getActionItem(PHONE_BUTTON); 109 ToggleButtonActionItem mediaItem = pref.getActionItem(MEDIA_BUTTON); 110 111 bluetoothItem.setVisible(true); 112 phoneItem.setVisible(true); 113 mediaItem.setVisible(true); 114 115 bluetoothItem.setContentDescription(getContext(), 116 R.string.bluetooth_bonded_bluetooth_toggle_content_description); 117 phoneItem.setContentDescription(getContext(), 118 R.string.bluetooth_bonded_phone_toggle_content_description); 119 mediaItem.setContentDescription(getContext(), 120 R.string.bluetooth_bonded_media_toggle_content_description); 121 122 pref.setToggleButtonUpdateListener(this); 123 mHasUxRestriction = hasNoSetupUxRestriction(); 124 setButtonsCheckedAndListeners(pref); 125 return pref; 126 } 127 128 @Override onDeviceClicked(CachedBluetoothDevice cachedDevice)129 protected void onDeviceClicked(CachedBluetoothDevice cachedDevice) { 130 if (mShowDeviceDetails) { 131 getFragmentController().launchFragment( 132 BluetoothDeviceDetailsFragment.newInstance(cachedDevice)); 133 } 134 } 135 136 @Override onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState)137 public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { 138 refreshUi(); 139 } 140 141 @Override updateState(PreferenceGroup preferenceGroup)142 protected void updateState(PreferenceGroup preferenceGroup) { 143 super.updateState(preferenceGroup); 144 updateActionAvailability(preferenceGroup); 145 } 146 147 @Override updateToggleButtonState(BluetoothDevicePreference preference)148 public void updateToggleButtonState(BluetoothDevicePreference preference) { 149 updateActionAvailability(preference); 150 } 151 updateActionAvailability(PreferenceGroup group)152 private void updateActionAvailability(PreferenceGroup group) { 153 for (int i = 0; i < group.getPreferenceCount(); i++) { 154 BluetoothDevicePreference preference = 155 (BluetoothDevicePreference) group.getPreference(i); 156 updateActionAvailability(preference); 157 } 158 } 159 updateActionAvailability(BluetoothDevicePreference preference)160 private void updateActionAvailability(BluetoothDevicePreference preference) { 161 mHasUxRestriction = hasNoSetupUxRestriction(); 162 if (!mHasUxRestriction) { 163 setButtonsCheckedAndListeners(preference); 164 } else { 165 updatePhoneActionItemAvailability(preference, /* isAvailable= */ false); 166 updateMediaActionItemAvailability(preference, /* isAvailable= */ false); 167 } 168 mShowDeviceDetails = !mHasUxRestriction; 169 } 170 toggleBluetoothConnectivity(boolean connect, CachedBluetoothDevice cachedDevice)171 private void toggleBluetoothConnectivity(boolean connect, CachedBluetoothDevice cachedDevice) { 172 if (connect) { 173 cachedDevice.connect(); 174 } else if (cachedDevice.isConnected()) { 175 cachedDevice.disconnect(); 176 } 177 } 178 setButtonsCheckedAndListeners(BluetoothDevicePreference preference)179 private void setButtonsCheckedAndListeners(BluetoothDevicePreference preference) { 180 CachedBluetoothDevice cachedDevice = preference.getCachedDevice(); 181 182 // If device is currently attempting to connect/disconnect, disable further actions 183 if (cachedDevice.isBusy()) { 184 disableAllActionItems(preference); 185 // There is a case where on creation the cached device will try to automatically connect 186 // but does not report itself as busy yet. This ensures that the bluetooth button state 187 // is correct (should be checked in either connecting or disconnecting states). 188 preference.getActionItem(BLUETOOTH_BUTTON).setChecked(true); 189 return; 190 } 191 192 LocalBluetoothProfile phoneProfile = null; 193 LocalBluetoothProfile mediaProfile = null; 194 for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) { 195 if (profile.getProfileId() == BluetoothProfile.HEADSET_CLIENT) { 196 phoneProfile = profile; 197 } else if (profile.getProfileId() == BluetoothProfile.A2DP_SINK) { 198 mediaProfile = profile; 199 } 200 } 201 LocalBluetoothProfile finalPhoneProfile = phoneProfile; 202 LocalBluetoothProfile finalMediaProfile = mediaProfile; 203 boolean isConnected = cachedDevice.isConnected(); 204 205 // Setup up bluetooth button 206 updateBluetoothActionItemAvailability(preference); 207 ToggleButtonActionItem bluetoothItem = preference.getActionItem(BLUETOOTH_BUTTON); 208 bluetoothItem.setChecked(isConnected); 209 bluetoothItem.setOnClickListener( 210 isChecked -> { 211 if (cachedDevice.isBusy()) { 212 return; 213 } 214 // If trying to connect and both phone and media are disabled, connecting will 215 // always fail. In this case force both profiles on. 216 if (isChecked && finalPhoneProfile != null && finalMediaProfile != null 217 && !finalPhoneProfile.isEnabled(cachedDevice.getDevice()) 218 && !finalMediaProfile.isEnabled(cachedDevice.getDevice())) { 219 finalPhoneProfile.setEnabled(cachedDevice.getDevice(), true); 220 finalMediaProfile.setEnabled(cachedDevice.getDevice(), true); 221 } 222 toggleBluetoothConnectivity(isChecked, cachedDevice); 223 }); 224 225 if (phoneProfile == null || !isConnected || mHasUxRestriction) { 226 // Disable phone button 227 updatePhoneActionItemAvailability(preference, /* isAvailable= */ false); 228 } else { 229 // Enable phone button 230 ToggleButtonActionItem phoneItem = preference.getActionItem(PHONE_BUTTON); 231 updatePhoneActionItemAvailability(preference, /* isAvailable= */ true); 232 boolean phoneEnabled = phoneProfile.isEnabled(cachedDevice.getDevice()); 233 234 if (hasDisallowConfigRestriction()) { 235 phoneItem.setOnClickWhileDisabledListener(p -> BluetoothUtils 236 .onClickWhileDisabled(getContext(), getFragmentController())); 237 } 238 phoneItem.setOnClickListener(isChecked -> 239 finalPhoneProfile.setEnabled(cachedDevice.getDevice(), isChecked)); 240 phoneItem.setChecked(phoneEnabled); 241 } 242 243 if (mediaProfile == null || !isConnected || mHasUxRestriction) { 244 // Disable media button 245 updateMediaActionItemAvailability(preference, /* isAvailable= */ false); 246 } else { 247 // Enable media button 248 ToggleButtonActionItem mediaItem = preference.getActionItem(MEDIA_BUTTON); 249 updateMediaActionItemAvailability(preference, /* isAvailable= */ true); 250 boolean mediaEnabled = mediaProfile.isEnabled(cachedDevice.getDevice()); 251 252 if (hasDisallowConfigRestriction()) { 253 mediaItem.setOnClickWhileDisabledListener(p -> BluetoothUtils 254 .onClickWhileDisabled(getContext(), getFragmentController())); 255 } 256 mediaItem.setOnClickListener(isChecked -> 257 finalMediaProfile.setEnabled(cachedDevice.getDevice(), isChecked)); 258 mediaItem.setChecked(mediaEnabled); 259 } 260 } 261 updateBluetoothActionItemAvailability(BluetoothDevicePreference preference)262 private void updateBluetoothActionItemAvailability(BluetoothDevicePreference preference) { 263 // Run on main thread because recyclerview may still be computing layout 264 getContext().getMainExecutor().execute(() -> { 265 ToggleButtonActionItem bluetoothItem = preference.getActionItem(BLUETOOTH_BUTTON); 266 bluetoothItem.setEnabled(true); 267 bluetoothItem.setDrawable(getContext(), R.drawable.ic_bluetooth_button); 268 }); 269 } 270 updatePhoneActionItemAvailability(BluetoothDevicePreference preference, boolean isAvailable)271 private void updatePhoneActionItemAvailability(BluetoothDevicePreference preference, 272 boolean isAvailable) { 273 // Run on main thread because recyclerview may still be computing layout 274 getContext().getMainExecutor().execute(() -> { 275 ToggleButtonActionItem phoneItem = preference.getActionItem(PHONE_BUTTON); 276 phoneItem.setEnabled(isAvailable && !hasDisallowConfigRestriction()); 277 phoneItem.setDrawable(getContext(), isAvailable 278 ? R.drawable.ic_bluetooth_phone : R.drawable.ic_bluetooth_phone_unavailable); 279 phoneItem.setRestricted(!isAvailable && mHasUxRestriction); 280 }); 281 } 282 updateMediaActionItemAvailability(BluetoothDevicePreference preference, boolean isAvailable)283 private void updateMediaActionItemAvailability(BluetoothDevicePreference preference, 284 boolean isAvailable) { 285 // Run on main thread because recyclerview may still be computing layout 286 getContext().getMainExecutor().execute(() -> { 287 ToggleButtonActionItem mediaItem = preference.getActionItem(MEDIA_BUTTON); 288 mediaItem.setEnabled(isAvailable && !hasDisallowConfigRestriction()); 289 mediaItem.setDrawable(getContext(), isAvailable 290 ? R.drawable.ic_bluetooth_media : R.drawable.ic_bluetooth_media_unavailable); 291 mediaItem.setRestricted(!isAvailable && mHasUxRestriction); 292 }); 293 } 294 disableAllActionItems(BluetoothDevicePreference preference)295 private void disableAllActionItems(BluetoothDevicePreference preference) { 296 // Run on main thread because recyclerview may still be computing layout 297 getContext().getMainExecutor().execute(() -> { 298 preference.getActionItem(BLUETOOTH_BUTTON).setEnabled(false); 299 preference.getActionItem(PHONE_BUTTON).setEnabled(false); 300 preference.getActionItem(MEDIA_BUTTON).setEnabled(false); 301 }); 302 } 303 hasDisallowConfigRestriction()304 private boolean hasDisallowConfigRestriction() { 305 return getUserManager().hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH); 306 } 307 hasNoSetupUxRestriction()308 private boolean hasNoSetupUxRestriction() { 309 return CarUxRestrictionsHelper.isNoSetup(getUxRestrictions()); 310 } 311 312 /** Filter that matches only bonded devices with specific device types. */ 313 //TODO(b/198339129): Use BluetoothDeviceFilter.BONDED_DEVICE_FILTER 314 private class BondedDeviceTypeFilter implements BluetoothDeviceFilter.Filter { 315 @Override matches(BluetoothDevice device)316 public boolean matches(BluetoothDevice device) { 317 Set<BluetoothDevice> bondedDevices = mBluetoothManager.getBluetoothAdapter() 318 .getBondedDevices(); 319 return bondedDevices != null && bondedDevices.contains(device); 320 } 321 } 322 } 323