1/*
2 * Copyright 2014 The WebRTC Project Authors. All rights reserved.
3 *
4 * Use of this source code is governed by a BSD-style license
5 * that can be found in the LICENSE file in the root of the source
6 * tree. An additional intellectual property rights grant can be found
7 * in the file PATENTS. All contributing project authors may
8 * be found in the AUTHORS file in the root of the source tree.
9 */
10package eu.siacs.conversations.services;
11
12import android.content.BroadcastReceiver;
13import android.content.Context;
14import android.content.Intent;
15import android.content.IntentFilter;
16import android.content.pm.PackageManager;
17import android.media.AudioDeviceInfo;
18import android.media.AudioManager;
19import android.util.Log;
20
21import androidx.annotation.Nullable;
22import androidx.core.content.ContextCompat;
23
24import com.google.common.collect.ImmutableSet;
25
26import eu.siacs.conversations.Config;
27import eu.siacs.conversations.utils.AppRTCUtils;
28
29import org.webrtc.ThreadUtils;
30
31import java.util.HashSet;
32import java.util.Set;
33
34/** AppRTCAudioManager manages all audio related parts of the AppRTC demo. */
35public class AppRTCAudioManager {
36
37 private final Context apprtcContext;
38 // Contains speakerphone setting: auto, true or false
39 // Handles all tasks related to Bluetooth headset devices.
40 private final AppRTCBluetoothManager bluetoothManager;
41 @Nullable private final AudioManager audioManager;
42 @Nullable private AudioManagerEvents audioManagerEvents;
43 private AudioManagerState amState;
44 private boolean savedIsSpeakerPhoneOn;
45 private boolean savedIsMicrophoneMute;
46 private boolean hasWiredHeadset;
47 // Default audio device; speaker phone for video calls or earpiece for audio
48 // only calls.
49 private CallIntegration.AudioDevice defaultAudioDevice;
50 // Contains the currently selected audio device.
51 // This device is changed automatically using a certain scheme where e.g.
52 // a wired headset "wins" over speaker phone. It is also possible for a
53 // user to explicitly select a device (and overrid any predefined scheme).
54 // See |userSelectedAudioDevice| for details.
55 private CallIntegration.AudioDevice selectedAudioDevice;
56 // Contains the user-selected audio device which overrides the predefined
57 // selection scheme.
58 // TODO(henrika): always set to AudioDevice.NONE today. Add support for
59 // explicit selection based on choice by userSelectedAudioDevice.
60 private CallIntegration.AudioDevice userSelectedAudioDevice;
61
62 // Contains a list of available audio devices. A Set collection is used to
63 // avoid duplicate elements.
64 private Set<CallIntegration.AudioDevice> audioDevices = new HashSet<>();
65 // Broadcast receiver for wired headset intent broadcasts.
66 private final BroadcastReceiver wiredHeadsetReceiver;
67 // Callback method for changes in audio focus.
68 @Nullable private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
69
70 public AppRTCAudioManager(final Context context) {
71 apprtcContext = context;
72 audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
73 bluetoothManager = AppRTCBluetoothManager.create(context, this);
74 wiredHeadsetReceiver = new WiredHeadsetReceiver();
75 amState = AudioManagerState.UNINITIALIZED;
76 // CallIntegration / Connection uses Earpiece as default too
77 if (hasEarpiece()) {
78 defaultAudioDevice = CallIntegration.AudioDevice.EARPIECE;
79 } else {
80 defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
81 }
82 Log.d(Config.LOGTAG, "defaultAudioDevice: " + defaultAudioDevice);
83 AppRTCUtils.logDeviceInfo(Config.LOGTAG);
84 }
85
86 @SuppressWarnings("deprecation")
87 public void start(final AudioManagerEvents audioManagerEvents) {
88 Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".start()");
89 ThreadUtils.checkIsOnMainThread();
90 if (amState == AudioManagerState.RUNNING) {
91 Log.e(Config.LOGTAG, "AudioManager is already active");
92 return;
93 }
94 this.audioManagerEvents = audioManagerEvents;
95 amState = AudioManagerState.RUNNING;
96 // Store current audio state so we can restore it when stop() is called.
97 savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
98 savedIsMicrophoneMute = audioManager.isMicrophoneMute();
99 hasWiredHeadset = hasWiredHeadset();
100 // Create an AudioManager.OnAudioFocusChangeListener instance.
101 audioFocusChangeListener =
102 new AudioManager.OnAudioFocusChangeListener() {
103 // Called on the listener to notify if the audio focus for this listener has
104 // been changed.
105 // The |focusChange| value indicates whether the focus was gained, whether the
106 // focus was lost,
107 // and whether that loss is transient, or whether the new focus holder will hold
108 // it for an
109 // unknown amount of time.
110 // TODO(henrika): possibly extend support of handling audio-focus changes. Only
111 // contains
112 // logging for now.
113 @Override
114 public void onAudioFocusChange(final int focusChange) {
115 final String typeOfChange =
116 switch (focusChange) {
117 case AudioManager.AUDIOFOCUS_GAIN -> "AUDIOFOCUS_GAIN";
118 case AudioManager
119 .AUDIOFOCUS_GAIN_TRANSIENT -> "AUDIOFOCUS_GAIN_TRANSIENT";
120 case AudioManager
121 .AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -> "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE";
122 case AudioManager
123 .AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK";
124 case AudioManager.AUDIOFOCUS_LOSS -> "AUDIOFOCUS_LOSS";
125 case AudioManager
126 .AUDIOFOCUS_LOSS_TRANSIENT -> "AUDIOFOCUS_LOSS_TRANSIENT";
127 case AudioManager
128 .AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK";
129 default -> "AUDIOFOCUS_INVALID";
130 };
131 Log.d(Config.LOGTAG, "onAudioFocusChange: " + typeOfChange);
132 }
133 };
134 // Request audio playout focus (without ducking) and install listener for changes in focus.
135 int result =
136 audioManager.requestAudioFocus(
137 audioFocusChangeListener,
138 AudioManager.STREAM_VOICE_CALL,
139 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
140 if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
141 Log.d(Config.LOGTAG, "Audio focus request granted for VOICE_CALL streams");
142 } else {
143 Log.e(Config.LOGTAG, "Audio focus request failed");
144 }
145 // Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
146 // required to be in this mode when playout and/or recording starts for
147 // best possible VoIP performance.
148 audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
149 // Always disable microphone mute during a WebRTC call.
150 setMicrophoneMute(false);
151 // Set initial device states.
152 userSelectedAudioDevice = CallIntegration.AudioDevice.NONE;
153 selectedAudioDevice = CallIntegration.AudioDevice.NONE;
154 audioDevices.clear();
155 // Initialize and start Bluetooth if a BT device is available or initiate
156 // detection of new (enabled) BT devices.
157 bluetoothManager.start();
158 // Do initial selection of audio device. This setting can later be changed
159 // either by adding/removing a BT or wired headset or by covering/uncovering
160 // the proximity sensor.
161 updateAudioDeviceState();
162 // Register receiver for broadcast intents related to adding/removing a
163 // wired headset.
164 registerReceiver(wiredHeadsetReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
165 Log.d(Config.LOGTAG, "AudioManager started");
166 }
167
168 @SuppressWarnings("deprecation")
169 public void stop() {
170 Log.d(Config.LOGTAG, "appRtpAudioManager.stop()");
171 Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".stop()");
172 ThreadUtils.checkIsOnMainThread();
173 if (amState != AudioManagerState.RUNNING) {
174 Log.e(Config.LOGTAG, "Trying to stop AudioManager in incorrect state: " + amState);
175 return;
176 }
177 amState = AudioManagerState.UNINITIALIZED;
178 unregisterReceiver(wiredHeadsetReceiver);
179 bluetoothManager.stop();
180 // Restore previously stored audio states.
181 setSpeakerphoneOn(savedIsSpeakerPhoneOn);
182 setMicrophoneMute(savedIsMicrophoneMute);
183 audioManager.setMode(AudioManager.MODE_NORMAL);
184 // Abandon audio focus. Gives the previous focus owner, if any, focus.
185 audioManager.abandonAudioFocus(audioFocusChangeListener);
186 audioFocusChangeListener = null;
187 audioManagerEvents = null;
188 Log.d(Config.LOGTAG, "appRtpAudioManager.stopped()");
189 }
190
191 /** Changes selection of the currently active audio device. */
192 private void setAudioDeviceInternal(final CallIntegration.AudioDevice device) {
193 Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")");
194 AppRTCUtils.assertIsTrue(audioDevices.contains(device));
195 switch (device) {
196 case SPEAKER_PHONE -> setSpeakerphoneOn(true);
197 case EARPIECE, WIRED_HEADSET, BLUETOOTH -> setSpeakerphoneOn(false);
198 default -> Log.e(Config.LOGTAG, "Invalid audio device selection");
199 }
200 selectedAudioDevice = device;
201 }
202
203 /**
204 * Changes default audio device. TODO(henrika): add usage of this method in the AppRTCMobile
205 * client.
206 */
207 public void setDefaultAudioDevice(final CallIntegration.AudioDevice defaultDevice) {
208 ThreadUtils.checkIsOnMainThread();
209 switch (defaultDevice) {
210 case SPEAKER_PHONE -> defaultAudioDevice = defaultDevice;
211 case EARPIECE -> {
212 if (hasEarpiece()) {
213 defaultAudioDevice = defaultDevice;
214 } else {
215 defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
216 }
217 }
218 default -> Log.e(Config.LOGTAG, "Invalid default audio device selection");
219 }
220 Log.d(Config.LOGTAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")");
221 updateAudioDeviceState();
222 }
223
224 /** Changes selection of the currently active audio device. */
225 public void selectAudioDevice(final CallIntegration.AudioDevice device) {
226 ThreadUtils.checkIsOnMainThread();
227 if (!audioDevices.contains(device)) {
228 Log.e(Config.LOGTAG, "Can not select " + device + " from available " + audioDevices);
229 }
230 userSelectedAudioDevice = device;
231 updateAudioDeviceState();
232 }
233
234 /** Returns current set of available/selectable audio devices. */
235 public Set<CallIntegration.AudioDevice> getAudioDevices() {
236 return ImmutableSet.copyOf(audioDevices);
237 }
238
239 /** Returns the currently selected audio device. */
240 public CallIntegration.AudioDevice getSelectedAudioDevice() {
241 return selectedAudioDevice;
242 }
243
244 /** Helper method for receiver registration. */
245 private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
246 apprtcContext.registerReceiver(receiver, filter);
247 }
248
249 /** Helper method for unregistration of an existing receiver. */
250 private void unregisterReceiver(BroadcastReceiver receiver) {
251 apprtcContext.unregisterReceiver(receiver);
252 }
253
254 /** Sets the speaker phone mode. */
255 private void setSpeakerphoneOn(boolean on) {
256 boolean wasOn = audioManager.isSpeakerphoneOn();
257 if (wasOn == on) {
258 return;
259 }
260 audioManager.setSpeakerphoneOn(on);
261 }
262
263 /** Sets the microphone mute state. */
264 private void setMicrophoneMute(boolean on) {
265 boolean wasMuted = audioManager.isMicrophoneMute();
266 if (wasMuted == on) {
267 return;
268 }
269 audioManager.setMicrophoneMute(on);
270 }
271
272 /** Gets the current earpiece state. */
273 private boolean hasEarpiece() {
274 return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
275 }
276
277 /**
278 * Checks whether a wired headset is connected or not. This is not a valid indication that audio
279 * playback is actually over the wired headset as audio routing depends on other conditions. We
280 * only use it as an early indicator (during initialization) of an attached wired headset.
281 */
282 @Deprecated
283 private boolean hasWiredHeadset() {
284 final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
285 for (AudioDeviceInfo device : devices) {
286 final int type = device.getType();
287 if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
288 Log.d(Config.LOGTAG, "hasWiredHeadset: found wired headset");
289 return true;
290 } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) {
291 Log.d(Config.LOGTAG, "hasWiredHeadset: found USB audio device");
292 return true;
293 }
294 }
295 return false;
296 }
297
298 /**
299 * Updates list of possible audio devices and make new device selection. TODO(henrika): add unit
300 * test to verify all state transitions.
301 */
302 public void updateAudioDeviceState() {
303 ThreadUtils.checkIsOnMainThread();
304 Log.d(
305 Config.LOGTAG,
306 "--- updateAudioDeviceState: "
307 + "wired headset="
308 + hasWiredHeadset
309 + ", "
310 + "BT state="
311 + bluetoothManager.getState());
312 Log.d(
313 Config.LOGTAG,
314 "Device status: "
315 + "available="
316 + audioDevices
317 + ", "
318 + "selected="
319 + selectedAudioDevice
320 + ", "
321 + "user selected="
322 + userSelectedAudioDevice);
323 // Check if any Bluetooth headset is connected. The internal BT state will
324 // change accordingly.
325 // TODO(henrika): perhaps wrap required state into BT manager.
326 if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
327 || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE
328 || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_DISCONNECTING) {
329 bluetoothManager.updateDevice();
330 }
331 // Update the set of available audio devices.
332 Set<CallIntegration.AudioDevice> newAudioDevices = new HashSet<>();
333 if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
334 || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
335 || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
336 newAudioDevices.add(CallIntegration.AudioDevice.BLUETOOTH);
337 }
338 if (hasWiredHeadset) {
339 // If a wired headset is connected, then it is the only possible option.
340 newAudioDevices.add(CallIntegration.AudioDevice.WIRED_HEADSET);
341 } else {
342 // No wired headset, hence the audio-device list can contain speaker
343 // phone (on a tablet), or speaker phone and earpiece (on mobile phone).
344 newAudioDevices.add(CallIntegration.AudioDevice.SPEAKER_PHONE);
345 if (hasEarpiece()) {
346 newAudioDevices.add(CallIntegration.AudioDevice.EARPIECE);
347 }
348 }
349 // Store state which is set to true if the device list has changed.
350 boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices);
351 // Update the existing audio device set.
352 audioDevices = newAudioDevices;
353 // Correct user selected audio devices if needed.
354 if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE
355 && userSelectedAudioDevice == CallIntegration.AudioDevice.BLUETOOTH) {
356 // If BT is not available, it can't be the user selection.
357 userSelectedAudioDevice = CallIntegration.AudioDevice.NONE;
358 }
359 if (hasWiredHeadset
360 && userSelectedAudioDevice == CallIntegration.AudioDevice.SPEAKER_PHONE) {
361 // If user selected speaker phone, but then plugged wired headset then make
362 // wired headset as user selected device.
363 userSelectedAudioDevice = CallIntegration.AudioDevice.WIRED_HEADSET;
364 }
365 if (!hasWiredHeadset
366 && userSelectedAudioDevice == CallIntegration.AudioDevice.WIRED_HEADSET) {
367 // If user selected wired headset, but then unplugged wired headset then make
368 // speaker phone as user selected device.
369 userSelectedAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
370 }
371 // Need to start Bluetooth if it is available and user either selected it explicitly or
372 // user did not select any output device.
373 boolean needBluetoothAudioStart =
374 bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
375 && (userSelectedAudioDevice == CallIntegration.AudioDevice.NONE
376 || userSelectedAudioDevice
377 == CallIntegration.AudioDevice.BLUETOOTH);
378 // Need to stop Bluetooth audio if user selected different device and
379 // Bluetooth SCO connection is established or in the process.
380 boolean needBluetoothAudioStop =
381 (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
382 || bluetoothManager.getState()
383 == AppRTCBluetoothManager.State.SCO_CONNECTING)
384 && (userSelectedAudioDevice != CallIntegration.AudioDevice.NONE
385 && userSelectedAudioDevice
386 != CallIntegration.AudioDevice.BLUETOOTH);
387 if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
388 || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
389 || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
390 Log.d(
391 Config.LOGTAG,
392 "Need BT audio: start="
393 + needBluetoothAudioStart
394 + ", "
395 + "stop="
396 + needBluetoothAudioStop
397 + ", "
398 + "BT state="
399 + bluetoothManager.getState());
400 }
401 // Start or stop Bluetooth SCO connection given states set earlier.
402 if (needBluetoothAudioStop) {
403 bluetoothManager.stopScoAudio();
404 bluetoothManager.updateDevice();
405 }
406 if (needBluetoothAudioStart && !needBluetoothAudioStop) {
407 // Attempt to start Bluetooth SCO audio (takes a few second to start).
408 if (!bluetoothManager.startScoAudio()) {
409 // Remove BLUETOOTH from list of available devices since SCO failed.
410 audioDevices.remove(CallIntegration.AudioDevice.BLUETOOTH);
411 audioDeviceSetUpdated = true;
412 }
413 }
414 // Update selected audio device.
415 final CallIntegration.AudioDevice newAudioDevice;
416 if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
417 // If a Bluetooth is connected, then it should be used as output audio
418 // device. Note that it is not sufficient that a headset is available;
419 // an active SCO channel must also be up and running.
420 newAudioDevice = CallIntegration.AudioDevice.BLUETOOTH;
421 } else if (hasWiredHeadset) {
422 // If a wired headset is connected, but Bluetooth is not, then wired headset is used as
423 // audio device.
424 newAudioDevice = CallIntegration.AudioDevice.WIRED_HEADSET;
425 } else {
426 // No wired headset and no Bluetooth, hence the audio-device list can contain speaker
427 // phone (on a tablet), or speaker phone and earpiece (on mobile phone).
428 // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or
429 // AudioDevice.EARPIECE
430 // depending on the user's selection.
431 newAudioDevice = defaultAudioDevice;
432 }
433 // Switch to new device but only if there has been any changes.
434 if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {
435 // Do the required device switch.
436 setAudioDeviceInternal(newAudioDevice);
437 Log.d(
438 Config.LOGTAG,
439 "New device status: "
440 + "available="
441 + audioDevices
442 + ", "
443 + "selected="
444 + newAudioDevice);
445 if (audioManagerEvents != null) {
446 // Notify a listening client that audio device has been changed.
447 audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices);
448 }
449 }
450 Log.d(Config.LOGTAG, "--- updateAudioDeviceState done");
451 }
452
453 public void executeOnMain(final Runnable runnable) {
454 ContextCompat.getMainExecutor(apprtcContext).execute(runnable);
455 }
456
457 /** AudioManager state. */
458 public enum AudioManagerState {
459 UNINITIALIZED,
460 PREINITIALIZED,
461 RUNNING,
462 }
463
464 /** Selected audio device change event. */
465 public interface AudioManagerEvents {
466 // Callback fired once audio device is changed or list of available audio devices changed.
467 void onAudioDeviceChanged(
468 CallIntegration.AudioDevice selectedAudioDevice,
469 Set<CallIntegration.AudioDevice> availableAudioDevices);
470 }
471
472 /* Receiver which handles changes in wired headset availability. */
473 private class WiredHeadsetReceiver extends BroadcastReceiver {
474 private static final int STATE_UNPLUGGED = 0;
475 private static final int STATE_PLUGGED = 1;
476 private static final int HAS_NO_MIC = 0;
477 private static final int HAS_MIC = 1;
478
479 @Override
480 public void onReceive(Context context, Intent intent) {
481 int state = intent.getIntExtra("state", STATE_UNPLUGGED);
482 int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
483 String name = intent.getStringExtra("name");
484 Log.d(
485 Config.LOGTAG,
486 "WiredHeadsetReceiver.onReceive"
487 + AppRTCUtils.getThreadInfo()
488 + ": "
489 + "a="
490 + intent.getAction()
491 + ", s="
492 + (state == STATE_UNPLUGGED ? "unplugged" : "plugged")
493 + ", m="
494 + (microphone == HAS_MIC ? "mic" : "no mic")
495 + ", n="
496 + name
497 + ", sb="
498 + isInitialStickyBroadcast());
499 hasWiredHeadset = (state == STATE_PLUGGED);
500 updateAudioDeviceState();
501 }
502 }
503}