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