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.settingslib.bluetooth; 18 19 import static com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data.INVALID_VOLUME; 20 21 import android.bluetooth.BluetoothDevice; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.database.ContentObserver; 25 import android.net.Uri; 26 import android.os.Handler; 27 import android.os.UserHandle; 28 import android.provider.Settings; 29 import android.util.ArrayMap; 30 import android.util.KeyValueListParser; 31 import android.util.Log; 32 33 import androidx.annotation.GuardedBy; 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 37 import com.android.settingslib.utils.ThreadUtils; 38 39 import java.util.HashMap; 40 import java.util.Map; 41 import java.util.concurrent.Executor; 42 43 /** 44 * The class to manage hearing device local data from Settings. 45 * 46 * <p><b>Note:</b> Before calling any methods to get or change the local data, you must first call 47 * the {@code start()} method to load the data from Settings. Whenever the data is modified, you 48 * must call the {@code stop()} method to save the data into Settings. After calling {@code stop()}, 49 * you should not call any methods to get or change the local data without again calling 50 * {@code start()}. 51 */ 52 public class HearingDeviceLocalDataManager { 53 private static final String TAG = "HearingDeviceDataMgr"; 54 private static final boolean DEBUG = true; 55 56 /** Interface for listening hearing device local data changed */ 57 public interface OnDeviceLocalDataChangeListener { 58 /** 59 * The method is called when the local data of the device with the address is changed. 60 * 61 * @param address the device anonymized address 62 * @param data the updated data 63 */ onDeviceLocalDataChange(@onNull String address, @Nullable Data data)64 void onDeviceLocalDataChange(@NonNull String address, @Nullable Data data); 65 } 66 67 static final String KEY_ADDR = "addr"; 68 static final String KEY_AMBIENT = "ambient"; 69 static final String KEY_GROUP_AMBIENT = "group_ambient"; 70 static final String KEY_AMBIENT_CONTROL_EXPANDED = "control_expanded"; 71 static final String LOCAL_AMBIENT_VOLUME_SETTINGS = 72 Settings.Global.HEARING_DEVICE_LOCAL_AMBIENT_VOLUME; 73 74 private static final Object sLock = new Object(); 75 76 private final Context mContext; 77 private Executor mListenerExecutor; 78 @GuardedBy("sLock") 79 private final Map<String, Data> mAddrToDataMap = new HashMap<>(); 80 private OnDeviceLocalDataChangeListener mListener; 81 private SettingsObserver mSettingsObserver; 82 private boolean mIsStarted = false; 83 HearingDeviceLocalDataManager(@onNull Context context)84 public HearingDeviceLocalDataManager(@NonNull Context context) { 85 mContext = context; 86 mSettingsObserver = new SettingsObserver(ThreadUtils.getUiThreadHandler()); 87 } 88 89 /** 90 * Clears the local data of the device. This method should be called when the device is 91 * unpaired. 92 */ clear(@onNull Context context, @NonNull BluetoothDevice device)93 public static void clear(@NonNull Context context, @NonNull BluetoothDevice device) { 94 HearingDeviceLocalDataManager manager = new HearingDeviceLocalDataManager(context); 95 manager.getLocalDataFromSettings(); 96 manager.remove(device); 97 manager.putAmbientVolumeSettings(); 98 } 99 100 /** Starts the manager. Loads the data from Settings and start observing any changes. */ start()101 public synchronized void start() { 102 if (mIsStarted) { 103 return; 104 } 105 mIsStarted = true; 106 getLocalDataFromSettings(); 107 mSettingsObserver.register(mContext.getContentResolver()); 108 } 109 110 /** Stops the manager. Flushes the data into Settings and stop observing. */ stop()111 public synchronized void stop() { 112 if (!mIsStarted) { 113 return; 114 } 115 putAmbientVolumeSettings(); 116 mSettingsObserver.unregister(mContext.getContentResolver()); 117 mIsStarted = false; 118 } 119 120 /** 121 * Sets a listener which will be be notified when hearing device local data is changed. 122 * 123 * @param listener the listener to be notified 124 * @param executor the executor to run the 125 * {@link OnDeviceLocalDataChangeListener#onDeviceLocalDataChange(String, 126 * Data)} callback 127 */ setOnDeviceLocalDataChangeListener( @onNull OnDeviceLocalDataChangeListener listener, @NonNull Executor executor)128 public void setOnDeviceLocalDataChangeListener( 129 @NonNull OnDeviceLocalDataChangeListener listener, @NonNull Executor executor) { 130 mListener = listener; 131 mListenerExecutor = executor; 132 } 133 134 /** 135 * Gets the local data of the corresponding hearing device. This should be called after 136 * {@link #start()} is called(). 137 * 138 * @param device the device to query the local data 139 */ 140 @NonNull get(@onNull BluetoothDevice device)141 public Data get(@NonNull BluetoothDevice device) { 142 if (!mIsStarted) { 143 Log.w(TAG, "Manager is not started. Please call start() first."); 144 return new Data(); 145 } 146 synchronized (sLock) { 147 return mAddrToDataMap.getOrDefault(device.getAnonymizedAddress(), new Data()); 148 } 149 } 150 151 /** Flushes the data into Settings . */ flush()152 public synchronized void flush() { 153 if (!mIsStarted) { 154 return; 155 } 156 putAmbientVolumeSettings(); 157 } 158 159 /** 160 * Puts the local data of the corresponding hearing device. 161 * 162 * @param device the device to update the local data 163 * @param data the local data to be stored 164 */ put(BluetoothDevice device, Data data)165 private void put(BluetoothDevice device, Data data) { 166 if (device == null) { 167 return; 168 } 169 synchronized (sLock) { 170 final String addr = device.getAnonymizedAddress(); 171 if (data == null) { 172 mAddrToDataMap.remove(addr); 173 } else { 174 mAddrToDataMap.put(addr, data); 175 } 176 if (mListener != null && mListenerExecutor != null) { 177 mListenerExecutor.execute(() -> mListener.onDeviceLocalDataChange(addr, data)); 178 } 179 } 180 } 181 182 /** 183 * Removes the local data of the corresponding hearing device. 184 * 185 * @param device the device to remove the local data 186 */ remove(BluetoothDevice device)187 private void remove(BluetoothDevice device) { 188 if (device == null) { 189 return; 190 } 191 synchronized (sLock) { 192 final String addr = device.getAnonymizedAddress(); 193 mAddrToDataMap.remove(addr); 194 if (mListener != null && mListenerExecutor != null) { 195 mListenerExecutor.execute(() -> mListener.onDeviceLocalDataChange(addr, null)); 196 } 197 } 198 } 199 200 /** 201 * Updates the ambient volume of the corresponding hearing device. This should be called after 202 * {@link #start()} is called(). 203 * 204 * @param device the device to update 205 * @param value the ambient value 206 * @return if the local data is updated 207 */ updateAmbient(@ullable BluetoothDevice device, int value)208 public boolean updateAmbient(@Nullable BluetoothDevice device, int value) { 209 if (!mIsStarted) { 210 Log.w(TAG, "Manager is not started. Please call start() first."); 211 return false; 212 } 213 if (device == null) { 214 return false; 215 } 216 synchronized (sLock) { 217 Data data = get(device); 218 if (value == data.ambient) { 219 return false; 220 } 221 put(device, new Data.Builder(data).ambient(value).build()); 222 return true; 223 } 224 } 225 226 /** 227 * Updates the group ambient volume of the corresponding hearing device. This should be called 228 * after {@link #start()} is called(). 229 * 230 * @param device the device to update 231 * @param value the group ambient value 232 * @return if the local data is updated 233 */ updateGroupAmbient(@ullable BluetoothDevice device, int value)234 public boolean updateGroupAmbient(@Nullable BluetoothDevice device, int value) { 235 if (!mIsStarted) { 236 Log.w(TAG, "Manager is not started. Please call start() first."); 237 return false; 238 } 239 if (device == null) { 240 return false; 241 } 242 synchronized (sLock) { 243 Data data = get(device); 244 if (value == data.groupAmbient) { 245 return false; 246 } 247 put(device, new Data.Builder(data).groupAmbient(value).build()); 248 return true; 249 } 250 } 251 252 /** 253 * Updates the ambient control is expanded or not of the corresponding hearing device. This 254 * should be called after {@link #start()} is called(). 255 * 256 * @param device the device to update 257 * @param expanded the ambient control is expanded or not 258 * @return if the local data is updated 259 */ updateAmbientControlExpanded(@ullable BluetoothDevice device, boolean expanded)260 public boolean updateAmbientControlExpanded(@Nullable BluetoothDevice device, 261 boolean expanded) { 262 if (!mIsStarted) { 263 Log.w(TAG, "Manager is not started. Please call start() first."); 264 return false; 265 } 266 if (device == null) { 267 return false; 268 } 269 synchronized (sLock) { 270 Data data = get(device); 271 if (expanded == data.ambientControlExpanded) { 272 return false; 273 } 274 put(device, new Data.Builder(data).ambientControlExpanded(expanded).build()); 275 return true; 276 } 277 } 278 getLocalDataFromSettings()279 void getLocalDataFromSettings() { 280 synchronized (sLock) { 281 Map<String, Data> updatedAddrToDataMap = parseFromSettings(); 282 notifyIfDataChanged(mAddrToDataMap, updatedAddrToDataMap); 283 mAddrToDataMap.clear(); 284 mAddrToDataMap.putAll(updatedAddrToDataMap); 285 } 286 } 287 putAmbientVolumeSettings()288 void putAmbientVolumeSettings() { 289 synchronized (sLock) { 290 StringBuilder builder = new StringBuilder(); 291 for (Map.Entry<String, Data> entry : mAddrToDataMap.entrySet()) { 292 builder.append(KEY_ADDR).append("=").append(entry.getKey()); 293 builder.append(entry.getValue().toSettingsFormat()).append(";"); 294 } 295 ThreadUtils.postOnBackgroundThread(() -> { 296 Settings.Global.putStringForUser(mContext.getContentResolver(), 297 LOCAL_AMBIENT_VOLUME_SETTINGS, builder.toString(), UserHandle.USER_SYSTEM); 298 }); 299 } 300 } 301 302 @GuardedBy("sLock") parseFromSettings()303 private Map<String, Data> parseFromSettings() { 304 String settings = Settings.Global.getStringForUser(mContext.getContentResolver(), 305 LOCAL_AMBIENT_VOLUME_SETTINGS, UserHandle.USER_SYSTEM); 306 Map<String, Data> addrToDataMap = new ArrayMap<>(); 307 if (settings != null && !settings.isEmpty()) { 308 String[] localDataArray = settings.split(";"); 309 for (String localData : localDataArray) { 310 KeyValueListParser parser = new KeyValueListParser(','); 311 parser.setString(localData); 312 String address = parser.getString(KEY_ADDR, ""); 313 if (!address.isEmpty()) { 314 Data data = new Data.Builder() 315 .ambient(parser.getInt(KEY_AMBIENT, INVALID_VOLUME)) 316 .groupAmbient(parser.getInt(KEY_GROUP_AMBIENT, INVALID_VOLUME)) 317 .ambientControlExpanded( 318 parser.getBoolean(KEY_AMBIENT_CONTROL_EXPANDED, false)) 319 .build(); 320 addrToDataMap.put(address, data); 321 } 322 } 323 } 324 return addrToDataMap; 325 } 326 327 @GuardedBy("sLock") notifyIfDataChanged(Map<String, Data> oldAddrToDataMap, Map<String, Data> newAddrToDataMap)328 private void notifyIfDataChanged(Map<String, Data> oldAddrToDataMap, 329 Map<String, Data> newAddrToDataMap) { 330 newAddrToDataMap.forEach((addr, data) -> { 331 Data oldData = oldAddrToDataMap.get(addr); 332 if (oldData == null || !oldData.equals(data)) { 333 if (mListener != null) { 334 mListenerExecutor.execute(() -> mListener.onDeviceLocalDataChange(addr, data)); 335 } 336 } 337 }); 338 } 339 340 private final class SettingsObserver extends ContentObserver { 341 private final Uri mAmbientVolumeUri = Settings.Global.getUriFor( 342 LOCAL_AMBIENT_VOLUME_SETTINGS); 343 SettingsObserver(Handler handler)344 SettingsObserver(Handler handler) { 345 super(handler); 346 } 347 register(ContentResolver contentResolver)348 void register(ContentResolver contentResolver) { 349 contentResolver.registerContentObserver(mAmbientVolumeUri, false, this, 350 UserHandle.USER_SYSTEM); 351 } 352 unregister(ContentResolver contentResolver)353 void unregister(ContentResolver contentResolver) { 354 contentResolver.unregisterContentObserver(this); 355 } 356 357 @Override onChange(boolean selfChange, @Nullable Uri uri)358 public void onChange(boolean selfChange, @Nullable Uri uri) { 359 if (mAmbientVolumeUri.equals(uri)) { 360 Log.v(TAG, "Local data on change, manager: " + HearingDeviceLocalDataManager.this); 361 getLocalDataFromSettings(); 362 } 363 } 364 } 365 Data(int ambient, int groupAmbient, boolean ambientControlExpanded)366 public record Data(int ambient, int groupAmbient, boolean ambientControlExpanded) { 367 368 public static int INVALID_VOLUME = Integer.MIN_VALUE; 369 370 private Data() { 371 this(INVALID_VOLUME, INVALID_VOLUME, false); 372 } 373 374 /** 375 * Return {@code true} if one of {@link #ambient} or {@link #groupAmbient} is assigned to 376 * a valid value. 377 */ 378 public boolean hasAmbientData() { 379 return ambient != INVALID_VOLUME || groupAmbient != INVALID_VOLUME; 380 } 381 382 /** 383 * @return the composed string which is used to store the local data in 384 * {@link Settings.Global#HEARING_DEVICE_LOCAL_AMBIENT_VOLUME} 385 */ 386 @NonNull 387 public String toSettingsFormat() { 388 String string = ""; 389 if (ambient != INVALID_VOLUME) { 390 string += ("," + KEY_AMBIENT + "=" + ambient); 391 } 392 if (groupAmbient != INVALID_VOLUME) { 393 string += ("," + KEY_GROUP_AMBIENT + "=" + groupAmbient); 394 } 395 string += ("," + KEY_AMBIENT_CONTROL_EXPANDED + "=" + ambientControlExpanded); 396 return string; 397 } 398 399 /** Builder for a Data object */ 400 public static final class Builder { 401 private int mAmbient; 402 private int mGroupAmbient; 403 private boolean mAmbientControlExpanded; 404 405 public Builder() { 406 this.mAmbient = INVALID_VOLUME; 407 this.mGroupAmbient = INVALID_VOLUME; 408 this.mAmbientControlExpanded = false; 409 } 410 411 public Builder(@NonNull Data other) { 412 this.mAmbient = other.ambient; 413 this.mGroupAmbient = other.groupAmbient; 414 this.mAmbientControlExpanded = other.ambientControlExpanded; 415 } 416 417 /** Sets the ambient volume */ 418 @NonNull 419 public Builder ambient(int ambient) { 420 this.mAmbient = ambient; 421 return this; 422 } 423 424 /** Sets the group ambient volume */ 425 @NonNull 426 public Builder groupAmbient(int groupAmbient) { 427 this.mGroupAmbient = groupAmbient; 428 return this; 429 } 430 431 /** Sets the ambient control expanded */ 432 @NonNull 433 public Builder ambientControlExpanded(boolean ambientControlExpanded) { 434 this.mAmbientControlExpanded = ambientControlExpanded; 435 return this; 436 } 437 438 /** Build the Data object */ 439 @NonNull 440 public Data build() { 441 return new Data(mAmbient, mGroupAmbient, mAmbientControlExpanded); 442 } 443 } 444 } 445 } 446