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 /** 44 * Displays a list of bonded (paired) Bluetooth devices. Clicking on a device launch the device 45 * details page. Additional buttons to will connect/disconnect from the device, toggle phone calls, 46 * and toggle media audio. 47 */ 48 public class BluetoothBondedDevicesPreferenceController extends 49 BluetoothDevicesGroupPreferenceController implements 50 BluetoothDevicePreference.UpdateToggleButtonListener { 51 52 private static final MultiActionPreference.ActionItem BLUETOOTH_BUTTON = 53 MultiActionPreference.ActionItem.ACTION_ITEM1; 54 private static final MultiActionPreference.ActionItem PHONE_BUTTON = 55 MultiActionPreference.ActionItem.ACTION_ITEM2; 56 private static final MultiActionPreference.ActionItem MEDIA_BUTTON = 57 MultiActionPreference.ActionItem.ACTION_ITEM3; 58 59 private final BluetoothDeviceFilter.Filter mBondedDeviceTypeFilter = 60 new BondedDeviceTypeFilter(); 61 private boolean mShowDeviceDetails = true; 62 BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)63 public BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey, 64 FragmentController fragmentController, CarUxRestrictions uxRestrictions) { 65 super(context, preferenceKey, fragmentController, uxRestrictions); 66 } 67 68 @VisibleForTesting BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions, LocalBluetoothManager localBluetoothManager, UserManager userManager)69 BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey, 70 FragmentController fragmentController, CarUxRestrictions uxRestrictions, 71 LocalBluetoothManager localBluetoothManager, UserManager userManager) { 72 super(context, preferenceKey, fragmentController, uxRestrictions, localBluetoothManager, 73 userManager); 74 } 75 76 @Override getDeviceFilter()77 protected BluetoothDeviceFilter.Filter getDeviceFilter() { 78 return mBondedDeviceTypeFilter; 79 } 80 81 @Override createDevicePreference(CachedBluetoothDevice cachedDevice)82 protected BluetoothDevicePreference createDevicePreference(CachedBluetoothDevice cachedDevice) { 83 BluetoothDevicePreference pref = super.createDevicePreference(cachedDevice); 84 pref.getActionItem(BLUETOOTH_BUTTON).setVisible(true); 85 pref.getActionItem(PHONE_BUTTON).setVisible(true); 86 pref.getActionItem(MEDIA_BUTTON).setVisible(true); 87 pref.setToggleButtonUpdateListener(this); 88 updateBluetoothActionItemAvailability(pref); 89 updateActionAvailability(pref, true); 90 91 return pref; 92 } 93 94 @Override onDeviceClicked(CachedBluetoothDevice cachedDevice)95 protected void onDeviceClicked(CachedBluetoothDevice cachedDevice) { 96 if (mShowDeviceDetails) { 97 getFragmentController().launchFragment( 98 BluetoothDeviceDetailsFragment.newInstance(cachedDevice)); 99 } 100 } 101 102 @Override onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState)103 public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { 104 refreshUi(); 105 } 106 107 @Override updateState(PreferenceGroup preferenceGroup)108 protected void updateState(PreferenceGroup preferenceGroup) { 109 super.updateState(preferenceGroup); 110 111 boolean hasUserRestriction = getUserManager() 112 .hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH); 113 updateActionAvailability(preferenceGroup, hasUserRestriction); 114 } 115 116 @Override onApplyUxRestrictions(CarUxRestrictions uxRestrictions)117 protected void onApplyUxRestrictions(CarUxRestrictions uxRestrictions) { 118 super.onApplyUxRestrictions(uxRestrictions); 119 120 if (CarUxRestrictionsHelper.isNoSetup(uxRestrictions)) { 121 updateActionAvailability(getPreference(), /* isRestricted= */ true); 122 } 123 } 124 125 @Override updateToggleButtonState(BluetoothDevicePreference preference)126 public void updateToggleButtonState(BluetoothDevicePreference preference) { 127 boolean hasUserRestriction = getUserManager() 128 .hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH); 129 updateActionAvailability(preference, hasUserRestriction); 130 } 131 updateActionAvailability(PreferenceGroup group, boolean isRestricted)132 private void updateActionAvailability(PreferenceGroup group, boolean isRestricted) { 133 for (int i = 0; i < group.getPreferenceCount(); i++) { 134 BluetoothDevicePreference preference = 135 (BluetoothDevicePreference) group.getPreference(i); 136 updateActionAvailability(preference, isRestricted); 137 } 138 } 139 updateActionAvailability(BluetoothDevicePreference preference, boolean isRestricted)140 private void updateActionAvailability(BluetoothDevicePreference preference, 141 boolean isRestricted) { 142 if (!isRestricted) { 143 setButtonsCheckedAndListeners(preference); 144 mShowDeviceDetails = true; 145 } else { 146 updatePhoneActionItemAvailability(preference, true); 147 updateMediaActionItemAvailability(preference, true); 148 mShowDeviceDetails = false; 149 } 150 } 151 toggleBluetoothConnectivity(boolean connect, CachedBluetoothDevice cachedDevice)152 private void toggleBluetoothConnectivity(boolean connect, CachedBluetoothDevice cachedDevice) { 153 if (connect) { 154 cachedDevice.connect(); 155 } else if (cachedDevice.isConnected()) { 156 cachedDevice.disconnect(); 157 } 158 } 159 setButtonsCheckedAndListeners(BluetoothDevicePreference preference)160 private void setButtonsCheckedAndListeners(BluetoothDevicePreference preference) { 161 CachedBluetoothDevice cachedDevice = preference.getCachedDevice(); 162 163 // If device is currently attempting to connect/disconnect, disable further actions 164 if (cachedDevice.isBusy()) { 165 disableAllActionItems(preference); 166 // There is a case where on creation the cached device will try to automatically connect 167 // but does not report itself as busy yet. This ensures that the bluetooth button state 168 // is correct (should be checked in either connecting or disconnecting states). 169 preference.getActionItem(BLUETOOTH_BUTTON).setChecked(true); 170 return; 171 } 172 173 LocalBluetoothProfile phoneProfile = null; 174 LocalBluetoothProfile mediaProfile = null; 175 for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) { 176 if (profile.getProfileId() == BluetoothProfile.HEADSET_CLIENT) { 177 phoneProfile = profile; 178 } else if (profile.getProfileId() == BluetoothProfile.A2DP_SINK) { 179 mediaProfile = profile; 180 } 181 } 182 LocalBluetoothProfile finalPhoneProfile = phoneProfile; 183 LocalBluetoothProfile finalMediaProfile = mediaProfile; 184 boolean isConnected = cachedDevice.isConnected(); 185 186 // Setup up bluetooth button 187 updateBluetoothActionItemAvailability(preference); 188 ToggleButtonActionItem bluetoothItem = preference.getActionItem(BLUETOOTH_BUTTON); 189 bluetoothItem.setChecked(isConnected); 190 bluetoothItem.setOnClickListener( 191 isChecked -> { 192 if (cachedDevice.isBusy()) { 193 return; 194 } 195 // If trying to connect and both phone and media are disabled, connecting will 196 // always fail. In this case force both profiles on. 197 if (isChecked && finalPhoneProfile != null && finalMediaProfile != null 198 && !finalPhoneProfile.isEnabled(cachedDevice.getDevice()) 199 && !finalMediaProfile.isEnabled(cachedDevice.getDevice())) { 200 finalPhoneProfile.setEnabled(cachedDevice.getDevice(), true); 201 finalMediaProfile.setEnabled(cachedDevice.getDevice(), true); 202 } 203 toggleBluetoothConnectivity(isChecked, cachedDevice); 204 }); 205 206 if (phoneProfile == null || !isConnected) { 207 // Disable phone button 208 updatePhoneActionItemAvailability(preference, true); 209 } else { 210 // Enable phone button 211 updatePhoneActionItemAvailability(preference, false); 212 ToggleButtonActionItem phoneItem = preference.getActionItem(PHONE_BUTTON); 213 boolean phoneEnabled = phoneProfile.isEnabled(cachedDevice.getDevice()); 214 phoneItem.setChecked(phoneEnabled); 215 phoneItem.setOnClickListener(isChecked -> 216 finalPhoneProfile.setEnabled(cachedDevice.getDevice(), isChecked)); 217 } 218 219 if (mediaProfile == null || !isConnected) { 220 // Disable media button 221 updateMediaActionItemAvailability(preference, true); 222 } else { 223 // Enable media button 224 updateMediaActionItemAvailability(preference, false); 225 ToggleButtonActionItem mediaItem = preference.getActionItem(MEDIA_BUTTON); 226 boolean mediaEnabled = mediaProfile.isEnabled(cachedDevice.getDevice()); 227 mediaItem.setChecked(mediaEnabled); 228 mediaItem.setOnClickListener(isChecked -> 229 finalMediaProfile.setEnabled(cachedDevice.getDevice(), isChecked)); 230 } 231 } 232 updateBluetoothActionItemAvailability(BluetoothDevicePreference preference)233 private void updateBluetoothActionItemAvailability(BluetoothDevicePreference preference) { 234 // Run on main thread because recyclerview may still be computing layout 235 getContext().getMainExecutor().execute(() -> { 236 ToggleButtonActionItem bluetoothItem = preference.getActionItem(BLUETOOTH_BUTTON); 237 bluetoothItem.setEnabled(true); 238 bluetoothItem.setDrawable(getContext(), R.drawable.ic_bluetooth_button); 239 }); 240 } 241 updatePhoneActionItemAvailability(BluetoothDevicePreference preference, boolean isRestricted)242 private void updatePhoneActionItemAvailability(BluetoothDevicePreference preference, 243 boolean isRestricted) { 244 // Run on main thread because recyclerview may still be computing layout 245 getContext().getMainExecutor().execute(() -> { 246 ToggleButtonActionItem phoneItem = preference.getActionItem(PHONE_BUTTON); 247 phoneItem.setEnabled(!isRestricted); 248 phoneItem.setDrawable(getContext(), isRestricted 249 ? R.drawable.ic_bluetooth_phone_unavailable : R.drawable.ic_bluetooth_phone); 250 }); 251 } 252 updateMediaActionItemAvailability(BluetoothDevicePreference preference, boolean isRestricted)253 private void updateMediaActionItemAvailability(BluetoothDevicePreference preference, 254 boolean isRestricted) { 255 // Run on main thread because recyclerview may still be computing layout 256 getContext().getMainExecutor().execute(() -> { 257 ToggleButtonActionItem mediaItem = preference.getActionItem(MEDIA_BUTTON); 258 mediaItem.setEnabled(!isRestricted); 259 mediaItem.setDrawable(getContext(), isRestricted 260 ? R.drawable.ic_bluetooth_media_unavailable : R.drawable.ic_bluetooth_media); 261 }); 262 } 263 disableAllActionItems(BluetoothDevicePreference preference)264 private void disableAllActionItems(BluetoothDevicePreference preference) { 265 // Run on main thread because recyclerview may still be computing layout 266 getContext().getMainExecutor().execute(() -> { 267 preference.getActionItem(BLUETOOTH_BUTTON).setEnabled(false); 268 preference.getActionItem(PHONE_BUTTON).setEnabled(false); 269 preference.getActionItem(MEDIA_BUTTON).setEnabled(false); 270 }); 271 } 272 273 /** Filter that matches only bonded devices with specific device types. */ 274 //TODO(b/198339129): Use BluetoothDeviceFilter.BONDED_DEVICE_FILTER 275 private class BondedDeviceTypeFilter implements BluetoothDeviceFilter.Filter { 276 @Override matches(BluetoothDevice device)277 public boolean matches(BluetoothDevice device) { 278 Set<BluetoothDevice> bondedDevices = mBluetoothManager.getBluetoothAdapter() 279 .getBondedDevices(); 280 return bondedDevices != null && bondedDevices.contains(device); 281 } 282 } 283 } 284