1 /* 2 * Copyright (C) 2024 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.systemui.accessibility.hearingaid; 18 19 import static java.util.Collections.emptyList; 20 21 import android.bluetooth.BluetoothCsipSetCoordinator; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothHapClient; 24 import android.bluetooth.BluetoothHapPresetInfo; 25 import android.util.Log; 26 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 30 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 31 import com.android.settingslib.bluetooth.HapClientProfile; 32 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 33 import com.android.settingslib.utils.ThreadUtils; 34 35 import java.util.ArrayList; 36 import java.util.List; 37 38 /** 39 * The controller of handling hearing device preset with Bluetooth Hearing Access Profile(HAP). 40 */ 41 public class HearingDevicesPresetsController implements 42 LocalBluetoothProfileManager.ServiceListener, BluetoothHapClient.Callback { 43 44 private static final String TAG = "HearingDevicesPresetsController"; 45 private static final boolean DEBUG = true; 46 47 private final LocalBluetoothProfileManager mProfileManager; 48 private final HapClientProfile mHapClientProfile; 49 private final PresetCallback mPresetCallback; 50 51 private CachedBluetoothDevice mDevice; 52 private List<BluetoothHapPresetInfo> mPresetInfos = new ArrayList<>(); 53 private int mActivePresetIndex = BluetoothHapClient.PRESET_INDEX_UNAVAILABLE; 54 private int mSelectedPresetIndex; 55 HearingDevicesPresetsController(@onNull LocalBluetoothProfileManager profileManager, @Nullable PresetCallback presetCallback)56 public HearingDevicesPresetsController(@NonNull LocalBluetoothProfileManager profileManager, 57 @Nullable PresetCallback presetCallback) { 58 mProfileManager = profileManager; 59 mHapClientProfile = mProfileManager.getHapClientProfile(); 60 mPresetCallback = presetCallback; 61 } 62 63 @Override onServiceConnected()64 public void onServiceConnected() { 65 if (mHapClientProfile != null && mHapClientProfile.isProfileReady()) { 66 mProfileManager.removeServiceListener(this); 67 registerHapCallback(); 68 refreshPresetInfo(); 69 } 70 } 71 72 @Override onServiceDisconnected()73 public void onServiceDisconnected() { 74 // Do nothing 75 } 76 77 @Override onPresetSelected(@onNull BluetoothDevice device, int presetIndex, int reason)78 public void onPresetSelected(@NonNull BluetoothDevice device, int presetIndex, int reason) { 79 if (mDevice == null) { 80 return; 81 } 82 if (device.equals(mDevice.getDevice())) { 83 if (DEBUG) { 84 Log.d(TAG, "onPresetSelected, device: " + device.getAddress() 85 + ", presetIndex: " + presetIndex + ", reason: " + reason); 86 } 87 refreshPresetInfo(); 88 } 89 } 90 91 @Override onPresetInfoChanged(@onNull BluetoothDevice device, @NonNull List<BluetoothHapPresetInfo> presetInfoList, int reason)92 public void onPresetInfoChanged(@NonNull BluetoothDevice device, 93 @NonNull List<BluetoothHapPresetInfo> presetInfoList, int reason) { 94 if (mDevice == null) { 95 return; 96 } 97 if (device.equals(mDevice.getDevice())) { 98 if (DEBUG) { 99 Log.d(TAG, "onPresetInfoChanged, device: " + device.getAddress() 100 + ", reason: " + reason + ", infoList: " + presetInfoList); 101 } 102 refreshPresetInfo(); 103 } 104 } 105 106 @Override onPresetSelectionFailed(@onNull BluetoothDevice device, int reason)107 public void onPresetSelectionFailed(@NonNull BluetoothDevice device, int reason) { 108 if (mDevice == null) { 109 return; 110 } 111 if (device.equals(mDevice.getDevice())) { 112 Log.w(TAG, "onPresetSelectionFailed, device: " + device.getAddress() 113 + ", reason: " + reason); 114 if (mPresetCallback != null) { 115 mPresetCallback.onPresetCommandFailed(reason); 116 } 117 } 118 } 119 120 @Override onPresetSelectionForGroupFailed(int hapGroupId, int reason)121 public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) { 122 if (mDevice == null || mHapClientProfile == null) { 123 return; 124 } 125 if (hapGroupId == mHapClientProfile.getHapGroup(mDevice.getDevice())) { 126 Log.w(TAG, "onPresetSelectionForGroupFailed, group: " + hapGroupId 127 + ", reason: " + reason); 128 selectPresetIndependently(mSelectedPresetIndex); 129 } 130 } 131 132 @Override onSetPresetNameFailed(@onNull BluetoothDevice device, int reason)133 public void onSetPresetNameFailed(@NonNull BluetoothDevice device, int reason) { 134 if (mDevice == null) { 135 return; 136 } 137 if (device.equals(mDevice.getDevice())) { 138 Log.w(TAG, "onSetPresetNameFailed, device: " + device.getAddress() 139 + ", reason: " + reason); 140 if (mPresetCallback != null) { 141 mPresetCallback.onPresetCommandFailed(reason); 142 } 143 } 144 } 145 146 @Override onSetPresetNameForGroupFailed(int hapGroupId, int reason)147 public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) { 148 if (mDevice == null || mHapClientProfile == null) { 149 return; 150 } 151 if (hapGroupId == mHapClientProfile.getHapGroup(mDevice.getDevice())) { 152 Log.w(TAG, "onSetPresetNameForGroupFailed, group: " + hapGroupId 153 + ", reason: " + reason); 154 } 155 if (mPresetCallback != null) { 156 mPresetCallback.onPresetCommandFailed(reason); 157 } 158 } 159 160 /** 161 * Registers a callback to be notified about operation changed of {@link HapClientProfile}. 162 */ registerHapCallback()163 public void registerHapCallback() { 164 if (mHapClientProfile != null) { 165 if (!mHapClientProfile.isProfileReady()) { 166 mProfileManager.addServiceListener(this); 167 Log.w(TAG, "Profile is not ready yet, the callback will be registered once the " 168 + "profile is ready."); 169 return; 170 } 171 try { 172 mHapClientProfile.registerCallback(ThreadUtils.getBackgroundExecutor(), this); 173 } catch (IllegalArgumentException e) { 174 // The callback was already registered 175 Log.w(TAG, "Cannot register callback: " + e.getMessage()); 176 } 177 178 } 179 } 180 181 /** 182 * Removes a previously-added {@link HapClientProfile} callback if exist. 183 */ unregisterHapCallback()184 public void unregisterHapCallback() { 185 mProfileManager.removeServiceListener(this); 186 if (mHapClientProfile != null) { 187 try { 188 mHapClientProfile.unregisterCallback(this); 189 } catch (IllegalArgumentException e) { 190 // The callback was never registered or was already unregistered 191 Log.w(TAG, "Cannot unregister callback: " + e.getMessage()); 192 } 193 } 194 } 195 196 /** 197 * Sets the device for this controller to control the preset if it supports 198 * {@link HapClientProfile}, otherwise the device of this controller will be {@code null}. 199 * 200 * @param device the {@link CachedBluetoothDevice} set to the controller 201 */ setDevice(@ullable CachedBluetoothDevice device)202 public void setDevice(@Nullable CachedBluetoothDevice device) { 203 if (device != null && device.getProfiles().stream().anyMatch( 204 profile -> profile instanceof HapClientProfile)) { 205 mDevice = device; 206 } else { 207 mDevice = null; 208 } 209 refreshPresetInfo(); 210 } 211 212 /** 213 * Refreshes the preset info of {@code mDevice}. If the preset info list or the active preset 214 * index is updated, the {@link PresetCallback#onPresetInfoUpdated(List, int)} will be called 215 * to notify the change. 216 * 217 * <b>Note:</b> If {@code mDevice} is null, the cached preset info and active preset index will 218 * be reset to empty list and {@code BluetoothHapClient.PRESET_INDEX_UNAVAILABLE} respectively. 219 */ refreshPresetInfo()220 public void refreshPresetInfo() { 221 List<BluetoothHapPresetInfo> updatedInfos = new ArrayList<>(); 222 int updatedActiveIndex = BluetoothHapClient.PRESET_INDEX_UNAVAILABLE; 223 if (mHapClientProfile != null && mDevice != null) { 224 updatedInfos = mHapClientProfile.getAllPresetInfo(mDevice.getDevice()).stream().filter( 225 BluetoothHapPresetInfo::isAvailable).toList(); 226 updatedActiveIndex = mHapClientProfile.getActivePresetIndex(mDevice.getDevice()); 227 } 228 final boolean infoUpdated = !mPresetInfos.equals(updatedInfos); 229 final boolean activeIndexUpdated = mActivePresetIndex != updatedActiveIndex; 230 mPresetInfos = updatedInfos; 231 mActivePresetIndex = updatedActiveIndex; 232 if (infoUpdated || activeIndexUpdated) { 233 if (mPresetCallback != null) { 234 mPresetCallback.onPresetInfoUpdated(mPresetInfos, mActivePresetIndex); 235 } 236 } 237 } 238 239 /** 240 * @return if the preset control is available. The preset control is available only 241 * when the {@code mDevice} supports HAP and the retrieved preset info list is not empty. 242 */ isPresetControlAvailable()243 public boolean isPresetControlAvailable() { 244 boolean deviceValid = mDevice != null && mDevice.isConnectedHapClientDevice(); 245 boolean hasPreset = mPresetInfos != null && !mPresetInfos.isEmpty(); 246 return deviceValid && hasPreset; 247 } 248 249 /** 250 * @return a list of {@link BluetoothHapPresetInfo} retrieved from {@code mDevice} 251 */ getAllPresetInfo()252 public List<BluetoothHapPresetInfo> getAllPresetInfo() { 253 if (mDevice == null || mHapClientProfile == null) { 254 return emptyList(); 255 } 256 return mPresetInfos; 257 } 258 259 /** 260 * Gets the currently active preset of {@code mDevice}. 261 * 262 * @return active preset index 263 */ getActivePresetIndex()264 public int getActivePresetIndex() { 265 if (mDevice == null || mHapClientProfile == null) { 266 return BluetoothHapClient.PRESET_INDEX_UNAVAILABLE; 267 } 268 return mActivePresetIndex; 269 } 270 271 /** 272 * Selects the preset for {@code mDevice}. Performs individual or group operation according 273 * to whether the device supports synchronized presets feature or not. 274 * 275 * @param presetIndex an index of one of the available presets 276 */ selectPreset(int presetIndex)277 public void selectPreset(int presetIndex) { 278 if (mDevice == null || mHapClientProfile == null) { 279 return; 280 } 281 mSelectedPresetIndex = presetIndex; 282 boolean supportSynchronizedPresets = mHapClientProfile.supportsSynchronizedPresets( 283 mDevice.getDevice()); 284 int hapGroupId = mHapClientProfile.getHapGroup(mDevice.getDevice()); 285 if (supportSynchronizedPresets) { 286 if (hapGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 287 selectPresetSynchronously(hapGroupId, presetIndex); 288 } else { 289 Log.w(TAG, "supportSynchronizedPresets but hapGroupId is invalid."); 290 selectPresetIndependently(presetIndex); 291 } 292 } else { 293 selectPresetIndependently(presetIndex); 294 } 295 } 296 selectPresetSynchronously(int groupId, int presetIndex)297 private void selectPresetSynchronously(int groupId, int presetIndex) { 298 if (mDevice == null || mHapClientProfile == null) { 299 return; 300 } 301 if (DEBUG) { 302 Log.d(TAG, "selectPresetSynchronously" 303 + ", presetIndex: " + presetIndex 304 + ", groupId: " + groupId 305 + ", device: " + mDevice.getAddress()); 306 } 307 mHapClientProfile.selectPresetForGroup(groupId, presetIndex); 308 } 309 selectPresetIndependently(int presetIndex)310 private void selectPresetIndependently(int presetIndex) { 311 if (mDevice == null || mHapClientProfile == null) { 312 return; 313 } 314 if (DEBUG) { 315 Log.d(TAG, "selectPresetIndependently" 316 + ", presetIndex: " + presetIndex 317 + ", device: " + mDevice.getAddress()); 318 } 319 mHapClientProfile.selectPreset(mDevice.getDevice(), presetIndex); 320 final CachedBluetoothDevice subDevice = mDevice.getSubDevice(); 321 if (subDevice != null) { 322 if (DEBUG) { 323 Log.d(TAG, "selectPreset for subDevice, device: " + subDevice); 324 } 325 mHapClientProfile.selectPreset(subDevice.getDevice(), presetIndex); 326 } 327 for (final CachedBluetoothDevice memberDevice : mDevice.getMemberDevice()) { 328 if (DEBUG) { 329 Log.d(TAG, "selectPreset for memberDevice, device: " + memberDevice); 330 } 331 mHapClientProfile.selectPreset(memberDevice.getDevice(), presetIndex); 332 } 333 } 334 335 /** 336 * Interface to provide callbacks when preset command result from {@link HapClientProfile} 337 * changed. 338 */ 339 public interface PresetCallback { 340 /** 341 * Called when preset info from {@link HapClientProfile} operation get updated. 342 * 343 * @param presetInfos all preset info of {@code mDevice} 344 * @param activePresetIndex currently active preset index of {@code mDevice} 345 */ onPresetInfoUpdated(List<BluetoothHapPresetInfo> presetInfos, int activePresetIndex)346 void onPresetInfoUpdated(List<BluetoothHapPresetInfo> presetInfos, int activePresetIndex); 347 348 /** 349 * Called when preset operation from {@link HapClientProfile} failed to handle. 350 * 351 * @param reason failure reason 352 */ onPresetCommandFailed(int reason)353 void onPresetCommandFailed(int reason); 354 } 355 } 356