2020-04-13 10:02:34 +00:00
|
|
|
/*
|
|
|
|
* Copyright 2014 The WebRTC Project Authors. All rights reserved.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by a BSD-style license
|
|
|
|
* that can be found in the LICENSE file in the root of the source
|
|
|
|
* tree. An additional intellectual property rights grant can be found
|
|
|
|
* in the file PATENTS. All contributing project authors may
|
|
|
|
* be found in the AUTHORS file in the root of the source tree.
|
|
|
|
*/
|
|
|
|
package eu.siacs.conversations.services;
|
|
|
|
|
|
|
|
import android.content.BroadcastReceiver;
|
|
|
|
import android.content.Context;
|
|
|
|
import android.content.Intent;
|
|
|
|
import android.content.IntentFilter;
|
|
|
|
import android.content.pm.PackageManager;
|
|
|
|
import android.media.AudioDeviceInfo;
|
2020-04-18 15:51:21 +00:00
|
|
|
import android.media.AudioFormat;
|
2020-04-13 10:02:34 +00:00
|
|
|
import android.media.AudioManager;
|
2020-04-18 15:51:21 +00:00
|
|
|
import android.media.AudioRecord;
|
|
|
|
import android.media.MediaRecorder;
|
2020-04-13 10:02:34 +00:00
|
|
|
import android.os.Build;
|
|
|
|
import android.support.annotation.Nullable;
|
|
|
|
import android.util.Log;
|
|
|
|
|
|
|
|
import org.webrtc.ThreadUtils;
|
|
|
|
|
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.HashSet;
|
|
|
|
import java.util.Set;
|
2020-04-19 06:49:08 +00:00
|
|
|
import java.util.concurrent.CountDownLatch;
|
2020-04-13 10:02:34 +00:00
|
|
|
|
|
|
|
import eu.siacs.conversations.Config;
|
|
|
|
import eu.siacs.conversations.utils.AppRTCUtils;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* AppRTCAudioManager manages all audio related parts of the AppRTC demo.
|
|
|
|
*/
|
|
|
|
public class AppRTCAudioManager {
|
2020-04-19 06:49:08 +00:00
|
|
|
|
|
|
|
private static CountDownLatch microphoneLatch;
|
|
|
|
|
2020-04-13 10:02:34 +00:00
|
|
|
private final Context apprtcContext;
|
|
|
|
// Contains speakerphone setting: auto, true or false
|
|
|
|
@Nullable
|
|
|
|
private final SpeakerPhonePreference speakerPhonePreference;
|
|
|
|
// Handles all tasks related to Bluetooth headset devices.
|
|
|
|
private final AppRTCBluetoothManager bluetoothManager;
|
|
|
|
@Nullable
|
|
|
|
private AudioManager audioManager;
|
|
|
|
@Nullable
|
|
|
|
private AudioManagerEvents audioManagerEvents;
|
|
|
|
private AudioManagerState amState;
|
|
|
|
private int savedAudioMode = AudioManager.MODE_INVALID;
|
|
|
|
private boolean savedIsSpeakerPhoneOn;
|
|
|
|
private boolean savedIsMicrophoneMute;
|
|
|
|
private boolean hasWiredHeadset;
|
|
|
|
// Default audio device; speaker phone for video calls or earpiece for audio
|
|
|
|
// only calls.
|
|
|
|
private AudioDevice defaultAudioDevice;
|
|
|
|
// Contains the currently selected audio device.
|
|
|
|
// This device is changed automatically using a certain scheme where e.g.
|
|
|
|
// a wired headset "wins" over speaker phone. It is also possible for a
|
|
|
|
// user to explicitly select a device (and overrid any predefined scheme).
|
|
|
|
// See |userSelectedAudioDevice| for details.
|
|
|
|
private AudioDevice selectedAudioDevice;
|
|
|
|
// Contains the user-selected audio device which overrides the predefined
|
|
|
|
// selection scheme.
|
|
|
|
// TODO(henrika): always set to AudioDevice.NONE today. Add support for
|
|
|
|
// explicit selection based on choice by userSelectedAudioDevice.
|
|
|
|
private AudioDevice userSelectedAudioDevice;
|
|
|
|
// Proximity sensor object. It measures the proximity of an object in cm
|
|
|
|
// relative to the view screen of a device and can therefore be used to
|
|
|
|
// assist device switching (close to ear <=> use headset earpiece if
|
|
|
|
// available, far from ear <=> use speaker phone).
|
|
|
|
@Nullable
|
|
|
|
private AppRTCProximitySensor proximitySensor;
|
|
|
|
// Contains a list of available audio devices. A Set collection is used to
|
|
|
|
// avoid duplicate elements.
|
|
|
|
private Set<AudioDevice> audioDevices = new HashSet<>();
|
|
|
|
// Broadcast receiver for wired headset intent broadcasts.
|
|
|
|
private BroadcastReceiver wiredHeadsetReceiver;
|
|
|
|
// Callback method for changes in audio focus.
|
|
|
|
@Nullable
|
|
|
|
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
|
|
|
|
|
|
|
|
private AppRTCAudioManager(Context context, final SpeakerPhonePreference speakerPhonePreference) {
|
|
|
|
Log.d(Config.LOGTAG, "ctor");
|
|
|
|
ThreadUtils.checkIsOnMainThread();
|
|
|
|
apprtcContext = context;
|
|
|
|
audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
|
|
|
|
bluetoothManager = AppRTCBluetoothManager.create(context, this);
|
|
|
|
wiredHeadsetReceiver = new WiredHeadsetReceiver();
|
|
|
|
amState = AudioManagerState.UNINITIALIZED;
|
|
|
|
Log.d(Config.LOGTAG, "speaker phone preference: " + speakerPhonePreference);
|
|
|
|
this.speakerPhonePreference = speakerPhonePreference;
|
|
|
|
if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE) {
|
|
|
|
defaultAudioDevice = AudioDevice.EARPIECE;
|
|
|
|
} else {
|
|
|
|
defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
|
|
|
|
}
|
|
|
|
// Create and initialize the proximity sensor.
|
|
|
|
// Tablet devices (e.g. Nexus 7) does not support proximity sensors.
|
|
|
|
// Note that, the sensor will not be active until start() has been called.
|
|
|
|
proximitySensor = AppRTCProximitySensor.create(context,
|
|
|
|
// This method will be called each time a state change is detected.
|
|
|
|
// Example: user holds his hand over the device (closer than ~5 cm),
|
|
|
|
// or removes his hand from the device.
|
|
|
|
this::onProximitySensorChangedState);
|
|
|
|
Log.d(Config.LOGTAG, "defaultAudioDevice: " + defaultAudioDevice);
|
|
|
|
AppRTCUtils.logDeviceInfo(Config.LOGTAG);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Construction.
|
|
|
|
*/
|
|
|
|
public static AppRTCAudioManager create(Context context, SpeakerPhonePreference speakerPhonePreference) {
|
|
|
|
return new AppRTCAudioManager(context, speakerPhonePreference);
|
|
|
|
}
|
|
|
|
|
2020-04-19 06:49:08 +00:00
|
|
|
public static boolean isMicrophoneAvailable() {
|
|
|
|
microphoneLatch = new CountDownLatch(1);
|
2020-04-18 15:51:21 +00:00
|
|
|
AudioRecord audioRecord = null;
|
|
|
|
boolean available = true;
|
|
|
|
try {
|
|
|
|
final int sampleRate = 44100;
|
|
|
|
final int channel = AudioFormat.CHANNEL_IN_MONO;
|
|
|
|
final int format = AudioFormat.ENCODING_PCM_16BIT;
|
|
|
|
final int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, format);
|
|
|
|
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channel, format, bufferSize);
|
|
|
|
audioRecord.startRecording();
|
|
|
|
final short[] buffer = new short[bufferSize];
|
|
|
|
final int audioStatus = audioRecord.read(buffer, 0, bufferSize);
|
|
|
|
if (audioStatus == AudioRecord.ERROR_INVALID_OPERATION || audioStatus == AudioRecord.STATE_UNINITIALIZED)
|
|
|
|
available = false;
|
|
|
|
} catch (Exception e) {
|
|
|
|
available = false;
|
|
|
|
} finally {
|
|
|
|
release(audioRecord);
|
|
|
|
|
|
|
|
}
|
2020-04-19 06:49:08 +00:00
|
|
|
microphoneLatch.countDown();
|
2020-04-18 15:51:21 +00:00
|
|
|
return available;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static void release(final AudioRecord audioRecord) {
|
|
|
|
if (audioRecord == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
audioRecord.release();
|
|
|
|
} catch (Exception e) {
|
|
|
|
//ignore
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-13 10:02:34 +00:00
|
|
|
/**
|
|
|
|
* This method is called when the proximity sensor reports a state change,
|
|
|
|
* e.g. from "NEAR to FAR" or from "FAR to NEAR".
|
|
|
|
*/
|
|
|
|
private void onProximitySensorChangedState() {
|
|
|
|
if (speakerPhonePreference != SpeakerPhonePreference.AUTO) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// The proximity sensor should only be activated when there are exactly two
|
|
|
|
// available audio devices.
|
|
|
|
if (audioDevices.size() == 2 && audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE)
|
|
|
|
&& audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) {
|
|
|
|
if (proximitySensor.sensorReportsNearState()) {
|
|
|
|
// Sensor reports that a "handset is being held up to a person's ear",
|
|
|
|
// or "something is covering the light sensor".
|
|
|
|
setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.EARPIECE);
|
|
|
|
} else {
|
|
|
|
// Sensor reports that a "handset is removed from a person's ear", or
|
|
|
|
// "the light sensor is no longer covered".
|
|
|
|
setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressWarnings("deprecation")
|
|
|
|
// TODO(henrika): audioManager.requestAudioFocus() is deprecated.
|
|
|
|
public void start(AudioManagerEvents audioManagerEvents) {
|
|
|
|
Log.d(Config.LOGTAG, "start");
|
|
|
|
ThreadUtils.checkIsOnMainThread();
|
|
|
|
if (amState == AudioManagerState.RUNNING) {
|
|
|
|
Log.e(Config.LOGTAG, "AudioManager is already active");
|
|
|
|
return;
|
|
|
|
}
|
2020-04-19 06:49:08 +00:00
|
|
|
awaitMicrophoneLatch();
|
2020-04-13 10:02:34 +00:00
|
|
|
// TODO(henrika): perhaps call new method called preInitAudio() here if UNINITIALIZED.
|
|
|
|
Log.d(Config.LOGTAG, "AudioManager starts...");
|
|
|
|
this.audioManagerEvents = audioManagerEvents;
|
|
|
|
amState = AudioManagerState.RUNNING;
|
|
|
|
// Store current audio state so we can restore it when stop() is called.
|
|
|
|
savedAudioMode = audioManager.getMode();
|
|
|
|
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
|
|
|
|
savedIsMicrophoneMute = audioManager.isMicrophoneMute();
|
|
|
|
hasWiredHeadset = hasWiredHeadset();
|
|
|
|
// Create an AudioManager.OnAudioFocusChangeListener instance.
|
|
|
|
audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
|
|
|
|
// Called on the listener to notify if the audio focus for this listener has been changed.
|
|
|
|
// The |focusChange| value indicates whether the focus was gained, whether the focus was lost,
|
|
|
|
// and whether that loss is transient, or whether the new focus holder will hold it for an
|
|
|
|
// unknown amount of time.
|
|
|
|
// TODO(henrika): possibly extend support of handling audio-focus changes. Only contains
|
|
|
|
// logging for now.
|
|
|
|
@Override
|
|
|
|
public void onAudioFocusChange(int focusChange) {
|
|
|
|
final String typeOfChange;
|
|
|
|
switch (focusChange) {
|
|
|
|
case AudioManager.AUDIOFOCUS_GAIN:
|
|
|
|
typeOfChange = "AUDIOFOCUS_GAIN";
|
|
|
|
break;
|
|
|
|
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
|
|
|
|
typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT";
|
|
|
|
break;
|
|
|
|
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
|
|
|
|
typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE";
|
|
|
|
break;
|
|
|
|
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
|
|
|
|
typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK";
|
|
|
|
break;
|
|
|
|
case AudioManager.AUDIOFOCUS_LOSS:
|
|
|
|
typeOfChange = "AUDIOFOCUS_LOSS";
|
|
|
|
break;
|
|
|
|
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
|
|
|
typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT";
|
|
|
|
break;
|
|
|
|
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
|
|
|
typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK";
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
typeOfChange = "AUDIOFOCUS_INVALID";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
Log.d(Config.LOGTAG, "onAudioFocusChange: " + typeOfChange);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// Request audio playout focus (without ducking) and install listener for changes in focus.
|
|
|
|
int result = audioManager.requestAudioFocus(audioFocusChangeListener,
|
|
|
|
AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
|
|
|
|
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
|
|
|
Log.d(Config.LOGTAG, "Audio focus request granted for VOICE_CALL streams");
|
|
|
|
} else {
|
|
|
|
Log.e(Config.LOGTAG, "Audio focus request failed");
|
|
|
|
}
|
|
|
|
// Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
|
|
|
|
// required to be in this mode when playout and/or recording starts for
|
|
|
|
// best possible VoIP performance.
|
|
|
|
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
|
|
|
|
// Always disable microphone mute during a WebRTC call.
|
|
|
|
setMicrophoneMute(false);
|
|
|
|
// Set initial device states.
|
|
|
|
userSelectedAudioDevice = AudioDevice.NONE;
|
|
|
|
selectedAudioDevice = AudioDevice.NONE;
|
|
|
|
audioDevices.clear();
|
|
|
|
// Initialize and start Bluetooth if a BT device is available or initiate
|
|
|
|
// detection of new (enabled) BT devices.
|
|
|
|
bluetoothManager.start();
|
|
|
|
// Do initial selection of audio device. This setting can later be changed
|
|
|
|
// either by adding/removing a BT or wired headset or by covering/uncovering
|
|
|
|
// the proximity sensor.
|
|
|
|
updateAudioDeviceState();
|
|
|
|
// Register receiver for broadcast intents related to adding/removing a
|
|
|
|
// wired headset.
|
|
|
|
registerReceiver(wiredHeadsetReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
|
|
|
|
Log.d(Config.LOGTAG, "AudioManager started");
|
|
|
|
}
|
|
|
|
|
2020-04-19 06:49:08 +00:00
|
|
|
private void awaitMicrophoneLatch() {
|
|
|
|
final CountDownLatch latch = microphoneLatch;
|
|
|
|
if (latch == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
latch.await();
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
//ignore
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-13 10:02:34 +00:00
|
|
|
@SuppressWarnings("deprecation")
|
|
|
|
// TODO(henrika): audioManager.abandonAudioFocus() is deprecated.
|
|
|
|
public void stop() {
|
|
|
|
Log.d(Config.LOGTAG, "stop");
|
|
|
|
ThreadUtils.checkIsOnMainThread();
|
|
|
|
if (amState != AudioManagerState.RUNNING) {
|
|
|
|
Log.e(Config.LOGTAG, "Trying to stop AudioManager in incorrect state: " + amState);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
amState = AudioManagerState.UNINITIALIZED;
|
|
|
|
unregisterReceiver(wiredHeadsetReceiver);
|
|
|
|
bluetoothManager.stop();
|
|
|
|
// Restore previously stored audio states.
|
|
|
|
setSpeakerphoneOn(savedIsSpeakerPhoneOn);
|
|
|
|
setMicrophoneMute(savedIsMicrophoneMute);
|
|
|
|
audioManager.setMode(savedAudioMode);
|
|
|
|
// Abandon audio focus. Gives the previous focus owner, if any, focus.
|
|
|
|
audioManager.abandonAudioFocus(audioFocusChangeListener);
|
|
|
|
audioFocusChangeListener = null;
|
|
|
|
Log.d(Config.LOGTAG, "Abandoned audio focus for VOICE_CALL streams");
|
|
|
|
if (proximitySensor != null) {
|
|
|
|
proximitySensor.stop();
|
|
|
|
proximitySensor = null;
|
|
|
|
}
|
|
|
|
audioManagerEvents = null;
|
|
|
|
Log.d(Config.LOGTAG, "AudioManager stopped");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Changes selection of the currently active audio device.
|
|
|
|
*/
|
|
|
|
private void setAudioDeviceInternal(AudioDevice device) {
|
|
|
|
Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")");
|
|
|
|
AppRTCUtils.assertIsTrue(audioDevices.contains(device));
|
|
|
|
switch (device) {
|
|
|
|
case SPEAKER_PHONE:
|
|
|
|
setSpeakerphoneOn(true);
|
|
|
|
break;
|
|
|
|
case EARPIECE:
|
|
|
|
setSpeakerphoneOn(false);
|
|
|
|
break;
|
|
|
|
case WIRED_HEADSET:
|
|
|
|
setSpeakerphoneOn(false);
|
|
|
|
break;
|
|
|
|
case BLUETOOTH:
|
|
|
|
setSpeakerphoneOn(false);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
Log.e(Config.LOGTAG, "Invalid audio device selection");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
selectedAudioDevice = device;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Changes default audio device.
|
|
|
|
* TODO(henrika): add usage of this method in the AppRTCMobile client.
|
|
|
|
*/
|
|
|
|
public void setDefaultAudioDevice(AudioDevice defaultDevice) {
|
|
|
|
ThreadUtils.checkIsOnMainThread();
|
|
|
|
switch (defaultDevice) {
|
|
|
|
case SPEAKER_PHONE:
|
|
|
|
defaultAudioDevice = defaultDevice;
|
|
|
|
break;
|
|
|
|
case EARPIECE:
|
|
|
|
if (hasEarpiece()) {
|
|
|
|
defaultAudioDevice = defaultDevice;
|
|
|
|
} else {
|
|
|
|
defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
Log.e(Config.LOGTAG, "Invalid default audio device selection");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
Log.d(Config.LOGTAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")");
|
|
|
|
updateAudioDeviceState();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Changes selection of the currently active audio device.
|
|
|
|
*/
|
|
|
|
public void selectAudioDevice(AudioDevice device) {
|
|
|
|
ThreadUtils.checkIsOnMainThread();
|
|
|
|
if (!audioDevices.contains(device)) {
|
|
|
|
Log.e(Config.LOGTAG, "Can not select " + device + " from available " + audioDevices);
|
|
|
|
}
|
|
|
|
userSelectedAudioDevice = device;
|
|
|
|
updateAudioDeviceState();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns current set of available/selectable audio devices.
|
|
|
|
*/
|
|
|
|
public Set<AudioDevice> getAudioDevices() {
|
|
|
|
ThreadUtils.checkIsOnMainThread();
|
|
|
|
return Collections.unmodifiableSet(new HashSet<>(audioDevices));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the currently selected audio device.
|
|
|
|
*/
|
|
|
|
public AudioDevice getSelectedAudioDevice() {
|
|
|
|
ThreadUtils.checkIsOnMainThread();
|
|
|
|
return selectedAudioDevice;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper method for receiver registration.
|
|
|
|
*/
|
|
|
|
private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
|
|
|
|
apprtcContext.registerReceiver(receiver, filter);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper method for unregistration of an existing receiver.
|
|
|
|
*/
|
|
|
|
private void unregisterReceiver(BroadcastReceiver receiver) {
|
|
|
|
apprtcContext.unregisterReceiver(receiver);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the speaker phone mode.
|
|
|
|
*/
|
|
|
|
private void setSpeakerphoneOn(boolean on) {
|
|
|
|
boolean wasOn = audioManager.isSpeakerphoneOn();
|
|
|
|
if (wasOn == on) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
audioManager.setSpeakerphoneOn(on);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the microphone mute state.
|
|
|
|
*/
|
|
|
|
private void setMicrophoneMute(boolean on) {
|
|
|
|
boolean wasMuted = audioManager.isMicrophoneMute();
|
|
|
|
if (wasMuted == on) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
audioManager.setMicrophoneMute(on);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the current earpiece state.
|
|
|
|
*/
|
|
|
|
private boolean hasEarpiece() {
|
|
|
|
return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether a wired headset is connected or not.
|
|
|
|
* This is not a valid indication that audio playback is actually over
|
|
|
|
* the wired headset as audio routing depends on other conditions. We
|
|
|
|
* only use it as an early indicator (during initialization) of an attached
|
|
|
|
* wired headset.
|
|
|
|
*/
|
|
|
|
@Deprecated
|
|
|
|
private boolean hasWiredHeadset() {
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
|
|
|
return audioManager.isWiredHeadsetOn();
|
|
|
|
} else {
|
|
|
|
final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
|
|
|
|
for (AudioDeviceInfo device : devices) {
|
|
|
|
final int type = device.getType();
|
|
|
|
if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
|
|
|
|
Log.d(Config.LOGTAG, "hasWiredHeadset: found wired headset");
|
|
|
|
return true;
|
|
|
|
} else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) {
|
|
|
|
Log.d(Config.LOGTAG, "hasWiredHeadset: found USB audio device");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates list of possible audio devices and make new device selection.
|
|
|
|
* TODO(henrika): add unit test to verify all state transitions.
|
|
|
|
*/
|
|
|
|
public void updateAudioDeviceState() {
|
|
|
|
ThreadUtils.checkIsOnMainThread();
|
|
|
|
Log.d(Config.LOGTAG, "--- updateAudioDeviceState: "
|
|
|
|
+ "wired headset=" + hasWiredHeadset + ", "
|
|
|
|
+ "BT state=" + bluetoothManager.getState());
|
|
|
|
Log.d(Config.LOGTAG, "Device status: "
|
|
|
|
+ "available=" + audioDevices + ", "
|
|
|
|
+ "selected=" + selectedAudioDevice + ", "
|
|
|
|
+ "user selected=" + userSelectedAudioDevice);
|
|
|
|
// Check if any Bluetooth headset is connected. The internal BT state will
|
|
|
|
// change accordingly.
|
|
|
|
// TODO(henrika): perhaps wrap required state into BT manager.
|
|
|
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
|
|
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE
|
|
|
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_DISCONNECTING) {
|
|
|
|
bluetoothManager.updateDevice();
|
|
|
|
}
|
|
|
|
// Update the set of available audio devices.
|
|
|
|
Set<AudioDevice> newAudioDevices = new HashSet<>();
|
|
|
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|
|
|
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|
|
|
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
|
|
|
|
newAudioDevices.add(AudioDevice.BLUETOOTH);
|
|
|
|
}
|
|
|
|
if (hasWiredHeadset) {
|
|
|
|
// If a wired headset is connected, then it is the only possible option.
|
|
|
|
newAudioDevices.add(AudioDevice.WIRED_HEADSET);
|
|
|
|
} else {
|
|
|
|
// No wired headset, hence the audio-device list can contain speaker
|
|
|
|
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
|
|
|
|
newAudioDevices.add(AudioDevice.SPEAKER_PHONE);
|
|
|
|
if (hasEarpiece()) {
|
|
|
|
newAudioDevices.add(AudioDevice.EARPIECE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Store state which is set to true if the device list has changed.
|
|
|
|
boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices);
|
|
|
|
// Update the existing audio device set.
|
|
|
|
audioDevices = newAudioDevices;
|
|
|
|
// Correct user selected audio devices if needed.
|
|
|
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE
|
|
|
|
&& userSelectedAudioDevice == AudioDevice.BLUETOOTH) {
|
|
|
|
// If BT is not available, it can't be the user selection.
|
|
|
|
userSelectedAudioDevice = AudioDevice.NONE;
|
|
|
|
}
|
|
|
|
if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) {
|
|
|
|
// If user selected speaker phone, but then plugged wired headset then make
|
|
|
|
// wired headset as user selected device.
|
|
|
|
userSelectedAudioDevice = AudioDevice.WIRED_HEADSET;
|
|
|
|
}
|
|
|
|
if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) {
|
|
|
|
// If user selected wired headset, but then unplugged wired headset then make
|
|
|
|
// speaker phone as user selected device.
|
|
|
|
userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE;
|
|
|
|
}
|
|
|
|
// Need to start Bluetooth if it is available and user either selected it explicitly or
|
|
|
|
// user did not select any output device.
|
|
|
|
boolean needBluetoothAudioStart =
|
|
|
|
bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
|
|
|
&& (userSelectedAudioDevice == AudioDevice.NONE
|
|
|
|
|| userSelectedAudioDevice == AudioDevice.BLUETOOTH);
|
|
|
|
// Need to stop Bluetooth audio if user selected different device and
|
|
|
|
// Bluetooth SCO connection is established or in the process.
|
|
|
|
boolean needBluetoothAudioStop =
|
|
|
|
(bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|
|
|
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING)
|
|
|
|
&& (userSelectedAudioDevice != AudioDevice.NONE
|
|
|
|
&& userSelectedAudioDevice != AudioDevice.BLUETOOTH);
|
|
|
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
|
|
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|
|
|
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
|
|
|
|
Log.d(Config.LOGTAG, "Need BT audio: start=" + needBluetoothAudioStart + ", "
|
|
|
|
+ "stop=" + needBluetoothAudioStop + ", "
|
|
|
|
+ "BT state=" + bluetoothManager.getState());
|
|
|
|
}
|
|
|
|
// Start or stop Bluetooth SCO connection given states set earlier.
|
|
|
|
if (needBluetoothAudioStop) {
|
|
|
|
bluetoothManager.stopScoAudio();
|
|
|
|
bluetoothManager.updateDevice();
|
|
|
|
}
|
|
|
|
if (needBluetoothAudioStart && !needBluetoothAudioStop) {
|
|
|
|
// Attempt to start Bluetooth SCO audio (takes a few second to start).
|
|
|
|
if (!bluetoothManager.startScoAudio()) {
|
|
|
|
// Remove BLUETOOTH from list of available devices since SCO failed.
|
|
|
|
audioDevices.remove(AudioDevice.BLUETOOTH);
|
|
|
|
audioDeviceSetUpdated = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Update selected audio device.
|
|
|
|
final AudioDevice newAudioDevice;
|
|
|
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
|
|
|
|
// If a Bluetooth is connected, then it should be used as output audio
|
|
|
|
// device. Note that it is not sufficient that a headset is available;
|
|
|
|
// an active SCO channel must also be up and running.
|
|
|
|
newAudioDevice = AudioDevice.BLUETOOTH;
|
|
|
|
} else if (hasWiredHeadset) {
|
|
|
|
// If a wired headset is connected, but Bluetooth is not, then wired headset is used as
|
|
|
|
// audio device.
|
|
|
|
newAudioDevice = AudioDevice.WIRED_HEADSET;
|
|
|
|
} else {
|
|
|
|
// No wired headset and no Bluetooth, hence the audio-device list can contain speaker
|
|
|
|
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
|
|
|
|
// |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE
|
|
|
|
// depending on the user's selection.
|
|
|
|
newAudioDevice = defaultAudioDevice;
|
|
|
|
}
|
|
|
|
// Switch to new device but only if there has been any changes.
|
|
|
|
if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {
|
|
|
|
// Do the required device switch.
|
|
|
|
setAudioDeviceInternal(newAudioDevice);
|
|
|
|
Log.d(Config.LOGTAG, "New device status: "
|
|
|
|
+ "available=" + audioDevices + ", "
|
|
|
|
+ "selected=" + newAudioDevice);
|
|
|
|
if (audioManagerEvents != null) {
|
|
|
|
// Notify a listening client that audio device has been changed.
|
|
|
|
audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Log.d(Config.LOGTAG, "--- updateAudioDeviceState done");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* AudioDevice is the names of possible audio devices that we currently
|
|
|
|
* support.
|
|
|
|
*/
|
|
|
|
public enum AudioDevice {SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* AudioManager state.
|
|
|
|
*/
|
|
|
|
public enum AudioManagerState {
|
|
|
|
UNINITIALIZED,
|
|
|
|
PREINITIALIZED,
|
|
|
|
RUNNING,
|
|
|
|
}
|
|
|
|
|
|
|
|
public enum SpeakerPhonePreference {
|
|
|
|
AUTO, EARPIECE, SPEAKER
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Selected audio device change event.
|
|
|
|
*/
|
|
|
|
public interface AudioManagerEvents {
|
|
|
|
// Callback fired once audio device is changed or list of available audio devices changed.
|
|
|
|
void onAudioDeviceChanged(
|
|
|
|
AudioDevice selectedAudioDevice, Set<AudioDevice> availableAudioDevices);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Receiver which handles changes in wired headset availability. */
|
|
|
|
private class WiredHeadsetReceiver extends BroadcastReceiver {
|
|
|
|
private static final int STATE_UNPLUGGED = 0;
|
|
|
|
private static final int STATE_PLUGGED = 1;
|
|
|
|
private static final int HAS_NO_MIC = 0;
|
|
|
|
private static final int HAS_MIC = 1;
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onReceive(Context context, Intent intent) {
|
|
|
|
int state = intent.getIntExtra("state", STATE_UNPLUGGED);
|
|
|
|
int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
|
|
|
|
String name = intent.getStringExtra("name");
|
|
|
|
Log.d(Config.LOGTAG, "WiredHeadsetReceiver.onReceive" + AppRTCUtils.getThreadInfo() + ": "
|
|
|
|
+ "a=" + intent.getAction() + ", s="
|
|
|
|
+ (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + ", m="
|
|
|
|
+ (microphone == HAS_MIC ? "mic" : "no mic") + ", n=" + name + ", sb="
|
|
|
|
+ isInitialStickyBroadcast());
|
|
|
|
hasWiredHeadset = (state == STATE_PLUGGED);
|
|
|
|
updateAudioDeviceState();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|