• 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 android.bluetooth.AudioInputControl.MUTE_NOT_MUTED;
20 import static android.bluetooth.AudioInputControl.MUTE_MUTED;
21 import static android.bluetooth.BluetoothDevice.BOND_BONDED;
22 
23 import static com.android.settingslib.bluetooth.AmbientVolumeUi.SIDE_UNIFIED;
24 import static com.android.settingslib.bluetooth.AmbientVolumeUi.VALID_SIDES;
25 import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_INVALID;
26 import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT;
27 import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT;
28 import static com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data.INVALID_VOLUME;
29 
30 import android.bluetooth.BluetoothDevice;
31 import android.bluetooth.BluetoothProfile;
32 import android.content.Context;
33 import android.util.ArraySet;
34 import android.util.Log;
35 import android.widget.Toast;
36 
37 import androidx.annotation.NonNull;
38 import androidx.annotation.Nullable;
39 import androidx.annotation.VisibleForTesting;
40 
41 import com.android.settingslib.R;
42 import com.android.settingslib.utils.ThreadUtils;
43 
44 import com.google.common.collect.BiMap;
45 import com.google.common.collect.HashBiMap;
46 
47 import java.util.Map;
48 import java.util.Set;
49 
50 /** This class controls ambient volume UI with local and remote ambient data. */
51 public class AmbientVolumeUiController implements
52         HearingDeviceLocalDataManager.OnDeviceLocalDataChangeListener,
53         AmbientVolumeController.AmbientVolumeControlCallback,
54         AmbientVolumeUi.AmbientVolumeUiListener, BluetoothCallback, CachedBluetoothDevice.Callback {
55 
56     private static final boolean DEBUG = true;
57     private static final String TAG = "AmbientVolumeUiController";
58 
59     private final Context mContext;
60     private final LocalBluetoothProfileManager mProfileManager;
61     private final BluetoothEventManager mEventManager;
62     private final AmbientVolumeUi mAmbientLayout;
63     private final AmbientVolumeController mVolumeController;
64     private final HearingDeviceLocalDataManager mLocalDataManager;
65 
66     private final Set<CachedBluetoothDevice> mCachedDevices = new ArraySet<>();
67     private final BiMap<Integer, BluetoothDevice> mSideToDeviceMap = HashBiMap.create();
68     private CachedBluetoothDevice mCachedDevice;
69     private boolean mShowUiWhenLocalDataExist = true;
70 
AmbientVolumeUiController(@onNull Context context, @NonNull LocalBluetoothManager bluetoothManager, @NonNull AmbientVolumeUi ambientLayout)71     public AmbientVolumeUiController(@NonNull Context context,
72             @NonNull LocalBluetoothManager bluetoothManager,
73             @NonNull AmbientVolumeUi ambientLayout) {
74         mContext = context;
75         mProfileManager = bluetoothManager.getProfileManager();
76         mEventManager = bluetoothManager.getEventManager();
77         mAmbientLayout = ambientLayout;
78         mAmbientLayout.setListener(this);
79         mVolumeController = new AmbientVolumeController(mProfileManager, this);
80         mLocalDataManager = new HearingDeviceLocalDataManager(context);
81         mLocalDataManager.setOnDeviceLocalDataChangeListener(this,
82                 ThreadUtils.getBackgroundExecutor());
83         mLocalDataManager.start();
84     }
85 
86     @VisibleForTesting
AmbientVolumeUiController(@onNull Context context, @NonNull LocalBluetoothManager bluetoothManager, @NonNull AmbientVolumeUi ambientLayout, @NonNull AmbientVolumeController volumeController, @NonNull HearingDeviceLocalDataManager localDataManager)87     public AmbientVolumeUiController(@NonNull Context context,
88             @NonNull LocalBluetoothManager bluetoothManager,
89             @NonNull AmbientVolumeUi ambientLayout,
90             @NonNull AmbientVolumeController volumeController,
91             @NonNull HearingDeviceLocalDataManager localDataManager) {
92         mContext = context;
93         mProfileManager = bluetoothManager.getProfileManager();
94         mEventManager = bluetoothManager.getEventManager();
95         mAmbientLayout = ambientLayout;
96         mVolumeController = volumeController;
97         mLocalDataManager = localDataManager;
98     }
99 
100 
101     @Override
onDeviceLocalDataChange(@onNull String address, @Nullable HearingDeviceLocalDataManager.Data data)102     public void onDeviceLocalDataChange(@NonNull String address,
103             @Nullable HearingDeviceLocalDataManager.Data data) {
104         if (data == null) {
105             // The local data is removed because the device is unpaired, do nothing
106             return;
107         }
108         if (DEBUG) {
109             Log.d(TAG, "onDeviceLocalDataChange, address:" + address + ", data:" + data);
110         }
111         for (BluetoothDevice device : mSideToDeviceMap.values()) {
112             if (device.getAnonymizedAddress().equals(address)) {
113                 postOnMainThread(() -> loadLocalDataToUi(device));
114                 return;
115             }
116         }
117     }
118 
119     @Override
onVolumeControlServiceConnected()120     public void onVolumeControlServiceConnected() {
121         mCachedDevices.forEach(device -> mVolumeController.registerCallback(
122                 ThreadUtils.getBackgroundExecutor(), device.getDevice()));
123     }
124 
125     @Override
onAmbientChanged(@onNull BluetoothDevice device, int gainSettings)126     public void onAmbientChanged(@NonNull BluetoothDevice device, int gainSettings) {
127         if (DEBUG) {
128             Log.d(TAG, "onAmbientChanged, value:" + gainSettings + ", device:" + device);
129         }
130         HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(device);
131         final boolean expanded = mAmbientLayout.isExpanded();
132         final boolean isInitiatedFromUi = (expanded && data.ambient() == gainSettings)
133                 || (!expanded && data.groupAmbient() == gainSettings);
134         if (isInitiatedFromUi) {
135             // The change is initiated from UI, no need to update UI
136             return;
137         }
138 
139         // We have to check if we need to expand the controls by getting all remote
140         // device's ambient value, delay for a while to wait all remote devices update
141         // to the latest value to avoid unnecessary expand action.
142         postDelayedOnMainThread(this::refresh, 1200L);
143     }
144 
145     @Override
onMuteChanged(@onNull BluetoothDevice device, int mute)146     public void onMuteChanged(@NonNull BluetoothDevice device, int mute) {
147         if (DEBUG) {
148             Log.d(TAG, "onMuteChanged, mute:" + mute + ", device:" + device);
149         }
150         final boolean muted = mAmbientLayout.isMuted();
151         boolean isInitiatedFromUi = (muted && mute == MUTE_MUTED)
152                 || (!muted && mute == MUTE_NOT_MUTED);
153         if (isInitiatedFromUi) {
154             // The change is initiated from UI, no need to update UI
155             return;
156         }
157 
158         // We have to check if we need to mute the devices by getting all remote
159         // device's mute state, delay for a while to wait all remote devices update
160         // to the latest value.
161         postDelayedOnMainThread(this::refresh, 1200L);
162     }
163 
164     @Override
onCommandFailed(@onNull BluetoothDevice device)165     public void onCommandFailed(@NonNull BluetoothDevice device) {
166         Log.w(TAG, "onCommandFailed, device:" + device);
167         postOnMainThread(() -> {
168             showErrorToast(R.string.bluetooth_hearing_device_ambient_error);
169             refresh();
170         });
171     }
172 
173     @Override
onExpandIconClick()174     public void onExpandIconClick() {
175         mSideToDeviceMap.forEach((s, d) -> {
176             if (!mAmbientLayout.isMuted()) {
177                 // Apply previous collapsed/expanded volume to remote device
178                 HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(d);
179                 int volume = mAmbientLayout.isExpanded()
180                         ? data.ambient() : data.groupAmbient();
181                 mVolumeController.setAmbient(d, volume);
182             }
183             // Update new value to local data
184             mLocalDataManager.updateAmbientControlExpanded(d,
185                     mAmbientLayout.isExpanded());
186         });
187         mLocalDataManager.flush();
188     }
189 
190     @Override
onAmbientVolumeIconClick()191     public void onAmbientVolumeIconClick() {
192         if (!mAmbientLayout.isMuted()) {
193             loadLocalDataToUi();
194         }
195         for (BluetoothDevice device : mSideToDeviceMap.values()) {
196             mVolumeController.setMuted(device, mAmbientLayout.isMuted());
197         }
198     }
199 
200     @Override
onSliderValueChange(int side, int value)201     public void onSliderValueChange(int side, int value) {
202         if (DEBUG) {
203             Log.d(TAG, "onSliderValueChange: side=" + side + ", value=" + value);
204         }
205         setVolumeIfValid(side, value);
206 
207         Runnable setAmbientRunnable = () -> {
208             if (side == SIDE_UNIFIED) {
209                 mSideToDeviceMap.forEach((s, d) -> mVolumeController.setAmbient(d, value));
210             } else {
211                 final BluetoothDevice device = mSideToDeviceMap.get(side);
212                 mVolumeController.setAmbient(device, value);
213             }
214         };
215 
216         if (mAmbientLayout.isMuted()) {
217             // User drag on the volume slider when muted. Unmute the devices first.
218             mAmbientLayout.setMuted(false);
219 
220             for (BluetoothDevice device : mSideToDeviceMap.values()) {
221                 mVolumeController.setMuted(device, false);
222             }
223             // Restore the value before muted
224             loadLocalDataToUi();
225             // Delay set ambient on remote device since the immediately sequential command
226             // might get failed sometimes
227             postDelayedOnMainThread(setAmbientRunnable, 1000L);
228         } else {
229             setAmbientRunnable.run();
230         }
231     }
232 
233     @Override
onProfileConnectionStateChanged(@onNull CachedBluetoothDevice cachedDevice, int state, int bluetoothProfile)234     public void onProfileConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
235             int state, int bluetoothProfile) {
236         if (bluetoothProfile == BluetoothProfile.VOLUME_CONTROL
237                 && state == BluetoothProfile.STATE_CONNECTED
238                 && mCachedDevices.contains(cachedDevice)) {
239             // After VCP connected, AICS may not ready yet and still return invalid value, delay
240             // a while to wait AICS ready as a workaround
241             postDelayedOnMainThread(this::refresh, 1000L);
242         }
243     }
244 
245     @Override
onDeviceAttributesChanged()246     public void onDeviceAttributesChanged() {
247         mCachedDevices.forEach(device -> {
248             device.unregisterCallback(this);
249             mVolumeController.unregisterCallback(device.getDevice());
250         });
251         postOnMainThread(()-> {
252             loadDevice(mCachedDevice);
253             ThreadUtils.postOnBackgroundThread(()-> {
254                 mCachedDevices.forEach(device -> {
255                     device.registerCallback(ThreadUtils.getBackgroundExecutor(), this);
256                     mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(),
257                             device.getDevice());
258                 });
259             });
260         });
261     }
262 
263     /**
264      * Registers callbacks and listeners, this should be called when needs to start listening to
265      * events.
266      */
start()267     public void start() {
268         mEventManager.registerCallback(this);
269         mLocalDataManager.start();
270         mCachedDevices.forEach(device -> {
271             device.registerCallback(ThreadUtils.getBackgroundExecutor(), this);
272             mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(),
273                     device.getDevice());
274         });
275     }
276 
277     /**
278      * Unregisters callbacks and listeners, this should be called when no longer needs to listen to
279      * events.
280      */
stop()281     public void stop() {
282         mEventManager.unregisterCallback(this);
283         mLocalDataManager.stop();
284         mCachedDevices.forEach(device -> {
285             device.unregisterCallback(this);
286             mVolumeController.unregisterCallback(device.getDevice());
287         });
288     }
289 
290     /**
291      * Loads all devices in the same set with {@code cachedDevice} and create corresponding sliders.
292      *
293      * <p>If the devices has valid ambient control points, the ambient volume UI will be visible.
294      * @param cachedDevice the remote device
295      */
loadDevice(CachedBluetoothDevice cachedDevice)296     public void loadDevice(CachedBluetoothDevice cachedDevice) {
297         if (DEBUG) {
298             Log.d(TAG, "loadDevice, device=" + cachedDevice);
299         }
300         mCachedDevice = cachedDevice;
301         mSideToDeviceMap.clear();
302         mCachedDevices.clear();
303         boolean deviceSupportVcp =
304                 cachedDevice != null && cachedDevice.getProfiles().stream().anyMatch(
305                         p -> p instanceof VolumeControlProfile);
306         if (!deviceSupportVcp) {
307             mAmbientLayout.setVisible(false);
308             return;
309         }
310 
311         // load devices in the same set
312         if (VALID_SIDES.contains(cachedDevice.getDeviceSide())
313                 && cachedDevice.getBondState() == BOND_BONDED) {
314             mSideToDeviceMap.put(cachedDevice.getDeviceSide(), cachedDevice.getDevice());
315             mCachedDevices.add(cachedDevice);
316         }
317         for (CachedBluetoothDevice memberDevice : cachedDevice.getMemberDevice()) {
318             if (VALID_SIDES.contains(memberDevice.getDeviceSide())
319                     && memberDevice.getBondState() == BOND_BONDED) {
320                 mSideToDeviceMap.put(memberDevice.getDeviceSide(), memberDevice.getDevice());
321                 mCachedDevices.add(memberDevice);
322             }
323         }
324 
325         mAmbientLayout.setExpandable(mSideToDeviceMap.size() >  1);
326         mAmbientLayout.setupSliders(mSideToDeviceMap);
327         refresh();
328     }
329 
330     /** Refreshes the ambient volume UI. */
refresh()331     public void refresh() {
332         if (isAmbientControlAvailable()) {
333             mAmbientLayout.setVisible(true);
334             loadRemoteDataToUi();
335         } else {
336             mAmbientLayout.setVisible(false);
337         }
338     }
339 
340     /** Sets if the ambient volume UI should be visible when local ambient data exist. */
setShowUiWhenLocalDataExist(boolean shouldShow)341     public void setShowUiWhenLocalDataExist(boolean shouldShow) {
342         mShowUiWhenLocalDataExist = shouldShow;
343     }
344 
345     /** Updates the ambient sliders according to current state. */
updateSliderUi()346     private void updateSliderUi() {
347         boolean isAnySliderEnabled = false;
348         for (Map.Entry<Integer, BluetoothDevice> entry : mSideToDeviceMap.entrySet()) {
349             final int side = entry.getKey();
350             final BluetoothDevice device = entry.getValue();
351             final boolean enabled = isDeviceConnectedToVcp(device)
352                     && mVolumeController.isAmbientControlAvailable(device);
353             isAnySliderEnabled |= enabled;
354             mAmbientLayout.setSliderEnabled(side, enabled);
355         }
356         mAmbientLayout.setSliderEnabled(SIDE_UNIFIED, isAnySliderEnabled);
357         mAmbientLayout.updateLayout();
358     }
359 
360     /** Sets the ambient to the corresponding control slider. */
setVolumeIfValid(int side, int volume)361     private void setVolumeIfValid(int side, int volume) {
362         if (volume == INVALID_VOLUME) {
363             return;
364         }
365         mAmbientLayout.setSliderValue(side, volume);
366         // Update new value to local data
367         if (side == SIDE_UNIFIED) {
368             mSideToDeviceMap.forEach((s, d) -> mLocalDataManager.updateGroupAmbient(d, volume));
369         } else {
370             mLocalDataManager.updateAmbient(mSideToDeviceMap.get(side), volume);
371         }
372         mLocalDataManager.flush();
373     }
374 
loadLocalDataToUi()375     private void loadLocalDataToUi() {
376         mSideToDeviceMap.forEach((s, d) -> loadLocalDataToUi(d));
377     }
378 
loadLocalDataToUi(BluetoothDevice device)379     private void loadLocalDataToUi(BluetoothDevice device) {
380         final HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(device);
381         if (DEBUG) {
382             Log.d(TAG, "loadLocalDataToUi, data=" + data + ", device=" + device);
383         }
384         if (isDeviceConnectedToVcp(device) && !mAmbientLayout.isMuted()) {
385             final int side = mSideToDeviceMap.inverse().getOrDefault(device, SIDE_INVALID);
386             setVolumeIfValid(side, data.ambient());
387             setVolumeIfValid(SIDE_UNIFIED, data.groupAmbient());
388         }
389         setAmbientControlExpanded(data.ambientControlExpanded());
390         updateSliderUi();
391     }
392 
loadRemoteDataToUi()393     private void loadRemoteDataToUi() {
394         BluetoothDevice leftDevice = mSideToDeviceMap.get(SIDE_LEFT);
395         AmbientVolumeController.RemoteAmbientState leftState =
396                 mVolumeController.refreshAmbientState(leftDevice);
397         BluetoothDevice rightDevice = mSideToDeviceMap.get(SIDE_RIGHT);
398         AmbientVolumeController.RemoteAmbientState rightState =
399                 mVolumeController.refreshAmbientState(rightDevice);
400         if (DEBUG) {
401             Log.d(TAG, "loadRemoteDataToUi, left=" + leftState + ", right=" + rightState);
402         }
403         mSideToDeviceMap.forEach((side, device) -> {
404             int ambientMax = mVolumeController.getAmbientMax(device);
405             int ambientMin = mVolumeController.getAmbientMin(device);
406             if (ambientMin != ambientMax) {
407                 mAmbientLayout.setSliderRange(side, ambientMin, ambientMax);
408                 mAmbientLayout.setSliderRange(SIDE_UNIFIED, ambientMin, ambientMax);
409             }
410         });
411 
412         // Update ambient volume
413         final int leftAmbient = leftState != null ? leftState.gainSetting() : INVALID_VOLUME;
414         final int rightAmbient = rightState != null ? rightState.gainSetting() : INVALID_VOLUME;
415         if (mAmbientLayout.isExpanded()) {
416             setVolumeIfValid(SIDE_LEFT, leftAmbient);
417             setVolumeIfValid(SIDE_RIGHT, rightAmbient);
418         } else {
419             if (leftAmbient != rightAmbient && leftAmbient != INVALID_VOLUME
420                     && rightAmbient != INVALID_VOLUME) {
421                 setVolumeIfValid(SIDE_LEFT, leftAmbient);
422                 setVolumeIfValid(SIDE_RIGHT, rightAmbient);
423                 setAmbientControlExpanded(true);
424             } else {
425                 int unifiedAmbient = leftAmbient != INVALID_VOLUME ? leftAmbient : rightAmbient;
426                 setVolumeIfValid(SIDE_UNIFIED, unifiedAmbient);
427             }
428         }
429         // Initialize local data between side and group value
430         initLocalAmbientDataIfNeeded();
431 
432         // Update mute state
433         boolean mutable = true;
434         boolean muted = true;
435         if (isDeviceConnectedToVcp(leftDevice) && leftState != null) {
436             mutable &= leftState.isMutable();
437             muted &= leftState.isMuted();
438         }
439         if (isDeviceConnectedToVcp(rightDevice) && rightState != null) {
440             mutable &= rightState.isMutable();
441             muted &= rightState.isMuted();
442         }
443         mAmbientLayout.setMutable(mutable);
444         mAmbientLayout.setMuted(muted);
445 
446         // Ensure remote device mute state is synced
447         syncMuteStateIfNeeded(leftDevice, leftState, muted);
448         syncMuteStateIfNeeded(rightDevice, rightState, muted);
449 
450         updateSliderUi();
451     }
452 
setAmbientControlExpanded(boolean expanded)453     private void setAmbientControlExpanded(boolean expanded) {
454         mAmbientLayout.setExpanded(expanded);
455         mSideToDeviceMap.forEach((s, d) -> {
456             // Update new value to local data
457             mLocalDataManager.updateAmbientControlExpanded(d, expanded);
458         });
459         mLocalDataManager.flush();
460     }
461 
462     /** Checks if any device in the same set has valid ambient control points */
isAmbientControlAvailable()463     public boolean isAmbientControlAvailable() {
464         for (BluetoothDevice device : mSideToDeviceMap.values()) {
465             if (mShowUiWhenLocalDataExist) {
466                 // Found local ambient data
467                 if (mLocalDataManager.get(device).hasAmbientData()) {
468                     return true;
469                 }
470             }
471             // Found remote ambient control points
472             if (mVolumeController.isAmbientControlAvailable(device)) {
473                 return true;
474             }
475         }
476         return false;
477     }
478 
initLocalAmbientDataIfNeeded()479     private void initLocalAmbientDataIfNeeded() {
480         int smallerVolumeAmongGroup = Integer.MAX_VALUE;
481         for (BluetoothDevice device : mSideToDeviceMap.values()) {
482             HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(device);
483             if (data.ambient() != INVALID_VOLUME) {
484                 smallerVolumeAmongGroup = Math.min(data.ambient(), smallerVolumeAmongGroup);
485             } else if (data.groupAmbient() != INVALID_VOLUME) {
486                 // Initialize side ambient from group ambient value
487                 mLocalDataManager.updateAmbient(device, data.groupAmbient());
488             }
489         }
490         if (smallerVolumeAmongGroup != Integer.MAX_VALUE) {
491             for (BluetoothDevice device : mSideToDeviceMap.values()) {
492                 HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(device);
493                 if (data.groupAmbient() == INVALID_VOLUME) {
494                     // Initialize group ambient from smaller side ambient value
495                     mLocalDataManager.updateGroupAmbient(device, smallerVolumeAmongGroup);
496                 }
497             }
498         }
499         mLocalDataManager.flush();
500     }
501 
syncMuteStateIfNeeded(@ullable BluetoothDevice device, @Nullable AmbientVolumeController.RemoteAmbientState state, boolean muted)502     private void syncMuteStateIfNeeded(@Nullable BluetoothDevice device,
503             @Nullable AmbientVolumeController.RemoteAmbientState state, boolean muted) {
504         if (isDeviceConnectedToVcp(device) && state != null && state.isMutable()) {
505             if (state.isMuted() != muted) {
506                 mVolumeController.setMuted(device, muted);
507             }
508         }
509     }
510 
isDeviceConnectedToVcp(@ullable BluetoothDevice device)511     private boolean isDeviceConnectedToVcp(@Nullable BluetoothDevice device) {
512         return device != null && device.isConnected()
513                 && mProfileManager.getVolumeControlProfile().getConnectionStatus(device)
514                 == BluetoothProfile.STATE_CONNECTED;
515     }
516 
postOnMainThread(Runnable runnable)517     private void postOnMainThread(Runnable runnable) {
518         mContext.getMainThreadHandler().post(runnable);
519     }
520 
postDelayedOnMainThread(Runnable runnable, long delay)521     private void postDelayedOnMainThread(Runnable runnable, long delay) {
522         mContext.getMainThreadHandler().postDelayed(runnable, delay);
523     }
524 
showErrorToast(int stringResId)525     private void showErrorToast(int stringResId) {
526         Toast.makeText(mContext, stringResId, Toast.LENGTH_SHORT).show();
527     }
528 }
529