• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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