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.settings.bluetooth; 18 19 import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.KEY_HEARING_DEVICE_GROUP; 20 import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.ORDER_HEARING_AIDS_PRESETS; 21 22 import android.bluetooth.BluetoothCsipSetCoordinator; 23 import android.bluetooth.BluetoothDevice; 24 import android.bluetooth.BluetoothHapClient; 25 import android.bluetooth.BluetoothHapPresetInfo; 26 import android.content.Context; 27 import android.text.TextUtils; 28 import android.util.Log; 29 import android.widget.Toast; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.annotation.VisibleForTesting; 34 import androidx.preference.ListPreference; 35 import androidx.preference.Preference; 36 import androidx.preference.PreferenceCategory; 37 import androidx.preference.PreferenceFragmentCompat; 38 import androidx.preference.PreferenceScreen; 39 40 import com.android.settings.R; 41 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 42 import com.android.settingslib.bluetooth.HapClientProfile; 43 import com.android.settingslib.bluetooth.LocalBluetoothManager; 44 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 45 import com.android.settingslib.core.lifecycle.Lifecycle; 46 import com.android.settingslib.core.lifecycle.events.OnPause; 47 import com.android.settingslib.core.lifecycle.events.OnResume; 48 import com.android.settingslib.core.lifecycle.events.OnStart; 49 import com.android.settingslib.core.lifecycle.events.OnStop; 50 import com.android.settingslib.utils.ThreadUtils; 51 52 import java.util.List; 53 54 /** 55 * The controller of the hearing aid presets. 56 */ 57 public class BluetoothDetailsHearingAidsPresetsController extends 58 BluetoothDetailsController implements Preference.OnPreferenceChangeListener, 59 BluetoothHapClient.Callback, LocalBluetoothProfileManager.ServiceListener, 60 OnStart, OnResume, OnPause, OnStop { 61 62 private static final boolean DEBUG = true; 63 private static final String TAG = "BluetoothDetailsHearingAidsPresetsController"; 64 static final String KEY_HEARING_AIDS_PRESETS = "hearing_aids_presets"; 65 66 private final LocalBluetoothProfileManager mProfileManager; 67 private final HapClientProfile mHapClientProfile; 68 69 @Nullable 70 private ListPreference mPreference; 71 BluetoothDetailsHearingAidsPresetsController(@onNull Context context, @NonNull PreferenceFragmentCompat fragment, @NonNull LocalBluetoothManager manager, @NonNull CachedBluetoothDevice device, @NonNull Lifecycle lifecycle)72 public BluetoothDetailsHearingAidsPresetsController(@NonNull Context context, 73 @NonNull PreferenceFragmentCompat fragment, 74 @NonNull LocalBluetoothManager manager, 75 @NonNull CachedBluetoothDevice device, 76 @NonNull Lifecycle lifecycle) { 77 super(context, fragment, device, lifecycle); 78 mProfileManager = manager.getProfileManager(); 79 mHapClientProfile = mProfileManager.getHapClientProfile(); 80 } 81 82 @Override onStart()83 public void onStart() { 84 if (mHapClientProfile != null && !mHapClientProfile.isProfileReady()) { 85 mProfileManager.addServiceListener(this); 86 } 87 } 88 89 @Override onResume()90 public void onResume() { 91 registerHapCallback(); 92 super.onResume(); 93 } 94 95 @Override onPause()96 public void onPause() { 97 unregisterHapCallback(); 98 super.onPause(); 99 } 100 101 @Override onStop()102 public void onStop() { 103 mProfileManager.removeServiceListener(this); 104 } 105 106 @Override onPreferenceChange(@onNull Preference preference, @Nullable Object newValue)107 public boolean onPreferenceChange(@NonNull Preference preference, @Nullable Object newValue) { 108 if (TextUtils.equals(preference.getKey(), getPreferenceKey())) { 109 if (newValue instanceof final String value 110 && preference instanceof final ListPreference listPreference) { 111 final int index = listPreference.findIndexOfValue(value); 112 final String presetName = listPreference.getEntries()[index].toString(); 113 final int presetIndex = Integer.parseInt(value); 114 listPreference.setSummary(presetName); 115 if (DEBUG) { 116 Log.d(TAG, "onPreferenceChange" 117 + ", presetIndex: " + presetIndex 118 + ", presetName: " + presetName); 119 } 120 boolean supportSynchronizedPresets = mHapClientProfile.supportsSynchronizedPresets( 121 mCachedDevice.getDevice()); 122 int hapGroupId = mHapClientProfile.getHapGroup(mCachedDevice.getDevice()); 123 if (supportSynchronizedPresets) { 124 if (hapGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 125 selectPresetSynchronously(hapGroupId, presetIndex); 126 } else { 127 Log.w(TAG, "supportSynchronizedPresets but hapGroupId is invalid."); 128 selectPresetIndependently(presetIndex); 129 } 130 } else { 131 selectPresetIndependently(presetIndex); 132 } 133 return true; 134 } 135 } 136 return false; 137 } 138 139 @Nullable 140 @Override getPreferenceKey()141 public String getPreferenceKey() { 142 return KEY_HEARING_AIDS_PRESETS; 143 } 144 145 @Override init(PreferenceScreen screen)146 protected void init(PreferenceScreen screen) { 147 PreferenceCategory deviceControls = screen.findPreference(KEY_HEARING_DEVICE_GROUP); 148 if (deviceControls != null) { 149 mPreference = createPresetPreference(deviceControls.getContext()); 150 deviceControls.addPreference(mPreference); 151 } 152 } 153 154 @Override refresh()155 protected void refresh() { 156 if (!isAvailable() || mPreference == null) { 157 return; 158 } 159 mPreference.setEnabled(mCachedDevice.isConnectedHapClientDevice()); 160 161 loadAllPresetInfo(); 162 if (mPreference.getEntries().length == 0) { 163 if (DEBUG) { 164 Log.w(TAG, "Disable the preference since preset info size = 0"); 165 } 166 mPreference.setEnabled(false); 167 } else { 168 int activePresetIndex = mHapClientProfile.getActivePresetIndex( 169 mCachedDevice.getDevice()); 170 if (activePresetIndex != BluetoothHapClient.PRESET_INDEX_UNAVAILABLE) { 171 mPreference.setValue(Integer.toString(activePresetIndex)); 172 mPreference.setSummary(mPreference.getEntry()); 173 } else { 174 mPreference.setSummary(null); 175 } 176 } 177 } 178 179 @Override isAvailable()180 public boolean isAvailable() { 181 if (mHapClientProfile == null) { 182 return false; 183 } 184 return mCachedDevice.getProfiles().stream().anyMatch( 185 profile -> profile instanceof HapClientProfile); 186 } 187 188 @Override onPresetSelected(@onNull BluetoothDevice device, int presetIndex, int reason)189 public void onPresetSelected(@NonNull BluetoothDevice device, int presetIndex, int reason) { 190 if (device.equals(mCachedDevice.getDevice())) { 191 if (DEBUG) { 192 Log.d(TAG, "onPresetSelected, device: " + device.getAddress() 193 + ", presetIndex: " + presetIndex + ", reason: " + reason); 194 } 195 mContext.getMainExecutor().execute(this::refresh); 196 } 197 } 198 199 @Override onPresetSelectionFailed(@onNull BluetoothDevice device, int reason)200 public void onPresetSelectionFailed(@NonNull BluetoothDevice device, int reason) { 201 if (device.equals(mCachedDevice.getDevice())) { 202 Log.w(TAG, "onPresetSelectionFailed, device: " + device.getAddress() 203 + ", reason: " + reason); 204 mContext.getMainExecutor().execute(() -> { 205 refresh(); 206 showErrorToast(); 207 }); 208 } 209 } 210 211 @Override onPresetSelectionForGroupFailed(int hapGroupId, int reason)212 public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) { 213 if (hapGroupId == mHapClientProfile.getHapGroup(mCachedDevice.getDevice())) { 214 Log.w(TAG, "onPresetSelectionForGroupFailed, group: " + hapGroupId 215 + ", reason: " + reason); 216 // Try to set the preset independently if group operation failed 217 if (mPreference != null) { 218 selectPresetIndependently(Integer.parseInt(mPreference.getValue())); 219 } 220 } 221 } 222 223 @Override onPresetInfoChanged(@onNull BluetoothDevice device, @NonNull List<BluetoothHapPresetInfo> presetInfoList, int reason)224 public void onPresetInfoChanged(@NonNull BluetoothDevice device, 225 @NonNull List<BluetoothHapPresetInfo> presetInfoList, int reason) { 226 if (device.equals(mCachedDevice.getDevice())) { 227 if (DEBUG) { 228 Log.d(TAG, "onPresetInfoChanged, device: " + device.getAddress() 229 + ", reason: " + reason); 230 for (BluetoothHapPresetInfo info: presetInfoList) { 231 Log.d(TAG, " preset " + info.getIndex() + ": " + info.getName()); 232 } 233 } 234 mContext.getMainExecutor().execute(this::refresh); 235 } 236 } 237 238 @Override onSetPresetNameFailed(@onNull BluetoothDevice device, int reason)239 public void onSetPresetNameFailed(@NonNull BluetoothDevice device, int reason) { 240 if (device.equals(mCachedDevice.getDevice())) { 241 Log.w(TAG, "onSetPresetNameFailed, device: " + device.getAddress() 242 + ", reason: " + reason); 243 mContext.getMainExecutor().execute(() -> { 244 refresh(); 245 showErrorToast(); 246 }); 247 } 248 } 249 250 @Override onSetPresetNameForGroupFailed(int hapGroupId, int reason)251 public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) { 252 if (hapGroupId == mHapClientProfile.getHapGroup(mCachedDevice.getDevice())) { 253 Log.w(TAG, "onSetPresetNameForGroupFailed, group: " + hapGroupId 254 + ", reason: " + reason); 255 mContext.getMainExecutor().execute(() -> { 256 refresh(); 257 showErrorToast(); 258 }); 259 } 260 } 261 createPresetPreference(Context context)262 private ListPreference createPresetPreference(Context context) { 263 ListPreference preference = new ListPreference(context); 264 preference.setKey(KEY_HEARING_AIDS_PRESETS); 265 preference.setOrder(ORDER_HEARING_AIDS_PRESETS); 266 preference.setTitle(context.getString(R.string.bluetooth_hearing_aids_presets)); 267 preference.setOnPreferenceChangeListener(this); 268 return preference; 269 } 270 loadAllPresetInfo()271 private void loadAllPresetInfo() { 272 if (mPreference == null) { 273 return; 274 } 275 List<BluetoothHapPresetInfo> infoList = mHapClientProfile.getAllPresetInfo( 276 mCachedDevice.getDevice()); 277 CharSequence[] presetNames = new CharSequence[infoList.size()]; 278 CharSequence[] presetIndexes = new CharSequence[infoList.size()]; 279 for (int i = 0; i < infoList.size(); i++) { 280 presetNames[i] = infoList.get(i).getName(); 281 presetIndexes[i] = Integer.toString(infoList.get(i).getIndex()); 282 } 283 mPreference.setEntries(presetNames); 284 mPreference.setEntryValues(presetIndexes); 285 } 286 287 @VisibleForTesting 288 @Nullable getPreference()289 ListPreference getPreference() { 290 return mPreference; 291 } 292 showErrorToast()293 void showErrorToast() { 294 Toast.makeText(mContext, R.string.bluetooth_hearing_aids_presets_error, 295 Toast.LENGTH_SHORT).show(); 296 } 297 registerHapCallback()298 private void registerHapCallback() { 299 if (mHapClientProfile != null) { 300 try { 301 mHapClientProfile.registerCallback(ThreadUtils.getBackgroundExecutor(), this); 302 } catch (IllegalArgumentException e) { 303 // The callback was already registered 304 Log.w(TAG, "Cannot register callback: " + e.getMessage()); 305 } 306 307 } 308 } 309 unregisterHapCallback()310 private void unregisterHapCallback() { 311 if (mHapClientProfile != null) { 312 try { 313 mHapClientProfile.unregisterCallback(this); 314 } catch (IllegalArgumentException e) { 315 // The callback was never registered or was already unregistered 316 Log.w(TAG, "Cannot unregister callback: " + e.getMessage()); 317 } 318 } 319 } 320 321 @Override onServiceConnected()322 public void onServiceConnected() { 323 if (mHapClientProfile != null && mHapClientProfile.isProfileReady()) { 324 mProfileManager.removeServiceListener(this); 325 registerHapCallback(); 326 refresh(); 327 } 328 } 329 330 @Override onServiceDisconnected()331 public void onServiceDisconnected() { 332 // Do nothing 333 } 334 selectPresetSynchronously(int groupId, int presetIndex)335 private void selectPresetSynchronously(int groupId, int presetIndex) { 336 if (mPreference == null) { 337 return; 338 } 339 if (DEBUG) { 340 Log.d(TAG, "selectPresetSynchronously" 341 + ", presetIndex: " + presetIndex 342 + ", groupId: " + groupId 343 + ", device: " + mCachedDevice.getAddress()); 344 } 345 mHapClientProfile.selectPresetForGroup(groupId, presetIndex); 346 } selectPresetIndependently(int presetIndex)347 private void selectPresetIndependently(int presetIndex) { 348 if (mPreference == null) { 349 return; 350 } 351 if (DEBUG) { 352 Log.d(TAG, "selectPresetIndependently" 353 + ", presetIndex: " + presetIndex 354 + ", device: " + mCachedDevice.getAddress()); 355 } 356 mHapClientProfile.selectPreset(mCachedDevice.getDevice(), presetIndex); 357 final CachedBluetoothDevice subDevice = mCachedDevice.getSubDevice(); 358 if (subDevice != null) { 359 if (DEBUG) { 360 Log.d(TAG, "selectPreset for subDevice, device: " + subDevice); 361 } 362 mHapClientProfile.selectPreset(subDevice.getDevice(), presetIndex); 363 } 364 for (final CachedBluetoothDevice memberDevice : 365 mCachedDevice.getMemberDevice()) { 366 if (DEBUG) { 367 Log.d(TAG, "selectPreset for memberDevice, device: " + memberDevice); 368 } 369 mHapClientProfile.selectPreset(memberDevice.getDevice(), presetIndex); 370 } 371 } 372 } 373