• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.connecteddevice.audiosharing;
18 
19 import android.annotation.IntRange;
20 import android.bluetooth.BluetoothCsipSetCoordinator;
21 import android.bluetooth.BluetoothDevice;
22 import android.bluetooth.BluetoothLeBroadcastAssistant;
23 import android.bluetooth.BluetoothLeBroadcastMetadata;
24 import android.bluetooth.BluetoothLeBroadcastReceiveState;
25 import android.bluetooth.BluetoothVolumeControl;
26 import android.content.ContentResolver;
27 import android.content.Context;
28 import android.database.ContentObserver;
29 import android.media.AudioManager;
30 import android.os.Handler;
31 import android.os.Looper;
32 import android.provider.Settings;
33 import android.util.Log;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import androidx.annotation.VisibleForTesting;
38 import androidx.lifecycle.LifecycleOwner;
39 import androidx.preference.Preference;
40 import androidx.preference.PreferenceGroup;
41 import androidx.preference.PreferenceScreen;
42 
43 import com.android.settings.bluetooth.BluetoothDeviceUpdater;
44 import com.android.settings.bluetooth.Utils;
45 import com.android.settings.connecteddevice.DevicePreferenceCallback;
46 import com.android.settings.dashboard.DashboardFragment;
47 import com.android.settingslib.bluetooth.BluetoothUtils;
48 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
49 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
50 import com.android.settingslib.bluetooth.LocalBluetoothManager;
51 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
52 import com.android.settingslib.bluetooth.VolumeControlProfile;
53 
54 import java.util.Map;
55 import java.util.concurrent.ConcurrentHashMap;
56 import java.util.concurrent.CopyOnWriteArraySet;
57 import java.util.concurrent.Executor;
58 import java.util.concurrent.Executors;
59 import java.util.concurrent.atomic.AtomicBoolean;
60 
61 public class AudioSharingDeviceVolumeGroupController extends AudioSharingBasePreferenceController
62         implements DevicePreferenceCallback {
63     private static final String TAG = "AudioSharingVolCtlr";
64     private static final String KEY = "audio_sharing_device_volume_group";
65 
66     @Nullable private final LocalBluetoothManager mBtManager;
67     @Nullable private final LocalBluetoothProfileManager mProfileManager;
68     @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
69     @Nullable private final VolumeControlProfile mVolumeControl;
70     @Nullable private final ContentResolver mContentResolver;
71     @Nullable private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
72     private final Executor mExecutor;
73     private final ContentObserver mSettingsObserver;
74     @Nullable private PreferenceGroup mPreferenceGroup;
75     private CopyOnWriteArraySet<AudioSharingDeviceVolumePreference> mVolumePreferences =
76             new CopyOnWriteArraySet<>();
77     private ConcurrentHashMap<Integer, Integer> mValueMap = new ConcurrentHashMap<>();
78     private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
79 
80     @VisibleForTesting
81     BluetoothVolumeControl.Callback mVolumeControlCallback =
82             new BluetoothVolumeControl.Callback() {
83                 @Override
84                 public void onDeviceVolumeChanged(
85                         @NonNull BluetoothDevice device,
86                         @IntRange(from = -255, to = 255) int volume) {
87                     CachedBluetoothDevice cachedDevice =
88                             mBtManager == null
89                                     ? null
90                                     : mBtManager.getCachedDeviceManager().findDevice(device);
91                     if (cachedDevice == null) return;
92                     int groupId = BluetoothUtils.getGroupId(cachedDevice);
93                     mValueMap.put(groupId, volume);
94                     for (AudioSharingDeviceVolumePreference preference : mVolumePreferences) {
95                         if (preference.getCachedDevice() != null
96                                 && BluetoothUtils.getGroupId(preference.getCachedDevice())
97                                         == groupId) {
98                             // If the callback return invalid volume, try to
99                             // get the volume from AudioManager.STREAM_MUSIC
100                             int finalVolume = getAudioVolumeIfNeeded(volume);
101                             Log.d(
102                                     TAG,
103                                     "onDeviceVolumeChanged: set volume to "
104                                             + finalVolume
105                                             + " for "
106                                             + device.getAnonymizedAddress());
107                             AudioSharingUtils.postOnMainThread(mContext,
108                                     () -> preference.setProgress(finalVolume));
109                             break;
110                         }
111                     }
112                 }
113             };
114 
115     @VisibleForTesting
116     BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
117             new BluetoothLeBroadcastAssistant.Callback() {
118                 @Override
119                 public void onSearchStarted(int reason) {}
120 
121                 @Override
122                 public void onSearchStartFailed(int reason) {}
123 
124                 @Override
125                 public void onSearchStopped(int reason) {}
126 
127                 @Override
128                 public void onSearchStopFailed(int reason) {}
129 
130                 @Override
131                 public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
132 
133                 @Override
134                 public void onSourceAdded(
135                         @NonNull BluetoothDevice sink, int sourceId, int reason) {
136                     Log.d(TAG, "onSourceAdded: update volume list.");
137                     if (mBluetoothDeviceUpdater != null) {
138                         mBluetoothDeviceUpdater.forceUpdate();
139                     }
140                 }
141 
142                 @Override
143                 public void onSourceAddFailed(
144                         @NonNull BluetoothDevice sink,
145                         @NonNull BluetoothLeBroadcastMetadata source,
146                         int reason) {}
147 
148                 @Override
149                 public void onSourceModified(
150                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
151 
152                 @Override
153                 public void onSourceModifyFailed(
154                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
155 
156                 @Override
157                 public void onSourceRemoved(
158                         @NonNull BluetoothDevice sink, int sourceId, int reason) {
159                     Log.d(TAG, "onSourceRemoved: update volume list.");
160                     if (mBluetoothDeviceUpdater != null) {
161                         mBluetoothDeviceUpdater.forceUpdate();
162                     }
163                 }
164 
165                 @Override
166                 public void onSourceRemoveFailed(
167                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
168 
169                 @Override
170                 public void onReceiveStateChanged(
171                         @NonNull BluetoothDevice sink,
172                         int sourceId,
173                         @NonNull BluetoothLeBroadcastReceiveState state) {}
174             };
175 
AudioSharingDeviceVolumeGroupController(Context context)176     public AudioSharingDeviceVolumeGroupController(Context context) {
177         super(context, KEY);
178         mBtManager = Utils.getLocalBtManager(mContext);
179         mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
180         mAssistant =
181                 mProfileManager == null
182                         ? null
183                         : mProfileManager.getLeAudioBroadcastAssistantProfile();
184         mVolumeControl = mProfileManager == null ? null : mProfileManager.getVolumeControlProfile();
185         mExecutor = Executors.newSingleThreadExecutor();
186         mContentResolver = context.getContentResolver();
187         mSettingsObserver = new SettingsObserver();
188     }
189 
190     private class SettingsObserver extends ContentObserver {
SettingsObserver()191         SettingsObserver() {
192             super(new Handler(Looper.getMainLooper()));
193         }
194 
195         @Override
onChange(boolean selfChange)196         public void onChange(boolean selfChange) {
197             Log.d(TAG, "onChange, fallback device group id has been changed");
198             for (AudioSharingDeviceVolumePreference preference : mVolumePreferences) {
199                 int order = getPreferenceOrderForDevice(preference.getCachedDevice());
200                 Log.d(TAG, "onChange: set order to " + order + " for " + preference);
201                 AudioSharingUtils.postOnMainThread(mContext, () -> preference.setOrder(order));
202             }
203         }
204     }
205 
206     @Override
onStart(@onNull LifecycleOwner owner)207     public void onStart(@NonNull LifecycleOwner owner) {
208         super.onStart(owner);
209         registerCallbacks();
210     }
211 
212     @Override
onStop(@onNull LifecycleOwner owner)213     public void onStop(@NonNull LifecycleOwner owner) {
214         super.onStop(owner);
215         unregisterCallbacks();
216     }
217 
218     @Override
onDestroy(@onNull LifecycleOwner owner)219     public void onDestroy(@NonNull LifecycleOwner owner) {
220         mVolumePreferences.clear();
221     }
222 
223     @Override
displayPreference(PreferenceScreen screen)224     public void displayPreference(PreferenceScreen screen) {
225         super.displayPreference(screen);
226 
227         mPreferenceGroup = screen.findPreference(KEY);
228         if (mPreferenceGroup != null) {
229             mPreferenceGroup.setVisible(false);
230         }
231 
232         if (isAvailable() && mBluetoothDeviceUpdater != null) {
233             mBluetoothDeviceUpdater.setPrefContext(screen.getContext());
234             mBluetoothDeviceUpdater.forceUpdate();
235         }
236     }
237 
238     @Override
getPreferenceKey()239     public String getPreferenceKey() {
240         return KEY;
241     }
242 
243     @Override
onDeviceAdded(Preference preference)244     public void onDeviceAdded(Preference preference) {
245         if (!(preference instanceof AudioSharingDeviceVolumePreference)) {
246             Log.d(TAG, "Skip onDeviceAdded, invalid preference type");
247             return;
248         }
249         var volumePref = (AudioSharingDeviceVolumePreference) preference;
250         mVolumePreferences.add(volumePref);
251         AudioSharingUtils.postOnMainThread(mContext, () -> {
252             if (mPreferenceGroup != null) {
253                 if (mPreferenceGroup.getPreferenceCount() == 0) {
254                     mPreferenceGroup.setVisible(true);
255                 }
256                 mPreferenceGroup.addPreference(volumePref);
257             }
258         });
259         CachedBluetoothDevice cachedDevice = volumePref.getCachedDevice();
260         String address = cachedDevice.getDevice() == null ? "null"
261                 : cachedDevice.getDevice().getAnonymizedAddress();
262         int order = getPreferenceOrderForDevice(cachedDevice);
263         Log.d(TAG, "onDeviceAdded: set order to " + order + " for " + address);
264         AudioSharingUtils.postOnMainThread(mContext, () -> volumePref.setOrder(order));
265         int volume = mValueMap.getOrDefault(BluetoothUtils.getGroupId(cachedDevice), -1);
266         // If the volume is invalid, try to get the volume from AudioManager.STREAM_MUSIC
267         int finalVolume = getAudioVolumeIfNeeded(volume);
268         Log.d(TAG, "onDeviceAdded: set volume to " + finalVolume + " for " + address);
269         AudioSharingUtils.postOnMainThread(mContext, () -> volumePref.setProgress(finalVolume));
270     }
271 
272     @Override
onDeviceRemoved(Preference preference)273     public void onDeviceRemoved(Preference preference) {
274         if (!(preference instanceof AudioSharingDeviceVolumePreference)) {
275             Log.d(TAG, "Skip onDeviceRemoved, invalid preference type");
276             return;
277         }
278         var volumePref = (AudioSharingDeviceVolumePreference) preference;
279         if (mVolumePreferences.contains(volumePref)) {
280             mVolumePreferences.remove(volumePref);
281         }
282         String address = volumePref.getCachedDevice().getDevice() == null ? "null"
283                 : volumePref.getCachedDevice().getDevice().getAnonymizedAddress();
284         Log.d(TAG, "onDeviceRemoved: " + address);
285         AudioSharingUtils.postOnMainThread(mContext, () -> {
286             if (mPreferenceGroup != null) {
287                 mPreferenceGroup.removePreference(volumePref);
288                 if (mPreferenceGroup.getPreferenceCount() == 0) {
289                     mPreferenceGroup.setVisible(false);
290                 }
291             }
292         });
293     }
294 
295     @Override
updateVisibility()296     public void updateVisibility() {
297         if (mPreferenceGroup != null && mPreferenceGroup.getPreferenceCount() == 0) {
298             mPreferenceGroup.setVisible(false);
299             return;
300         }
301         super.updateVisibility();
302     }
303 
304     @Override
onAudioSharingProfilesConnected()305     public void onAudioSharingProfilesConnected() {
306         registerCallbacks();
307     }
308 
309     /**
310      * Initialize the controller.
311      *
312      * @param fragment The fragment to provide the context and metrics category for {@link
313      *     AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs.
314      */
init(DashboardFragment fragment)315     public void init(DashboardFragment fragment) {
316         mBluetoothDeviceUpdater =
317                 new AudioSharingDeviceVolumeControlUpdater(
318                         fragment.getContext(),
319                         AudioSharingDeviceVolumeGroupController.this,
320                         fragment.getMetricsCategory());
321     }
322 
323     @VisibleForTesting
setDeviceUpdater(@ullable AudioSharingDeviceVolumeControlUpdater updater)324     void setDeviceUpdater(@Nullable AudioSharingDeviceVolumeControlUpdater updater) {
325         mBluetoothDeviceUpdater = updater;
326     }
327 
328     /** Test only: set callback registration status in tests. */
329     @VisibleForTesting
setCallbacksRegistered(boolean registered)330     void setCallbacksRegistered(boolean registered) {
331         mCallbacksRegistered.set(registered);
332     }
333 
334     /** Test only: set volume map in tests. */
335     @VisibleForTesting
setVolumeMap(@ullable Map<Integer, Integer> map)336     void setVolumeMap(@Nullable Map<Integer, Integer> map) {
337         mValueMap.clear();
338         mValueMap.putAll(map);
339     }
340 
341     /** Test only: set value for private preferenceGroup in tests. */
342     @VisibleForTesting
setPreferenceGroup(@ullable PreferenceGroup group)343     void setPreferenceGroup(@Nullable PreferenceGroup group) {
344         mPreferenceGroup = group;
345         mPreference = group;
346     }
347 
348     @VisibleForTesting
getSettingsObserver()349     ContentObserver getSettingsObserver() {
350         return mSettingsObserver;
351     }
352 
registerCallbacks()353     private void registerCallbacks() {
354         if (!isAvailable()) {
355             Log.d(TAG, "Skip registerCallbacks(). Feature is not available.");
356             return;
357         }
358         if (mAssistant == null
359                 || mVolumeControl == null
360                 || mBluetoothDeviceUpdater == null
361                 || mContentResolver == null
362                 || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
363             Log.d(TAG, "Skip registerCallbacks(). Profile is not ready.");
364             return;
365         }
366         if (!mCallbacksRegistered.get()) {
367             Log.d(TAG, "registerCallbacks()");
368             mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
369             mVolumeControl.registerCallback(mExecutor, mVolumeControlCallback);
370             mBluetoothDeviceUpdater.registerCallback();
371             mBluetoothDeviceUpdater.refreshPreference();
372             mContentResolver.registerContentObserver(
373                     Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast()),
374                     false,
375                     mSettingsObserver);
376             mCallbacksRegistered.set(true);
377         }
378     }
379 
unregisterCallbacks()380     private void unregisterCallbacks() {
381         if (!isAvailable()) {
382             Log.d(TAG, "Skip unregister callbacks. Feature is not available.");
383             return;
384         }
385         if (mAssistant == null
386                 || mVolumeControl == null
387                 || mBluetoothDeviceUpdater == null
388                 || mContentResolver == null
389                 || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
390             Log.d(TAG, "Skip unregisterCallbacks(). Profile is not ready.");
391             return;
392         }
393         if (mCallbacksRegistered.get()) {
394             Log.d(TAG, "unregisterCallbacks()");
395             mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
396             mVolumeControl.unregisterCallback(mVolumeControlCallback);
397             mBluetoothDeviceUpdater.unregisterCallback();
398             mContentResolver.unregisterContentObserver(mSettingsObserver);
399             mValueMap.clear();
400             mCallbacksRegistered.set(false);
401         }
402     }
403 
getAudioVolumeIfNeeded(int volume)404     private int getAudioVolumeIfNeeded(int volume) {
405         if (volume >= 0) return volume;
406         try {
407             AudioManager audioManager = mContext.getSystemService(AudioManager.class);
408             int max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
409             int min = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC);
410             return Math.round(
411                     audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) * 255f / (max - min));
412         } catch (RuntimeException e) {
413             Log.e(TAG, "Fail to fetch current music stream volume, error = " + e);
414             return volume;
415         }
416     }
417 
getPreferenceOrderForDevice(@onNull CachedBluetoothDevice cachedDevice)418     private int getPreferenceOrderForDevice(@NonNull CachedBluetoothDevice cachedDevice) {
419         int groupId = BluetoothUtils.getGroupId(cachedDevice);
420         // The fallback device rank first among the audio sharing device list.
421         return (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
422                 && groupId == BluetoothUtils.getPrimaryGroupIdForBroadcast(mContentResolver,
423                 mBtManager))
424                 ? 0
425                 : 1;
426     }
427 }
428