• 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_DISABLED;
20 import static android.bluetooth.AudioInputControl.MUTE_MUTED;
21 import static android.bluetooth.AudioInputControl.MUTE_NOT_MUTED;
22 
23 import static com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data.INVALID_VOLUME;
24 
25 import android.bluetooth.AudioInputControl;
26 import android.bluetooth.BluetoothDevice;
27 import android.bluetooth.BluetoothProfile;
28 import android.util.ArrayMap;
29 import android.util.Log;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 
34 import java.util.Collections;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.concurrent.Executor;
38 
39 /**
40  * AmbientVolumeController manages the {@link AudioInputControl}s of
41  * {@link AudioInputControl#AUDIO_INPUT_TYPE_AMBIENT} on the remote device.
42  */
43 public class AmbientVolumeController implements LocalBluetoothProfileManager.ServiceListener {
44 
45     private static final boolean DEBUG = true;
46     private static final String TAG = "AmbientController";
47 
48     private final LocalBluetoothProfileManager mProfileManager;
49     private final VolumeControlProfile mVolumeControlProfile;
50     private final Map<BluetoothDevice, List<AudioInputControl>> mDeviceAmbientControlsMap =
51             new ArrayMap<>();
52     private final Map<BluetoothDevice, AmbientCallback> mDeviceCallbackMap = new ArrayMap<>();
53     final Map<BluetoothDevice, RemoteAmbientState> mDeviceAmbientStateMap =
54             new ArrayMap<>();
55     @Nullable
56     private final AmbientVolumeControlCallback mCallback;
57 
AmbientVolumeController( @onNull LocalBluetoothProfileManager profileManager, @Nullable AmbientVolumeControlCallback callback)58     public AmbientVolumeController(
59             @NonNull LocalBluetoothProfileManager profileManager,
60             @Nullable AmbientVolumeControlCallback callback) {
61         mProfileManager = profileManager;
62         mVolumeControlProfile = profileManager.getVolumeControlProfile();
63         if (mVolumeControlProfile != null && !mVolumeControlProfile.isProfileReady()) {
64             mProfileManager.addServiceListener(this);
65         }
66         mCallback = callback;
67     }
68 
69     @Override
onServiceConnected()70     public void onServiceConnected() {
71         if (mVolumeControlProfile != null && mVolumeControlProfile.isProfileReady()) {
72             mProfileManager.removeServiceListener(this);
73             if (mCallback != null) {
74                 mCallback.onVolumeControlServiceConnected();
75             }
76         }
77     }
78 
79     @Override
onServiceDisconnected()80     public void onServiceDisconnected() {
81         // Do nothing
82     }
83 
84     /**
85      * Registers the same {@link AmbientCallback} on all ambient control points of the remote
86      * device. The {@link AmbientCallback} will pass the event to registered
87      * {@link AmbientVolumeControlCallback} if exists.
88      *
89      * @param executor the executor to run the callback
90      * @param device the remote device
91      */
registerCallback(@onNull Executor executor, @NonNull BluetoothDevice device)92     public void registerCallback(@NonNull Executor executor, @NonNull BluetoothDevice device) {
93         AmbientCallback ambientCallback = new AmbientCallback(device, mCallback);
94         synchronized (mDeviceCallbackMap) {
95             mDeviceCallbackMap.put(device, ambientCallback);
96         }
97 
98         // register callback on all ambient input control points of this device
99         List<AudioInputControl> controls = getAmbientControls(device);
100         controls.forEach((control) -> {
101             try {
102                 control.registerCallback(executor, ambientCallback);
103             } catch (IllegalArgumentException e) {
104                 // The callback was already registered
105                 Log.i(TAG, "Skip registering the callback, " + e.getMessage());
106             }
107         });
108     }
109 
110     /**
111      * Unregisters the {@link AmbientCallback} on all ambient control points of the remote
112      * device which is previously registered with {@link #registerCallback}.
113      *
114      * @param device the remote device
115      */
unregisterCallback(@onNull BluetoothDevice device)116     public void unregisterCallback(@NonNull BluetoothDevice device) {
117         AmbientCallback ambientCallback;
118         synchronized (mDeviceCallbackMap) {
119             ambientCallback = mDeviceCallbackMap.remove(device);
120         }
121         if (ambientCallback == null) {
122             // callback not found, no need to unregister
123             return;
124         }
125 
126         // unregister callback on all ambient input control points of this device
127         List<AudioInputControl> controls = getAmbientControls(device);
128         controls.forEach(control -> {
129             try {
130                 control.unregisterCallback(ambientCallback);
131             } catch (IllegalArgumentException e) {
132                 // The callback was never registered or was already unregistered
133                 Log.i(TAG, "Skip unregistering the callback, " + e.getMessage());
134             }
135         });
136     }
137 
138     /**
139      * Gets the gain setting max value from first ambient control point of the remote device.
140      *
141      * @param device the remote device
142      */
getAmbientMax(@onNull BluetoothDevice device)143     public int getAmbientMax(@NonNull BluetoothDevice device) {
144         List<AudioInputControl> ambientControls = getAmbientControls(device);
145         int value = INVALID_VOLUME;
146         if (!ambientControls.isEmpty()) {
147             value = ambientControls.getFirst().getGainSettingMax();
148         }
149         return value;
150     }
151 
152     /**
153      * Gets the gain setting min value from first ambient control point of the remote device.
154      *
155      * @param device the remote device
156      */
getAmbientMin(@onNull BluetoothDevice device)157     public int getAmbientMin(@NonNull BluetoothDevice device) {
158         List<AudioInputControl> ambientControls = getAmbientControls(device);
159         int value = INVALID_VOLUME;
160         if (!ambientControls.isEmpty()) {
161             value = ambientControls.getFirst().getGainSettingMin();
162         }
163         return value;
164     }
165 
166     /**
167      * Gets the latest values in {@link RemoteAmbientState}.
168      *
169      * @param device the remote device
170      * @return the {@link RemoteAmbientState} represents current remote ambient control point state
171      */
172     @Nullable
refreshAmbientState(@ullable BluetoothDevice device)173     public RemoteAmbientState refreshAmbientState(@Nullable BluetoothDevice device) {
174         if (device == null || !device.isConnected()) {
175             return null;
176         }
177         int gainSetting = getAmbient(device);
178         int mute = getMute(device);
179         return new RemoteAmbientState(gainSetting, mute);
180     }
181 
182     /**
183      * Gets the gain setting value from first ambient control point of the remote device and
184      * stores it in cached {@link RemoteAmbientState}.
185      *
186      * When any audio input point receives {@link AmbientCallback#onGainSettingChanged(int)}
187      * callback, only the changed value which is different from the value stored in the cached
188      * state will be notified to the {@link AmbientVolumeControlCallback} of this controller.
189      *
190      * @param device the remote device
191      */
getAmbient(@onNull BluetoothDevice device)192     public int getAmbient(@NonNull BluetoothDevice device) {
193         List<AudioInputControl> ambientControls = getAmbientControls(device);
194         int value = INVALID_VOLUME;
195         if (!ambientControls.isEmpty()) {
196             synchronized (mDeviceAmbientStateMap) {
197                 value = ambientControls.getFirst().getGainSetting();
198                 RemoteAmbientState state = mDeviceAmbientStateMap.getOrDefault(device,
199                         new RemoteAmbientState(INVALID_VOLUME, MUTE_DISABLED));
200                 RemoteAmbientState updatedState = new RemoteAmbientState(value, state.mute);
201                 mDeviceAmbientStateMap.put(device, updatedState);
202             }
203         }
204         return value;
205     }
206 
207     /**
208      * Sets the gain setting value to all ambient control points of the remote device.
209      *
210      * @param device the remote device
211      * @param value the gain setting value to be updated
212      */
setAmbient(@onNull BluetoothDevice device, int value)213     public void setAmbient(@NonNull BluetoothDevice device, int value) {
214         if (DEBUG) {
215             Log.d(TAG, "setAmbient, value:" + value + ", device:" + device);
216         }
217         List<AudioInputControl> ambientControls = getAmbientControls(device);
218         ambientControls.forEach(control -> control.setGainSetting(value));
219     }
220 
221     /**
222      * Gets the mute state from first ambient control point of the remote device and
223      * stores it in cached {@link RemoteAmbientState}. The value will be one of
224      * {@link AudioInputControl.Mute}.
225      *
226      * When any audio input point receives {@link AmbientCallback#onMuteChanged(int)} callback,
227      * only the changed value which is different from the value stored in the cached state will
228      * be notified to the {@link AmbientVolumeControlCallback} of this controller.
229      *
230      * @param device the remote device
231      */
getMute(@onNull BluetoothDevice device)232     public int getMute(@NonNull BluetoothDevice device) {
233         List<AudioInputControl> ambientControls = getAmbientControls(device);
234         int value = MUTE_DISABLED;
235         if (!ambientControls.isEmpty()) {
236             synchronized (mDeviceAmbientStateMap) {
237                 value = ambientControls.getFirst().getMute();
238                 RemoteAmbientState state = mDeviceAmbientStateMap.getOrDefault(device,
239                         new RemoteAmbientState(INVALID_VOLUME, MUTE_DISABLED));
240                 RemoteAmbientState updatedState = new RemoteAmbientState(state.gainSetting, value);
241                 mDeviceAmbientStateMap.put(device, updatedState);
242             }
243         }
244         return value;
245     }
246 
247     /**
248      * Sets the mute state to all ambient control points of the remote device.
249      *
250      * @param device the remote device
251      * @param muted the mute state to be updated
252      */
setMuted(@onNull BluetoothDevice device, boolean muted)253     public void setMuted(@NonNull BluetoothDevice device, boolean muted) {
254         if (DEBUG) {
255             Log.d(TAG, "setMuted, muted:" + muted + ", device:" + device);
256         }
257         List<AudioInputControl> ambientControls = getAmbientControls(device);
258         ambientControls.forEach(control -> {
259             try {
260                 control.setMute(muted ? MUTE_MUTED : MUTE_NOT_MUTED);
261             } catch (IllegalStateException e) {
262                 // Sometimes remote will throw this exception due to initialization not done
263                 // yet. Catch it to prevent crashes on UI.
264                 Log.w(TAG, "Remote mute state is currently disabled.");
265             }
266         });
267     }
268 
269     /**
270      * Checks if there's any valid ambient control point exists on the remote device
271      *
272      * @param device the remote device
273      */
isAmbientControlAvailable(@onNull BluetoothDevice device)274     public boolean isAmbientControlAvailable(@NonNull BluetoothDevice device) {
275         final boolean hasAmbientControlPoint = !getAmbientControls(device).isEmpty();
276         final boolean connectedToVcp = mVolumeControlProfile.getConnectionStatus(device)
277                 == BluetoothProfile.STATE_CONNECTED;
278         return hasAmbientControlPoint && connectedToVcp;
279     }
280 
281     @NonNull
getAmbientControls(@onNull BluetoothDevice device)282     private List<AudioInputControl> getAmbientControls(@NonNull BluetoothDevice device) {
283         if (mVolumeControlProfile == null) {
284             return Collections.emptyList();
285         }
286         synchronized (mDeviceAmbientControlsMap) {
287             if (mDeviceAmbientControlsMap.containsKey(device)) {
288                 return mDeviceAmbientControlsMap.get(device);
289             }
290             List<AudioInputControl> ambientControls =
291                     mVolumeControlProfile.getAudioInputControlServices(device).stream().filter(
292                             this::isValidAmbientControl).toList();
293             if (!ambientControls.isEmpty()) {
294                 mDeviceAmbientControlsMap.put(device, ambientControls);
295             }
296             return ambientControls;
297         }
298     }
299 
isValidAmbientControl(AudioInputControl control)300     private boolean isValidAmbientControl(AudioInputControl control) {
301         boolean isAmbientControl =
302                 control.getAudioInputType() == AudioInputControl.AUDIO_INPUT_TYPE_AMBIENT;
303         boolean isManual = control.getGainMode() == AudioInputControl.GAIN_MODE_MANUAL
304                 || control.getGainMode() == AudioInputControl.GAIN_MODE_MANUAL_ONLY;
305         boolean isActive =
306                 control.getAudioInputStatus() == AudioInputControl.AUDIO_INPUT_STATUS_ACTIVE;
307 
308         return isAmbientControl && isManual && isActive;
309     }
310 
311     /**
312      * Callback providing information about the status and received events of
313      * {@link AmbientVolumeController}.
314      */
315     public interface AmbientVolumeControlCallback {
316 
317         /** This method is called when the Volume Control Service is connected */
onVolumeControlServiceConnected()318         default void onVolumeControlServiceConnected() {
319         }
320 
321         /**
322          * This method is called when one of the remote device's ambient control point's gain
323          * settings value is changed.
324          *
325          * @param device the remote device
326          * @param gainSettings the new gain setting value
327          */
onAmbientChanged(@onNull BluetoothDevice device, int gainSettings)328         default void onAmbientChanged(@NonNull BluetoothDevice device, int gainSettings) {
329         }
330 
331         /**
332          * This method is called when one of the remote device's ambient control point's mute
333          * state is changed.
334          *
335          * @param device the remote device
336          * @param mute the new mute state
337          */
onMuteChanged(@onNull BluetoothDevice device, int mute)338         default void onMuteChanged(@NonNull BluetoothDevice device, int mute) {
339         }
340 
341         /**
342          * This method is called when any command to the remote device's ambient control point
343          * is failed.
344          *
345          * @param device the remote device.
346          */
onCommandFailed(@onNull BluetoothDevice device)347         default void onCommandFailed(@NonNull BluetoothDevice device) {
348         }
349     }
350 
351     /**
352      * A wrapper callback that will pass {@link AudioInputControl.AudioInputCallback} with extra
353      * device information to {@link AmbientVolumeControlCallback}.
354      */
355     class AmbientCallback implements AudioInputControl.AudioInputCallback {
356 
357         private final BluetoothDevice mDevice;
358         private final AmbientVolumeControlCallback mCallback;
359 
AmbientCallback(@onNull BluetoothDevice device, @Nullable AmbientVolumeControlCallback callback)360         AmbientCallback(@NonNull BluetoothDevice device,
361                 @Nullable AmbientVolumeControlCallback callback) {
362             mDevice = device;
363             mCallback = callback;
364         }
365 
366         @Override
onGainSettingChanged(int gainSetting)367         public void onGainSettingChanged(int gainSetting) {
368             if (mCallback != null) {
369                 synchronized (mDeviceAmbientStateMap) {
370                     RemoteAmbientState previousState = mDeviceAmbientStateMap.get(mDevice);
371                     if (previousState.gainSetting != gainSetting) {
372                         mCallback.onAmbientChanged(mDevice, gainSetting);
373                     }
374                 }
375             }
376         }
377 
378         @Override
onSetGainSettingFailed()379         public void onSetGainSettingFailed() {
380             Log.w(TAG, "onSetGainSettingFailed, device=" + mDevice);
381             if (mCallback != null) {
382                 mCallback.onCommandFailed(mDevice);
383             }
384         }
385 
386         @Override
onMuteChanged(int mute)387         public void onMuteChanged(int mute) {
388             if (mCallback != null) {
389                 synchronized (mDeviceAmbientStateMap) {
390                     RemoteAmbientState previousState = mDeviceAmbientStateMap.get(mDevice);
391                     if (previousState.mute != mute) {
392                         mCallback.onMuteChanged(mDevice, mute);
393                     }
394                 }
395             }
396         }
397 
398         @Override
onSetMuteFailed()399         public void onSetMuteFailed() {
400             Log.w(TAG, "onSetMuteFailed, device=" + mDevice);
401             if (mCallback != null) {
402                 mCallback.onCommandFailed(mDevice);
403             }
404         }
405     }
406 
RemoteAmbientState(int gainSetting, int mute)407     public record RemoteAmbientState(int gainSetting, int mute) {
408         public boolean isMutable() {
409             return mute != MUTE_DISABLED;
410         }
411         public boolean isMuted() {
412             return mute == MUTE_MUTED;
413         }
414     }
415 }
416