run some AppRTCAudioManager actions on main thread

This commit is contained in:
Daniel Gultsch 2024-01-16 18:50:40 +01:00
parent 6ba9208eea
commit d79fc1bb79
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
7 changed files with 30 additions and 254 deletions

View file

@ -23,6 +23,7 @@ import android.os.Build;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import org.webrtc.ThreadUtils;
@ -44,8 +45,6 @@ public class AppRTCAudioManager {
private final Context apprtcContext;
// Contains speakerphone setting: auto, true or false
@Nullable
private SpeakerPhonePreference speakerPhonePreference;
// Handles all tasks related to Bluetooth headset devices.
private final AppRTCBluetoothManager bluetoothManager;
@Nullable
@ -70,12 +69,7 @@ public class AppRTCAudioManager {
// TODO(henrika): always set to AudioDevice.NONE today. Add support for
// explicit selection based on choice by userSelectedAudioDevice.
private CallIntegration.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<CallIntegration.AudioDevice> audioDevices = new HashSet<>();
@ -86,7 +80,6 @@ public class AppRTCAudioManager {
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
public AppRTCAudioManager(final Context context) {
ThreadUtils.checkIsOnMainThread();
apprtcContext = context;
audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
bluetoothManager = AppRTCBluetoothManager.create(context, this);
@ -98,28 +91,10 @@ public class AppRTCAudioManager {
} else {
defaultAudioDevice = CallIntegration.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);
}
public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) {
this.speakerPhonePreference = speakerPhonePreference;
if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) {
defaultAudioDevice = CallIntegration.AudioDevice.EARPIECE;
} else {
defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
}
updateAudioDeviceState();
}
public static boolean isMicrophoneAvailable() {
microphoneLatch = new CountDownLatch(1);
AudioRecord audioRecord = null;
@ -156,30 +131,6 @@ public class AppRTCAudioManager {
}
}
/**
* 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(CallIntegration.AudioDevice.EARPIECE)
&& audioDevices.contains(CallIntegration.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(CallIntegration.AudioDevice.EARPIECE);
} else {
// Sensor reports that a "handset is removed from a person's ear", or
// "the light sensor is no longer covered".
setAudioDeviceInternal(CallIntegration.AudioDevice.SPEAKER_PHONE);
}
}
}
@SuppressWarnings("deprecation")
public void start(AudioManagerEvents audioManagerEvents) {
Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".start()");
@ -280,6 +231,7 @@ public class AppRTCAudioManager {
@SuppressWarnings("deprecation")
public void stop() {
Log.d(Config.LOGTAG,"appRtpAudioManager.stop()");
Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".stop()");
ThreadUtils.checkIsOnMainThread();
if (amState != AudioManagerState.RUNNING) {
@ -296,12 +248,8 @@ public class AppRTCAudioManager {
// 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,"appRtpAudioManager.stopped()");
}
/**
@ -375,7 +323,6 @@ public class AppRTCAudioManager {
* Returns the currently selected audio device.
*/
public CallIntegration.AudioDevice getSelectedAudioDevice() {
ThreadUtils.checkIsOnMainThread();
return selectedAudioDevice;
}
@ -574,6 +521,10 @@ public class AppRTCAudioManager {
Log.d(Config.LOGTAG, "--- updateAudioDeviceState done");
}
public void executeOnMain(final Runnable runnable) {
ContextCompat.getMainExecutor(apprtcContext).execute(runnable);
}
/**
* AudioManager state.
*/
@ -583,18 +534,6 @@ public class AppRTCAudioManager {
RUNNING,
}
public enum SpeakerPhonePreference {
AUTO, EARPIECE, SPEAKER;
public static SpeakerPhonePreference of(final Set<Media> media) {
if (media.contains(Media.VIDEO)) {
return SPEAKER;
} else {
return EARPIECE;
}
}
}
/**
* Selected audio device change event.
*/

View file

@ -68,8 +68,6 @@ public class AppRTCBluetoothManager {
};
protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) {
Log.d(Config.LOGTAG, "ctor");
ThreadUtils.checkIsOnMainThread();
apprtcContext = context;
apprtcAudioManager = audioManager;
this.audioManager = getAudioManager(context);

View file

@ -1,171 +0,0 @@
/*
* 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.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.Log;
import androidx.annotation.Nullable;
import org.webrtc.ThreadUtils;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.AppRTCUtils;
/**
* AppRTCProximitySensor manages functions related to the proximity sensor in
* the AppRTC demo.
* On most device, the proximity sensor is implemented as a boolean-sensor.
* It returns just two values "NEAR" or "FAR". Thresholding is done on the LUX
* value i.e. the LUX value of the light sensor is compared with a threshold.
* A LUX-value more than the threshold means the proximity sensor returns "FAR".
* Anything less than the threshold value and the sensor returns "NEAR".
*/
public class AppRTCProximitySensor implements SensorEventListener {
// This class should be created, started and stopped on one thread
// (e.g. the main thread). We use |nonThreadSafe| to ensure that this is
// the case. Only active when |DEBUG| is set to true.
private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker();
private final Runnable onSensorStateListener;
private final SensorManager sensorManager;
@Nullable
private Sensor proximitySensor;
private boolean lastStateReportIsNear;
private AppRTCProximitySensor(Context context, Runnable sensorStateListener) {
Log.d(Config.LOGTAG, "AppRTCProximitySensor" + AppRTCUtils.getThreadInfo());
onSensorStateListener = sensorStateListener;
sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE));
}
/**
* Construction
*/
static AppRTCProximitySensor create(Context context, Runnable sensorStateListener) {
return new AppRTCProximitySensor(context, sensorStateListener);
}
/**
* Activate the proximity sensor. Also do initialization if called for the
* first time.
*/
public boolean start() {
threadChecker.checkIsOnValidThread();
Log.d(Config.LOGTAG, "start" + AppRTCUtils.getThreadInfo());
if (!initDefaultSensor()) {
// Proximity sensor is not supported on this device.
return false;
}
sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
return true;
}
/**
* Deactivate the proximity sensor.
*/
public void stop() {
threadChecker.checkIsOnValidThread();
Log.d(Config.LOGTAG, "stop" + AppRTCUtils.getThreadInfo());
if (proximitySensor == null) {
return;
}
sensorManager.unregisterListener(this, proximitySensor);
}
/**
* Getter for last reported state. Set to true if "near" is reported.
*/
public boolean sensorReportsNearState() {
threadChecker.checkIsOnValidThread();
return lastStateReportIsNear;
}
@Override
public final void onAccuracyChanged(Sensor sensor, int accuracy) {
threadChecker.checkIsOnValidThread();
AppRTCUtils.assertIsTrue(sensor.getType() == Sensor.TYPE_PROXIMITY);
if (accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
Log.e(Config.LOGTAG, "The values returned by this sensor cannot be trusted");
}
}
@Override
public final void onSensorChanged(SensorEvent event) {
threadChecker.checkIsOnValidThread();
AppRTCUtils.assertIsTrue(event.sensor.getType() == Sensor.TYPE_PROXIMITY);
// As a best practice; do as little as possible within this method and
// avoid blocking.
float distanceInCentimeters = event.values[0];
if (distanceInCentimeters < proximitySensor.getMaximumRange()) {
Log.d(Config.LOGTAG, "Proximity sensor => NEAR state");
lastStateReportIsNear = true;
} else {
Log.d(Config.LOGTAG, "Proximity sensor => FAR state");
lastStateReportIsNear = false;
}
// Report about new state to listening client. Client can then call
// sensorReportsNearState() to query the current state (NEAR or FAR).
if (onSensorStateListener != null) {
onSensorStateListener.run();
}
Log.d(Config.LOGTAG, "onSensorChanged" + AppRTCUtils.getThreadInfo() + ": "
+ "accuracy=" + event.accuracy + ", timestamp=" + event.timestamp + ", distance="
+ event.values[0]);
}
/**
* Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7)
* does not support this type of sensor and false will be returned in such
* cases.
*/
private boolean initDefaultSensor() {
if (proximitySensor != null) {
return true;
}
proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
if (proximitySensor == null) {
return false;
}
logProximitySensorInfo();
return true;
}
/**
* Helper method for logging information about the proximity sensor.
*/
private void logProximitySensorInfo() {
if (proximitySensor == null) {
return;
}
StringBuilder info = new StringBuilder("Proximity sensor: ");
info.append("name=").append(proximitySensor.getName());
info.append(", vendor: ").append(proximitySensor.getVendor());
info.append(", power: ").append(proximitySensor.getPower());
info.append(", resolution: ").append(proximitySensor.getResolution());
info.append(", max range: ").append(proximitySensor.getMaximumRange());
info.append(", min delay: ").append(proximitySensor.getMinDelay());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
// Added in API level 20.
info.append(", type: ").append(proximitySensor.getStringType());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Added in API level 21.
info.append(", max delay: ").append(proximitySensor.getMaxDelay());
info.append(", reporting mode: ").append(proximitySensor.getReportingMode());
info.append(", isWakeUpSensor: ").append(proximitySensor.isWakeUpSensor());
}
Log.d(Config.LOGTAG, info.toString());
}
}

View file

@ -11,6 +11,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
@ -42,8 +43,8 @@ public class CallIntegration extends Connection {
this.appRTCAudioManager = null;
} else {
this.appRTCAudioManager = new AppRTCAudioManager(context);
this.appRTCAudioManager.start(this::onAudioDeviceChanged);
// TODO WebRTCWrapper would issue one call to eventCallback.onAudioDeviceChanged
ContextCompat.getMainExecutor(context)
.execute(() -> this.appRTCAudioManager.start(this::onAudioDeviceChanged));
}
setRingbackRequested(true);
}
@ -149,6 +150,12 @@ public class CallIntegration extends Connection {
final var available = getAudioDevices();
if (available.contains(audioDevice)) {
this.setAudioDevice(audioDevice);
} else {
Log.d(
Config.LOGTAG,
"application requested to switch to "
+ audioDevice
+ " but device was not available");
}
}
@ -269,7 +276,8 @@ public class CallIntegration extends Connection {
}
private void setAudioDeviceFallback(final AudioDevice audioDevice) {
requireAppRtcAudioManager().setDefaultAudioDevice(audioDevice);
final var audioManager = requireAppRtcAudioManager();
audioManager.executeOnMain(() -> audioManager.setDefaultAudioDevice(audioDevice));
}
@NonNull
@ -287,7 +295,7 @@ public class CallIntegration extends Connection {
if (state == STATE_DISCONNECTED) {
final var audioManager = this.appRTCAudioManager;
if (audioManager != null) {
audioManager.stop();
audioManager.executeOnMain(audioManager::stop);
}
}
}
@ -382,8 +390,11 @@ public class CallIntegration extends Connection {
return;
}
final var audioManager = requireAppRtcAudioManager();
audioManager.executeOnMain(
() ->
this.onAudioDeviceChanged(
audioManager.getSelectedAudioDevice(), audioManager.getAudioDevices());
audioManager.getSelectedAudioDevice(),
audioManager.getAudioDevices()));
}
/** AudioDevice is the names of possible audio devices that we currently support. */

View file

@ -122,6 +122,7 @@ public class CallIntegrationConnectionService extends ConnectionService {
public Connection onCreateIncomingConnection(
final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) {
Log.d(Config.LOGTAG, "onCreateIncomingConnection()");
final var service = ServiceConnectionService.get(this.serviceFuture);
final Bundle extras = request.getExtras();
final Bundle extraExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
@ -182,7 +183,8 @@ public class CallIntegrationConnectionService extends ConnectionService {
}
public static void unregisterPhoneAccount(final Context context, final Account account) {
context.getSystemService(TelecomManager.class).unregisterPhoneAccount(getHandle(context, account));
context.getSystemService(TelecomManager.class)
.unregisterPhoneAccount(getHandle(context, account));
}
public static PhoneAccountHandle getHandle(final Context context, final Account account) {

View file

@ -2300,8 +2300,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final boolean trickle)
throws WebRTCWrapper.InitializationException {
this.jingleConnectionManager.ensureConnectionIsRegistered(this);
this.webRTCWrapper.setup(
this.xmppConnectionService, AppRTCAudioManager.SpeakerPhonePreference.of(media));
this.webRTCWrapper.setup(this.xmppConnectionService);
this.webRTCWrapper.initializePeerConnection(media, iceServers, trickle);
}

View file

@ -222,9 +222,7 @@ public class WebRTCWrapper {
}
}
public void setup(
final XmppConnectionService service,
@Nonnull final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference)
public void setup(final XmppConnectionService service)
throws InitializationException {
try {
PeerConnectionFactory.initialize(