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 static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID;
20 
21 import android.annotation.IntRange;
22 import android.bluetooth.BluetoothCsipSetCoordinator;
23 import android.bluetooth.BluetoothDevice;
24 import android.bluetooth.BluetoothLeBroadcastAssistant;
25 import android.bluetooth.BluetoothLeBroadcastMetadata;
26 import android.bluetooth.BluetoothLeBroadcastReceiveState;
27 import android.bluetooth.BluetoothVolumeControl;
28 import android.content.ContentResolver;
29 import android.content.Context;
30 import android.database.ContentObserver;
31 import android.media.AudioManager;
32 import android.os.Handler;
33 import android.os.Looper;
34 import android.provider.Settings;
35 import android.util.Log;
36 
37 import androidx.annotation.NonNull;
38 import androidx.annotation.Nullable;
39 import androidx.annotation.VisibleForTesting;
40 import androidx.lifecycle.LifecycleOwner;
41 import androidx.preference.Preference;
42 import androidx.preference.PreferenceGroup;
43 import androidx.preference.PreferenceScreen;
44 
45 import com.android.settings.bluetooth.BluetoothDeviceUpdater;
46 import com.android.settings.bluetooth.Utils;
47 import com.android.settings.connecteddevice.DevicePreferenceCallback;
48 import com.android.settings.dashboard.DashboardFragment;
49 import com.android.settingslib.bluetooth.BluetoothUtils;
50 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
51 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
52 import com.android.settingslib.bluetooth.LocalBluetoothManager;
53 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
54 import com.android.settingslib.bluetooth.VolumeControlProfile;
55 
56 import java.util.ArrayList;
57 import java.util.HashMap;
58 import java.util.List;
59 import java.util.Map;
60 import java.util.concurrent.Executor;
61 import java.util.concurrent.Executors;
62 import java.util.concurrent.atomic.AtomicBoolean;
63 
64 public class AudioSharingDeviceVolumeGroupController extends AudioSharingBasePreferenceController
65         implements DevicePreferenceCallback {
66     private static final String TAG = "AudioSharingDeviceVolumeGroupController";
67     private static final String KEY = "audio_sharing_device_volume_group";
68 
69     @Nullable private final LocalBluetoothManager mBtManager;
70     @Nullable private final LocalBluetoothProfileManager mProfileManager;
71     @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
72     @Nullable private final VolumeControlProfile mVolumeControl;
73     @Nullable private final ContentResolver mContentResolver;
74     @Nullable private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
75     private final Executor mExecutor;
76     private final ContentObserver mSettingsObserver;
77     @Nullable private PreferenceGroup mPreferenceGroup;
78     private List<AudioSharingDeviceVolumePreference> mVolumePreferences = new ArrayList<>();
79     private Map<Integer, Integer> mValueMap = new HashMap<Integer, Integer>();
80     private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
81 
82     @VisibleForTesting
83     BluetoothVolumeControl.Callback mVolumeControlCallback =
84             new BluetoothVolumeControl.Callback() {
85                 @Override
86                 public void onDeviceVolumeChanged(
87                         @NonNull BluetoothDevice device,
88                         @IntRange(from = -255, to = 255) int volume) {
89                     CachedBluetoothDevice cachedDevice =
90                             mBtManager == null
91                                     ? null
92                                     : mBtManager.getCachedDeviceManager().findDevice(device);
93                     if (cachedDevice == null) return;
94                     int groupId = AudioSharingUtils.getGroupId(cachedDevice);
95                     mValueMap.put(groupId, volume);
96                     for (AudioSharingDeviceVolumePreference preference : mVolumePreferences) {
97                         if (preference.getCachedDevice() != null
98                                 && AudioSharingUtils.getGroupId(preference.getCachedDevice())
99                                         == groupId) {
100                             // If the callback return invalid volume, try to
101                             // get the volume from AudioManager.STREAM_MUSIC
102                             int finalVolume = getAudioVolumeIfNeeded(volume);
103                             Log.d(
104                                     TAG,
105                                     "onDeviceVolumeChanged: set volume to "
106                                             + finalVolume
107                                             + " for "
108                                             + device.getAnonymizedAddress());
109                             mContext.getMainExecutor()
110                                     .execute(() -> preference.setProgress(finalVolume));
111                             break;
112                         }
113                     }
114                 }
115             };
116 
117     @VisibleForTesting
118     BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
119             new BluetoothLeBroadcastAssistant.Callback() {
120                 @Override
121                 public void onSearchStarted(int reason) {}
122 
123                 @Override
124                 public void onSearchStartFailed(int reason) {}
125 
126                 @Override
127                 public void onSearchStopped(int reason) {}
128 
129                 @Override
130                 public void onSearchStopFailed(int reason) {}
131 
132                 @Override
133                 public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
134 
135                 @Override
136                 public void onSourceAdded(
137                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
138 
139                 @Override
140                 public void onSourceAddFailed(
141                         @NonNull BluetoothDevice sink,
142                         @NonNull BluetoothLeBroadcastMetadata source,
143                         int reason) {}
144 
145                 @Override
146                 public void onSourceModified(
147                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
148 
149                 @Override
150                 public void onSourceModifyFailed(
151                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
152 
153                 @Override
154                 public void onSourceRemoved(
155                         @NonNull BluetoothDevice sink, int sourceId, int reason) {
156                     Log.d(TAG, "onSourceRemoved: update volume list.");
157                     if (mBluetoothDeviceUpdater != null) {
158                         mBluetoothDeviceUpdater.forceUpdate();
159                     }
160                 }
161 
162                 @Override
163                 public void onSourceRemoveFailed(
164                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
165 
166                 @Override
167                 public void onReceiveStateChanged(
168                         @NonNull BluetoothDevice sink,
169                         int sourceId,
170                         @NonNull BluetoothLeBroadcastReceiveState state) {
171                     if (BluetoothUtils.isConnected(state)) {
172                         Log.d(TAG, "onReceiveStateChanged: synced, update volume list.");
173                         if (mBluetoothDeviceUpdater != null) {
174                             mBluetoothDeviceUpdater.forceUpdate();
175                         }
176                     }
177                 }
178             };
179 
AudioSharingDeviceVolumeGroupController(Context context)180     public AudioSharingDeviceVolumeGroupController(Context context) {
181         super(context, KEY);
182         mBtManager = Utils.getLocalBtManager(mContext);
183         mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
184         mAssistant =
185                 mProfileManager == null
186                         ? null
187                         : mProfileManager.getLeAudioBroadcastAssistantProfile();
188         mVolumeControl = mProfileManager == null ? null : mProfileManager.getVolumeControlProfile();
189         mExecutor = Executors.newSingleThreadExecutor();
190         mContentResolver = context.getContentResolver();
191         mSettingsObserver = new SettingsObserver();
192     }
193 
194     private class SettingsObserver extends ContentObserver {
SettingsObserver()195         SettingsObserver() {
196             super(new Handler(Looper.getMainLooper()));
197         }
198 
199         @Override
onChange(boolean selfChange)200         public void onChange(boolean selfChange) {
201             Log.d(TAG, "onChange, fallback device group id has been changed");
202             for (AudioSharingDeviceVolumePreference preference : mVolumePreferences) {
203                 preference.setOrder(getPreferenceOrderForDevice(preference.getCachedDevice()));
204             }
205         }
206     }
207 
208     @Override
onStart(@onNull LifecycleOwner owner)209     public void onStart(@NonNull LifecycleOwner owner) {
210         super.onStart(owner);
211         registerCallbacks();
212     }
213 
214     @Override
onStop(@onNull LifecycleOwner owner)215     public void onStop(@NonNull LifecycleOwner owner) {
216         super.onStop(owner);
217         unregisterCallbacks();
218     }
219 
220     @Override
onDestroy(@onNull LifecycleOwner owner)221     public void onDestroy(@NonNull LifecycleOwner owner) {
222         mVolumePreferences.clear();
223     }
224 
225     @Override
displayPreference(PreferenceScreen screen)226     public void displayPreference(PreferenceScreen screen) {
227         super.displayPreference(screen);
228 
229         mPreferenceGroup = screen.findPreference(KEY);
230         if (mPreferenceGroup != null) {
231             mPreferenceGroup.setVisible(false);
232         }
233 
234         if (isAvailable() && mBluetoothDeviceUpdater != null) {
235             mBluetoothDeviceUpdater.setPrefContext(screen.getContext());
236             mBluetoothDeviceUpdater.forceUpdate();
237         }
238     }
239 
240     @Override
getPreferenceKey()241     public String getPreferenceKey() {
242         return KEY;
243     }
244 
245     @Override
onDeviceAdded(Preference preference)246     public void onDeviceAdded(Preference preference) {
247         if (mPreferenceGroup != null) {
248             if (mPreferenceGroup.getPreferenceCount() == 0) {
249                 mPreferenceGroup.setVisible(true);
250             }
251             mPreferenceGroup.addPreference(preference);
252         }
253         if (preference instanceof AudioSharingDeviceVolumePreference) {
254             var volumePref = (AudioSharingDeviceVolumePreference) preference;
255             CachedBluetoothDevice cachedDevice = volumePref.getCachedDevice();
256             volumePref.setOrder(getPreferenceOrderForDevice(cachedDevice));
257             mVolumePreferences.add(volumePref);
258             if (volumePref.getProgress() > 0) return;
259             int volume = mValueMap.getOrDefault(AudioSharingUtils.getGroupId(cachedDevice), -1);
260             // If the volume is invalid, try to get the volume from AudioManager.STREAM_MUSIC
261             int finalVolume = getAudioVolumeIfNeeded(volume);
262             Log.d(
263                     TAG,
264                     "onDeviceAdded: set volume to "
265                             + finalVolume
266                             + " for "
267                             + cachedDevice.getDevice().getAnonymizedAddress());
268             AudioSharingUtils.postOnMainThread(mContext, () -> volumePref.setProgress(finalVolume));
269         }
270     }
271 
272     @Override
onDeviceRemoved(Preference preference)273     public void onDeviceRemoved(Preference preference) {
274         if (mPreferenceGroup != null) {
275             mPreferenceGroup.removePreference(preference);
276             if (mPreferenceGroup.getPreferenceCount() == 0) {
277                 mPreferenceGroup.setVisible(false);
278             }
279         }
280         if (preference instanceof AudioSharingDeviceVolumePreference) {
281             var volumePref = (AudioSharingDeviceVolumePreference) preference;
282             if (mVolumePreferences.contains(volumePref)) {
283                 mVolumePreferences.remove(volumePref);
284             }
285             CachedBluetoothDevice device = volumePref.getCachedDevice();
286             Log.d(
287                     TAG,
288                     "onDeviceRemoved: "
289                             + (device == null
290                                     ? "null"
291                                     : device.getDevice().getAnonymizedAddress()));
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             mContentResolver.registerContentObserver(
372                     Settings.Secure.getUriFor(SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID),
373                     false,
374                     mSettingsObserver);
375             mCallbacksRegistered.set(true);
376         }
377     }
378 
unregisterCallbacks()379     private void unregisterCallbacks() {
380         if (!isAvailable()) {
381             Log.d(TAG, "Skip unregister callbacks. Feature is not available.");
382             return;
383         }
384         if (mAssistant == null
385                 || mVolumeControl == null
386                 || mBluetoothDeviceUpdater == null
387                 || mContentResolver == null
388                 || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
389             Log.d(TAG, "Skip unregisterCallbacks(). Profile is not ready.");
390             return;
391         }
392         if (mCallbacksRegistered.get()) {
393             Log.d(TAG, "unregisterCallbacks()");
394             mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
395             mVolumeControl.unregisterCallback(mVolumeControlCallback);
396             mBluetoothDeviceUpdater.unregisterCallback();
397             mContentResolver.unregisterContentObserver(mSettingsObserver);
398             mValueMap.clear();
399             mCallbacksRegistered.set(false);
400         }
401     }
402 
getAudioVolumeIfNeeded(int volume)403     private int getAudioVolumeIfNeeded(int volume) {
404         if (volume >= 0) return volume;
405         try {
406             AudioManager audioManager = mContext.getSystemService(AudioManager.class);
407             int max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
408             int min = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC);
409             return Math.round(
410                     audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) * 255f / (max - min));
411         } catch (RuntimeException e) {
412             Log.e(TAG, "Fail to fetch current music stream volume, error = " + e);
413             return volume;
414         }
415     }
416 
getPreferenceOrderForDevice(@onNull CachedBluetoothDevice cachedDevice)417     private int getPreferenceOrderForDevice(@NonNull CachedBluetoothDevice cachedDevice) {
418         int groupId = AudioSharingUtils.getGroupId(cachedDevice);
419         // The fallback device rank first among the audio sharing device list.
420         return (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
421                         && groupId == AudioSharingUtils.getFallbackActiveGroupId(mContext))
422                 ? 0
423                 : 1;
424     }
425 }
426