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