use call integration via MANAGE_OWN_CALLS

better integrate calls into the system via 'Build a calling app'¹

a few hooks like onAnswer/onReject and automatic PhoneAccount creation are still missing

¹: https://developer.android.com/develop/connectivity/telecom/selfManaged
This commit is contained in:
Daniel Gultsch 2024-01-14 10:58:00 +01:00
parent aefcce430d
commit 19c634f3d2
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
13 changed files with 909 additions and 212 deletions

View file

@ -95,7 +95,7 @@ android {
compileSdk 34 compileSdk 34
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 23
targetSdkVersion 34 targetSdkVersion 34
versionCode 42094 versionCode 42094
versionName "2.13.4" versionName "2.13.4"

View file

@ -5,9 +5,6 @@
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.READ_PHONE_STATE"
android:maxSdkVersion="22" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
@ -50,6 +47,8 @@
<!-- this foreground service type permission is exclusively used for import and export backup --> <!-- this foreground service type permission is exclusively used for import and export backup -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
<uses-feature <uses-feature
android:name="android.hardware.camera" android:name="android.hardware.camera"
android:required="false" /> android:required="false" />
@ -133,6 +132,14 @@
</intent-filter> </intent-filter>
</service> </service>
<service android:name=".services.CallIntegrationConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
<receiver <receiver
android:name=".services.EventReceiver" android:name=".services.EventReceiver"
android:exported="false"> android:exported="false">

View file

@ -58,18 +58,18 @@ public class AppRTCAudioManager {
private boolean hasWiredHeadset; private boolean hasWiredHeadset;
// Default audio device; speaker phone for video calls or earpiece for audio // Default audio device; speaker phone for video calls or earpiece for audio
// only calls. // only calls.
private AudioDevice defaultAudioDevice; private CallIntegration.AudioDevice defaultAudioDevice;
// Contains the currently selected audio device. // Contains the currently selected audio device.
// This device is changed automatically using a certain scheme where e.g. // 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 // a wired headset "wins" over speaker phone. It is also possible for a
// user to explicitly select a device (and overrid any predefined scheme). // user to explicitly select a device (and overrid any predefined scheme).
// See |userSelectedAudioDevice| for details. // See |userSelectedAudioDevice| for details.
private AudioDevice selectedAudioDevice; private CallIntegration.AudioDevice selectedAudioDevice;
// Contains the user-selected audio device which overrides the predefined // Contains the user-selected audio device which overrides the predefined
// selection scheme. // selection scheme.
// TODO(henrika): always set to AudioDevice.NONE today. Add support for // TODO(henrika): always set to AudioDevice.NONE today. Add support for
// explicit selection based on choice by userSelectedAudioDevice. // explicit selection based on choice by userSelectedAudioDevice.
private AudioDevice userSelectedAudioDevice; private CallIntegration.AudioDevice userSelectedAudioDevice;
// Proximity sensor object. It measures the proximity of an object in cm // 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 // relative to the view screen of a device and can therefore be used to
// assist device switching (close to ear <=> use headset earpiece if // assist device switching (close to ear <=> use headset earpiece if
@ -78,26 +78,25 @@ public class AppRTCAudioManager {
private AppRTCProximitySensor proximitySensor; private AppRTCProximitySensor proximitySensor;
// Contains a list of available audio devices. A Set collection is used to // Contains a list of available audio devices. A Set collection is used to
// avoid duplicate elements. // avoid duplicate elements.
private Set<AudioDevice> audioDevices = new HashSet<>(); private Set<CallIntegration.AudioDevice> audioDevices = new HashSet<>();
// Broadcast receiver for wired headset intent broadcasts. // Broadcast receiver for wired headset intent broadcasts.
private final BroadcastReceiver wiredHeadsetReceiver; private final BroadcastReceiver wiredHeadsetReceiver;
// Callback method for changes in audio focus. // Callback method for changes in audio focus.
@Nullable @Nullable
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
private AppRTCAudioManager(Context context, final SpeakerPhonePreference speakerPhonePreference) { public AppRTCAudioManager(final Context context) {
Log.d(Config.LOGTAG, "ctor");
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
apprtcContext = context; apprtcContext = context;
audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
bluetoothManager = AppRTCBluetoothManager.create(context, this); bluetoothManager = AppRTCBluetoothManager.create(context, this);
wiredHeadsetReceiver = new WiredHeadsetReceiver(); wiredHeadsetReceiver = new WiredHeadsetReceiver();
amState = AudioManagerState.UNINITIALIZED; amState = AudioManagerState.UNINITIALIZED;
this.speakerPhonePreference = speakerPhonePreference; // CallIntegration / Connection uses Earpiece as default too
if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) { if (hasEarpiece()) {
defaultAudioDevice = AudioDevice.EARPIECE; defaultAudioDevice = CallIntegration.AudioDevice.EARPIECE;
} else { } else {
defaultAudioDevice = AudioDevice.SPEAKER_PHONE; defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
} }
// Create and initialize the proximity sensor. // Create and initialize the proximity sensor.
// Tablet devices (e.g. Nexus 7) does not support proximity sensors. // Tablet devices (e.g. Nexus 7) does not support proximity sensors.
@ -114,20 +113,13 @@ public class AppRTCAudioManager {
public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) { public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) {
this.speakerPhonePreference = speakerPhonePreference; this.speakerPhonePreference = speakerPhonePreference;
if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) { if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) {
defaultAudioDevice = AudioDevice.EARPIECE; defaultAudioDevice = CallIntegration.AudioDevice.EARPIECE;
} else { } else {
defaultAudioDevice = AudioDevice.SPEAKER_PHONE; defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
} }
updateAudioDeviceState(); updateAudioDeviceState();
} }
/**
* Construction.
*/
public static AppRTCAudioManager create(Context context, SpeakerPhonePreference speakerPhonePreference) {
return new AppRTCAudioManager(context, speakerPhonePreference);
}
public static boolean isMicrophoneAvailable() { public static boolean isMicrophoneAvailable() {
microphoneLatch = new CountDownLatch(1); microphoneLatch = new CountDownLatch(1);
AudioRecord audioRecord = null; AudioRecord audioRecord = null;
@ -174,16 +166,16 @@ public class AppRTCAudioManager {
} }
// The proximity sensor should only be activated when there are exactly two // The proximity sensor should only be activated when there are exactly two
// available audio devices. // available audio devices.
if (audioDevices.size() == 2 && audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE) if (audioDevices.size() == 2 && audioDevices.contains(CallIntegration.AudioDevice.EARPIECE)
&& audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) { && audioDevices.contains(CallIntegration.AudioDevice.SPEAKER_PHONE)) {
if (proximitySensor.sensorReportsNearState()) { if (proximitySensor.sensorReportsNearState()) {
// Sensor reports that a "handset is being held up to a person's ear", // Sensor reports that a "handset is being held up to a person's ear",
// or "something is covering the light sensor". // or "something is covering the light sensor".
setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.EARPIECE); setAudioDeviceInternal(CallIntegration.AudioDevice.EARPIECE);
} else { } else {
// Sensor reports that a "handset is removed from a person's ear", or // Sensor reports that a "handset is removed from a person's ear", or
// "the light sensor is no longer covered". // "the light sensor is no longer covered".
setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); setAudioDeviceInternal(CallIntegration.AudioDevice.SPEAKER_PHONE);
} }
} }
} }
@ -258,8 +250,8 @@ public class AppRTCAudioManager {
// Always disable microphone mute during a WebRTC call. // Always disable microphone mute during a WebRTC call.
setMicrophoneMute(false); setMicrophoneMute(false);
// Set initial device states. // Set initial device states.
userSelectedAudioDevice = AudioDevice.NONE; userSelectedAudioDevice = CallIntegration.AudioDevice.NONE;
selectedAudioDevice = AudioDevice.NONE; selectedAudioDevice = CallIntegration.AudioDevice.NONE;
audioDevices.clear(); audioDevices.clear();
// Initialize and start Bluetooth if a BT device is available or initiate // Initialize and start Bluetooth if a BT device is available or initiate
// detection of new (enabled) BT devices. // detection of new (enabled) BT devices.
@ -315,7 +307,7 @@ public class AppRTCAudioManager {
/** /**
* Changes selection of the currently active audio device. * Changes selection of the currently active audio device.
*/ */
private void setAudioDeviceInternal(AudioDevice device) { private void setAudioDeviceInternal(CallIntegration.AudioDevice device) {
Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")"); Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")");
AppRTCUtils.assertIsTrue(audioDevices.contains(device)); AppRTCUtils.assertIsTrue(audioDevices.contains(device));
switch (device) { switch (device) {
@ -338,7 +330,7 @@ public class AppRTCAudioManager {
* Changes default audio device. * Changes default audio device.
* TODO(henrika): add usage of this method in the AppRTCMobile client. * TODO(henrika): add usage of this method in the AppRTCMobile client.
*/ */
public void setDefaultAudioDevice(AudioDevice defaultDevice) { public void setDefaultAudioDevice(CallIntegration.AudioDevice defaultDevice) {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
switch (defaultDevice) { switch (defaultDevice) {
case SPEAKER_PHONE: case SPEAKER_PHONE:
@ -348,7 +340,7 @@ public class AppRTCAudioManager {
if (hasEarpiece()) { if (hasEarpiece()) {
defaultAudioDevice = defaultDevice; defaultAudioDevice = defaultDevice;
} else { } else {
defaultAudioDevice = AudioDevice.SPEAKER_PHONE; defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
} }
break; break;
default: default:
@ -362,7 +354,7 @@ public class AppRTCAudioManager {
/** /**
* Changes selection of the currently active audio device. * Changes selection of the currently active audio device.
*/ */
public void selectAudioDevice(AudioDevice device) { public void selectAudioDevice(CallIntegration.AudioDevice device) {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
if (!audioDevices.contains(device)) { if (!audioDevices.contains(device)) {
Log.e(Config.LOGTAG, "Can not select " + device + " from available " + audioDevices); Log.e(Config.LOGTAG, "Can not select " + device + " from available " + audioDevices);
@ -374,7 +366,7 @@ public class AppRTCAudioManager {
/** /**
* Returns current set of available/selectable audio devices. * Returns current set of available/selectable audio devices.
*/ */
public Set<AudioDevice> getAudioDevices() { public Set<CallIntegration.AudioDevice> getAudioDevices() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
return Collections.unmodifiableSet(new HashSet<>(audioDevices)); return Collections.unmodifiableSet(new HashSet<>(audioDevices));
} }
@ -382,7 +374,7 @@ public class AppRTCAudioManager {
/** /**
* Returns the currently selected audio device. * Returns the currently selected audio device.
*/ */
public AudioDevice getSelectedAudioDevice() { public CallIntegration.AudioDevice getSelectedAudioDevice() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
return selectedAudioDevice; return selectedAudioDevice;
} }
@ -479,21 +471,21 @@ public class AppRTCAudioManager {
bluetoothManager.updateDevice(); bluetoothManager.updateDevice();
} }
// Update the set of available audio devices. // Update the set of available audio devices.
Set<AudioDevice> newAudioDevices = new HashSet<>(); Set<CallIntegration.AudioDevice> newAudioDevices = new HashSet<>();
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) { || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
newAudioDevices.add(AudioDevice.BLUETOOTH); newAudioDevices.add(CallIntegration.AudioDevice.BLUETOOTH);
} }
if (hasWiredHeadset) { if (hasWiredHeadset) {
// If a wired headset is connected, then it is the only possible option. // If a wired headset is connected, then it is the only possible option.
newAudioDevices.add(AudioDevice.WIRED_HEADSET); newAudioDevices.add(CallIntegration.AudioDevice.WIRED_HEADSET);
} else { } else {
// No wired headset, hence the audio-device list can contain speaker // No wired headset, hence the audio-device list can contain speaker
// phone (on a tablet), or speaker phone and earpiece (on mobile phone). // phone (on a tablet), or speaker phone and earpiece (on mobile phone).
newAudioDevices.add(AudioDevice.SPEAKER_PHONE); newAudioDevices.add(CallIntegration.AudioDevice.SPEAKER_PHONE);
if (hasEarpiece()) { if (hasEarpiece()) {
newAudioDevices.add(AudioDevice.EARPIECE); newAudioDevices.add(CallIntegration.AudioDevice.EARPIECE);
} }
} }
// Store state which is set to true if the device list has changed. // Store state which is set to true if the device list has changed.
@ -502,33 +494,33 @@ public class AppRTCAudioManager {
audioDevices = newAudioDevices; audioDevices = newAudioDevices;
// Correct user selected audio devices if needed. // Correct user selected audio devices if needed.
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE
&& userSelectedAudioDevice == AudioDevice.BLUETOOTH) { && userSelectedAudioDevice == CallIntegration.AudioDevice.BLUETOOTH) {
// If BT is not available, it can't be the user selection. // If BT is not available, it can't be the user selection.
userSelectedAudioDevice = AudioDevice.NONE; userSelectedAudioDevice = CallIntegration.AudioDevice.NONE;
} }
if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) { if (hasWiredHeadset && userSelectedAudioDevice == CallIntegration.AudioDevice.SPEAKER_PHONE) {
// If user selected speaker phone, but then plugged wired headset then make // If user selected speaker phone, but then plugged wired headset then make
// wired headset as user selected device. // wired headset as user selected device.
userSelectedAudioDevice = AudioDevice.WIRED_HEADSET; userSelectedAudioDevice = CallIntegration.AudioDevice.WIRED_HEADSET;
} }
if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) { if (!hasWiredHeadset && userSelectedAudioDevice == CallIntegration.AudioDevice.WIRED_HEADSET) {
// If user selected wired headset, but then unplugged wired headset then make // If user selected wired headset, but then unplugged wired headset then make
// speaker phone as user selected device. // speaker phone as user selected device.
userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE; userSelectedAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
} }
// Need to start Bluetooth if it is available and user either selected it explicitly or // Need to start Bluetooth if it is available and user either selected it explicitly or
// user did not select any output device. // user did not select any output device.
boolean needBluetoothAudioStart = boolean needBluetoothAudioStart =
bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
&& (userSelectedAudioDevice == AudioDevice.NONE && (userSelectedAudioDevice == CallIntegration.AudioDevice.NONE
|| userSelectedAudioDevice == AudioDevice.BLUETOOTH); || userSelectedAudioDevice == CallIntegration.AudioDevice.BLUETOOTH);
// Need to stop Bluetooth audio if user selected different device and // Need to stop Bluetooth audio if user selected different device and
// Bluetooth SCO connection is established or in the process. // Bluetooth SCO connection is established or in the process.
boolean needBluetoothAudioStop = boolean needBluetoothAudioStop =
(bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING) || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING)
&& (userSelectedAudioDevice != AudioDevice.NONE && (userSelectedAudioDevice != CallIntegration.AudioDevice.NONE
&& userSelectedAudioDevice != AudioDevice.BLUETOOTH); && userSelectedAudioDevice != CallIntegration.AudioDevice.BLUETOOTH);
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) { || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
@ -545,21 +537,21 @@ public class AppRTCAudioManager {
// Attempt to start Bluetooth SCO audio (takes a few second to start). // Attempt to start Bluetooth SCO audio (takes a few second to start).
if (!bluetoothManager.startScoAudio()) { if (!bluetoothManager.startScoAudio()) {
// Remove BLUETOOTH from list of available devices since SCO failed. // Remove BLUETOOTH from list of available devices since SCO failed.
audioDevices.remove(AudioDevice.BLUETOOTH); audioDevices.remove(CallIntegration.AudioDevice.BLUETOOTH);
audioDeviceSetUpdated = true; audioDeviceSetUpdated = true;
} }
} }
// Update selected audio device. // Update selected audio device.
final AudioDevice newAudioDevice; final CallIntegration.AudioDevice newAudioDevice;
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) { if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
// If a Bluetooth is connected, then it should be used as output audio // 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; // device. Note that it is not sufficient that a headset is available;
// an active SCO channel must also be up and running. // an active SCO channel must also be up and running.
newAudioDevice = AudioDevice.BLUETOOTH; newAudioDevice = CallIntegration.AudioDevice.BLUETOOTH;
} else if (hasWiredHeadset) { } else if (hasWiredHeadset) {
// If a wired headset is connected, but Bluetooth is not, then wired headset is used as // If a wired headset is connected, but Bluetooth is not, then wired headset is used as
// audio device. // audio device.
newAudioDevice = AudioDevice.WIRED_HEADSET; newAudioDevice = CallIntegration.AudioDevice.WIRED_HEADSET;
} else { } else {
// No wired headset and no Bluetooth, hence the audio-device list can contain speaker // 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). // phone (on a tablet), or speaker phone and earpiece (on mobile phone).
@ -582,12 +574,6 @@ public class AppRTCAudioManager {
Log.d(Config.LOGTAG, "--- updateAudioDeviceState done"); 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. * AudioManager state.
*/ */
@ -615,7 +601,7 @@ public class AppRTCAudioManager {
public interface AudioManagerEvents { public interface AudioManagerEvents {
// Callback fired once audio device is changed or list of available audio devices changed. // Callback fired once audio device is changed or list of available audio devices changed.
void onAudioDeviceChanged( void onAudioDeviceChanged(
AudioDevice selectedAudioDevice, Set<AudioDevice> availableAudioDevices); CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices);
} }
/* Receiver which handles changes in wired headset availability. */ /* Receiver which handles changes in wired headset availability. */

View file

@ -0,0 +1,408 @@
package eu.siacs.conversations.services;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.telecom.CallAudioState;
import android.telecom.CallEndpoint;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.ui.util.MainThreadExecutor;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.Media;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
public class CallIntegration extends Connection {
private final AppRTCAudioManager appRTCAudioManager;
private AudioDevice initialAudioDevice = null;
private final AtomicBoolean initialAudioDeviceConfigured = new AtomicBoolean(false);
private List<CallEndpoint> availableEndpoints = Collections.emptyList();
private Callback callback = null;
public CallIntegration(final Context context) {
if (selfManaged()) {
setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
this.appRTCAudioManager = null;
} else {
this.appRTCAudioManager = new AppRTCAudioManager(context);
this.appRTCAudioManager.start(this::onAudioDeviceChanged);
// TODO WebRTCWrapper would issue one call to eventCallback.onAudioDeviceChanged
}
setRingbackRequested(true);
}
public void setCallback(final Callback callback) {
this.callback = callback;
}
@Override
public void onShowIncomingCallUi() {
Log.d(Config.LOGTAG, "onShowIncomingCallUi");
this.callback.onCallIntegrationShowIncomingCallUi();
}
@Override
public void onAnswer() {
Log.d(Config.LOGTAG, "onAnswer()");
}
@Override
public void onDisconnect() {
Log.d(Config.LOGTAG, "onDisconnect()");
this.callback.onCallIntegrationDisconnect();
}
@Override
public void onReject() {
Log.d(Config.LOGTAG, "onReject()");
}
@Override
public void onReject(final String replyMessage) {
Log.d(Config.LOGTAG, "onReject(" + replyMessage + ")");
}
@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Override
public void onAvailableCallEndpointsChanged(@NonNull List<CallEndpoint> availableEndpoints) {
Log.d(Config.LOGTAG, "onAvailableCallEndpointsChanged(" + availableEndpoints + ")");
this.availableEndpoints = availableEndpoints;
this.onAudioDeviceChanged(
getAudioDeviceUpsideDownCake(getCurrentCallEndpoint()),
ImmutableSet.copyOf(
Lists.transform(
availableEndpoints,
CallIntegration::getAudioDeviceUpsideDownCake)));
}
@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Override
public void onCallEndpointChanged(@NonNull final CallEndpoint callEndpoint) {
Log.d(Config.LOGTAG, "onCallEndpointChanged()");
this.onAudioDeviceChanged(
getAudioDeviceUpsideDownCake(callEndpoint),
ImmutableSet.copyOf(
Lists.transform(
this.availableEndpoints,
CallIntegration::getAudioDeviceUpsideDownCake)));
}
@Override
public void onCallAudioStateChanged(final CallAudioState state) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
Log.d(Config.LOGTAG, "ignoring onCallAudioStateChange() on Upside Down Cake");
return;
}
Log.d(Config.LOGTAG, "onCallAudioStateChange(" + state + ")");
this.onAudioDeviceChanged(getAudioDeviceOreo(state), getAudioDevicesOreo(state));
}
public Set<AudioDevice> getAudioDevices() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
return getAudioDevicesUpsideDownCake();
} else if (selfManaged()) {
return getAudioDevicesOreo();
} else {
return getAudioDevicesFallback();
}
}
public AudioDevice getSelectedAudioDevice() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
return getAudioDeviceUpsideDownCake();
} else if (selfManaged()) {
return getAudioDeviceOreo();
} else {
return getAudioDeviceFallback();
}
}
public void setAudioDevice(final AudioDevice audioDevice) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
setAudioDeviceUpsideDownCake(audioDevice);
} else if (selfManaged()) {
setAudioDeviceOreo(audioDevice);
} else {
setAudioDeviceFallback(audioDevice);
}
}
@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private Set<AudioDevice> getAudioDevicesUpsideDownCake() {
return ImmutableSet.copyOf(
Lists.transform(
this.availableEndpoints, CallIntegration::getAudioDeviceUpsideDownCake));
}
@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private AudioDevice getAudioDeviceUpsideDownCake() {
return getAudioDeviceUpsideDownCake(getCurrentCallEndpoint());
}
@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private static AudioDevice getAudioDeviceUpsideDownCake(final CallEndpoint callEndpoint) {
if (callEndpoint == null) {
return AudioDevice.NONE;
}
final var endpointType = callEndpoint.getEndpointType();
return switch (endpointType) {
case CallEndpoint.TYPE_BLUETOOTH -> AudioDevice.BLUETOOTH;
case CallEndpoint.TYPE_EARPIECE -> AudioDevice.EARPIECE;
case CallEndpoint.TYPE_SPEAKER -> AudioDevice.SPEAKER_PHONE;
case CallEndpoint.TYPE_WIRED_HEADSET -> AudioDevice.WIRED_HEADSET;
case CallEndpoint.TYPE_STREAMING -> AudioDevice.STREAMING;
case CallEndpoint.TYPE_UNKNOWN -> AudioDevice.NONE;
default -> throw new IllegalStateException("Unknown endpoint type " + endpointType);
};
}
@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private void setAudioDeviceUpsideDownCake(final AudioDevice audioDevice) {
final var callEndpointOptional =
Iterables.tryFind(
this.availableEndpoints,
e -> getAudioDeviceUpsideDownCake(e) == audioDevice);
if (callEndpointOptional.isPresent()) {
final var endpoint = callEndpointOptional.get();
requestCallEndpointChange(
endpoint,
MainThreadExecutor.getInstance(),
result -> Log.d(Config.LOGTAG, "switched to endpoint " + endpoint));
} else {
Log.w(Config.LOGTAG, "no endpoint found matching " + audioDevice);
}
}
private Set<AudioDevice> getAudioDevicesOreo() {
final var audioState = getCallAudioState();
if (audioState == null) {
Log.d(
Config.LOGTAG,
"no CallAudioState available. returning empty set for audio devices");
return Collections.emptySet();
}
return getAudioDevicesOreo(audioState);
}
private static Set<AudioDevice> getAudioDevicesOreo(final CallAudioState callAudioState) {
final ImmutableSet.Builder<AudioDevice> supportedAudioDevicesBuilder =
new ImmutableSet.Builder<>();
final var supportedRouteMask = callAudioState.getSupportedRouteMask();
if ((supportedRouteMask & CallAudioState.ROUTE_BLUETOOTH)
== CallAudioState.ROUTE_BLUETOOTH) {
supportedAudioDevicesBuilder.add(AudioDevice.BLUETOOTH);
}
if ((supportedRouteMask & CallAudioState.ROUTE_EARPIECE) == CallAudioState.ROUTE_EARPIECE) {
supportedAudioDevicesBuilder.add(AudioDevice.EARPIECE);
}
if ((supportedRouteMask & CallAudioState.ROUTE_SPEAKER) == CallAudioState.ROUTE_SPEAKER) {
supportedAudioDevicesBuilder.add(AudioDevice.SPEAKER_PHONE);
}
if ((supportedRouteMask & CallAudioState.ROUTE_WIRED_HEADSET)
== CallAudioState.ROUTE_WIRED_HEADSET) {
supportedAudioDevicesBuilder.add(AudioDevice.WIRED_HEADSET);
}
return supportedAudioDevicesBuilder.build();
}
private AudioDevice getAudioDeviceOreo() {
final var audioState = getCallAudioState();
if (audioState == null) {
Log.d(Config.LOGTAG, "no CallAudioState available. returning NONE as audio device");
return AudioDevice.NONE;
}
return getAudioDeviceOreo(audioState);
}
private static AudioDevice getAudioDeviceOreo(final CallAudioState audioState) {
// technically we get a mask here; maybe we should query the mask instead
return switch (audioState.getRoute()) {
case CallAudioState.ROUTE_BLUETOOTH -> AudioDevice.BLUETOOTH;
case CallAudioState.ROUTE_EARPIECE -> AudioDevice.EARPIECE;
case CallAudioState.ROUTE_SPEAKER -> AudioDevice.SPEAKER_PHONE;
case CallAudioState.ROUTE_WIRED_HEADSET -> AudioDevice.WIRED_HEADSET;
default -> AudioDevice.NONE;
};
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void setAudioDeviceOreo(final AudioDevice audioDevice) {
switch (audioDevice) {
case EARPIECE -> setAudioRoute(CallAudioState.ROUTE_EARPIECE);
case BLUETOOTH -> setAudioRoute(CallAudioState.ROUTE_BLUETOOTH);
case WIRED_HEADSET -> setAudioRoute(CallAudioState.ROUTE_WIRED_HEADSET);
case SPEAKER_PHONE -> setAudioRoute(CallAudioState.ROUTE_SPEAKER);
}
}
private Set<AudioDevice> getAudioDevicesFallback() {
return requireAppRtcAudioManager().getAudioDevices();
}
private AudioDevice getAudioDeviceFallback() {
return requireAppRtcAudioManager().getSelectedAudioDevice();
}
private void setAudioDeviceFallback(final AudioDevice audioDevice) {
requireAppRtcAudioManager().setDefaultAudioDevice(audioDevice);
}
@NonNull
private AppRTCAudioManager requireAppRtcAudioManager() {
if (this.appRTCAudioManager == null) {
throw new IllegalStateException(
"You are trying to access the fallback audio manager on a modern device");
}
return this.appRTCAudioManager;
}
@Override
public void onStateChanged(final int state) {
Log.d(Config.LOGTAG, "onStateChanged(" + state + ")");
if (state == STATE_DISCONNECTED) {
final var audioManager = this.appRTCAudioManager;
if (audioManager != null) {
audioManager.stop();
}
}
}
public void success() {
Log.d(Config.LOGTAG, "CallIntegration.success()");
this.destroyWith(new DisconnectCause(DisconnectCause.LOCAL, null));
}
public void accepted() {
Log.d(Config.LOGTAG, "CallIntegration.accepted()");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
this.destroyWith(new DisconnectCause(DisconnectCause.ANSWERED_ELSEWHERE, null));
} else {
this.destroyWith(new DisconnectCause(DisconnectCause.CANCELED, null));
}
}
public void error() {
Log.d(Config.LOGTAG, "CallIntegration.error()");
this.destroyWith(new DisconnectCause(DisconnectCause.ERROR, null));
}
public void retracted() {
Log.d(Config.LOGTAG, "CallIntegration.retracted()");
// an alternative cause would be LOCAL
this.destroyWith(new DisconnectCause(DisconnectCause.CANCELED, null));
}
public void rejected() {
Log.d(Config.LOGTAG, "CallIntegration.rejected()");
this.destroyWith(new DisconnectCause(DisconnectCause.REJECTED, null));
}
public void busy() {
Log.d(Config.LOGTAG, "CallIntegration.busy()");
this.destroyWith(new DisconnectCause(DisconnectCause.BUSY, null));
}
private void destroyWith(final DisconnectCause disconnectCause) {
if (this.getState() == STATE_DISCONNECTED) {
Log.d(Config.LOGTAG, "CallIntegration has already been destroyed");
return;
}
this.setDisconnected(disconnectCause);
this.destroy();
}
public static Uri address(final Jid contact) {
return Uri.parse(String.format("xmpp:%s", contact.toEscapedString()));
}
public void verifyDisconnected() {
if (this.getState() == STATE_DISCONNECTED) {
return;
}
throw new AssertionError("CallIntegration has not been disconnected");
}
private void onAudioDeviceChanged(
final CallIntegration.AudioDevice selectedAudioDevice,
final Set<CallIntegration.AudioDevice> availableAudioDevices) {
if (this.initialAudioDevice != null
&& this.initialAudioDeviceConfigured.compareAndSet(false, true)) {
if (availableAudioDevices.contains(this.initialAudioDevice)) {
setAudioDevice(this.initialAudioDevice);
Log.d(Config.LOGTAG, "configured initial audio device");
} else {
Log.d(
Config.LOGTAG,
"initial audio device not available. available devices: "
+ availableAudioDevices);
}
}
final var callback = this.callback;
if (callback == null) {
return;
}
callback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
}
public static boolean selfManaged() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
public void setInitialAudioDevice(final AudioDevice audioDevice) {
Log.d(Config.LOGTAG, "setInitialAudioDevice(" + audioDevice + ")");
this.initialAudioDevice = audioDevice;
if (CallIntegration.selfManaged()) {
// once the 'CallIntegration' gets added to the system we receive calls to update audio
// state
return;
}
final var audioManager = requireAppRtcAudioManager();
this.onAudioDeviceChanged(
audioManager.getSelectedAudioDevice(), audioManager.getAudioDevices());
}
/** AudioDevice is the names of possible audio devices that we currently support. */
public enum AudioDevice {
NONE,
SPEAKER_PHONE,
WIRED_HEADSET,
EARPIECE,
BLUETOOTH,
STREAMING
}
public static AudioDevice initialAudioDevice(final Set<Media> media) {
if (Media.audioOnly(media)) {
return AudioDevice.EARPIECE;
} else {
return AudioDevice.SPEAKER_PHONE;
}
}
public interface Callback {
void onCallIntegrationShowIncomingCallUi();
void onCallIntegrationDisconnect();
void onAudioDeviceChanged(
CallIntegration.AudioDevice selectedAudioDevice,
Set<CallIntegration.AudioDevice> availableAudioDevices);
}
}

View file

@ -0,0 +1,255 @@
package eu.siacs.conversations.services;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.telecom.Connection;
import android.telecom.ConnectionRequest;
import android.telecom.ConnectionService;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.util.Log;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.ui.RtpSessionActivity;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class CallIntegrationConnectionService extends ConnectionService {
private ListenableFuture<ServiceConnectionService> serviceFuture;
@Override
public void onCreate() {
super.onCreate();
this.serviceFuture = ServiceConnectionService.bindService(this);
}
@Override
public void onDestroy() {
Log.d(Config.LOGTAG, "destroying CallIntegrationConnectionService");
super.onDestroy();
final ServiceConnection serviceConnection;
try {
serviceConnection = serviceFuture.get().serviceConnection;
} catch (final Exception e) {
Log.d(Config.LOGTAG, "could not fetch service connection", e);
return;
}
this.unbindService(serviceConnection);
}
@Override
public Connection onCreateOutgoingConnection(
final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) {
Log.d(Config.LOGTAG, "onCreateOutgoingConnection(" + request.getAddress() + ")");
final var uri = request.getAddress();
final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart());
final var extras = request.getExtras();
final int videoState = extras.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE);
final Set<Media> media =
videoState == VideoProfile.STATE_AUDIO_ONLY
? ImmutableSet.of(Media.AUDIO)
: ImmutableSet.of(Media.AUDIO, Media.VIDEO);
Log.d(Config.LOGTAG, "jid=" + jid);
Log.d(Config.LOGTAG, "phoneAccountHandle:" + phoneAccountHandle.getId());
Log.d(Config.LOGTAG, "media " + media);
final var service = ServiceConnectionService.get(this.serviceFuture);
if (service == null) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR, "service connection not found"));
}
final Account account = service.findAccountByUuid(phoneAccountHandle.getId());
final Intent intent = new Intent(this, RtpSessionActivity.class);
intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString());
intent.putExtra(RtpSessionActivity.EXTRA_WITH, jid.toEscapedString());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
final CallIntegration callIntegration;
if (jid.isBareJid()) {
final var proposal =
service.getJingleConnectionManager()
.proposeJingleRtpSession(account, jid, media);
if (Media.audioOnly(media)) {
intent.setAction(RtpSessionActivity.ACTION_MAKE_VOICE_CALL);
} else {
intent.setAction(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL);
}
callIntegration = proposal.getCallIntegration();
} else {
final JingleRtpConnection jingleRtpConnection =
service.getJingleConnectionManager().initializeRtpSession(account, jid, media);
final String sessionId = jingleRtpConnection.getId().sessionId;
intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
callIntegration = jingleRtpConnection.getCallIntegration();
}
Log.d(Config.LOGTAG, "start activity!");
startActivity(intent);
return callIntegration;
}
public Connection onCreateIncomingConnection(
final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) {
final var service = ServiceConnectionService.get(this.serviceFuture);
final Bundle extras = request.getExtras();
final Bundle extraExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
final String incomingCallAddress =
extras.getString(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS);
final String sid = extraExtras == null ? null : extraExtras.getString("sid");
Log.d(Config.LOGTAG, "sid " + sid);
final Uri uri = incomingCallAddress == null ? null : Uri.parse(incomingCallAddress);
Log.d(Config.LOGTAG, "uri=" + uri);
if (uri == null || sid == null) {
return Connection.createFailedConnection(
new DisconnectCause(
DisconnectCause.ERROR,
"connection request is missing required information"));
}
if (service == null) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR, "service connection not found"));
}
final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart());
final Account account = service.findAccountByUuid(phoneAccountHandle.getId());
final var weakReference =
service.getJingleConnectionManager().findJingleRtpConnection(account, jid, sid);
if (weakReference == null) {
Log.d(Config.LOGTAG, "no connection found for " + jid + " and sid=" + sid);
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR, "no incoming connection found"));
}
final var jingleRtpConnection = weakReference.get();
if (jingleRtpConnection == null) {
Log.d(Config.LOGTAG, "connection has been terminated");
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR, "connection has been terminated"));
}
Log.d(Config.LOGTAG, "registering call integration for incoming call");
return jingleRtpConnection.getCallIntegration();
}
public static void registerPhoneAccount(final Context context, final Account account) {
final var builder =
PhoneAccount.builder(getHandle(context, account), account.getJid().asBareJid());
builder.setSupportedUriSchemes(Collections.singletonList("xmpp"));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setCapabilities(
PhoneAccount.CAPABILITY_SELF_MANAGED
| PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING);
}
final var phoneAccount = builder.build();
context.getSystemService(TelecomManager.class).registerPhoneAccount(phoneAccount);
}
public static void registerPhoneAccounts(
final Context context, final Collection<Account> accounts) {
for (final Account account : accounts) {
registerPhoneAccount(context, account);
}
}
public static PhoneAccountHandle getHandle(final Context context, final Account account) {
final var competentName =
new ComponentName(context, CallIntegrationConnectionService.class);
return new PhoneAccountHandle(competentName, account.getUuid());
}
public static void placeCall(
final Context context, final Account account, final Jid with, final Set<Media> media) {
Log.d(Config.LOGTAG, "place call media=" + media);
final var extras = new Bundle();
extras.putParcelable(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, getHandle(context, account));
extras.putInt(
TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
Media.audioOnly(media)
? VideoProfile.STATE_AUDIO_ONLY
: VideoProfile.STATE_BIDIRECTIONAL);
context.getSystemService(TelecomManager.class)
.placeCall(CallIntegration.address(with), extras);
}
public static void addNewIncomingCall(
final Context context, final AbstractJingleConnection.Id id) {
final var phoneAccountHandle =
CallIntegrationConnectionService.getHandle(context, id.account);
final var bundle = new Bundle();
bundle.putString(
TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
CallIntegration.address(id.with).toString());
final var extras = new Bundle();
extras.putString("sid", id.sessionId);
bundle.putBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras);
context.getSystemService(TelecomManager.class)
.addNewIncomingCall(phoneAccountHandle, bundle);
}
public static class ServiceConnectionService {
private final ServiceConnection serviceConnection;
private final XmppConnectionService service;
public ServiceConnectionService(
final ServiceConnection serviceConnection, final XmppConnectionService service) {
this.serviceConnection = serviceConnection;
this.service = service;
}
public static XmppConnectionService get(
final ListenableFuture<ServiceConnectionService> future) {
try {
return future.get(2, TimeUnit.SECONDS).service;
} catch (final ExecutionException | InterruptedException | TimeoutException e) {
return null;
}
}
public static ListenableFuture<ServiceConnectionService> bindService(
final Context context) {
final SettableFuture<ServiceConnectionService> serviceConnectionFuture =
SettableFuture.create();
final var intent = new Intent(context, XmppConnectionService.class);
intent.setAction(XmppConnectionService.ACTION_CALL_INTEGRATION_SERVICE_STARTED);
final var serviceConnection =
new ServiceConnection() {
@Override
public void onServiceConnected(
final ComponentName name, final IBinder iBinder) {
final XmppConnectionService.XmppConnectionBinder binder =
(XmppConnectionService.XmppConnectionBinder) iBinder;
serviceConnectionFuture.set(
new ServiceConnectionService(this, binder.getService()));
}
@Override
public void onServiceDisconnected(final ComponentName name) {}
};
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
return serviceConnectionFuture;
}
}
}

View file

@ -198,6 +198,7 @@ public class XmppConnectionService extends Service {
public static final String ACTION_DISMISS_CALL = "dismiss_call"; public static final String ACTION_DISMISS_CALL = "dismiss_call";
public static final String ACTION_END_CALL = "end_call"; public static final String ACTION_END_CALL = "end_call";
public static final String ACTION_PROVISION_ACCOUNT = "provision_account"; public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
public static final String ACTION_CALL_INTEGRATION_SERVICE_STARTED = "call_integration_service_started";
private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE"; private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = "eu.siacs.conversations.UNIFIED_PUSH_RENEW"; public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = "eu.siacs.conversations.UNIFIED_PUSH_RENEW";
public static final String ACTION_QUICK_LOG = "eu.siacs.conversations.QUICK_LOG"; public static final String ACTION_QUICK_LOG = "eu.siacs.conversations.QUICK_LOG";
@ -303,16 +304,6 @@ public class XmppConnectionService extends Service {
return false; return false;
} }
}; };
private final AtomicBoolean isPhoneInCall = new AtomicBoolean(false);
private final PhoneStateListener phoneStateListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(final int state, final String phoneNumber) {
isPhoneInCall.set(state != TelephonyManager.CALL_STATE_IDLE);
if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
mJingleConnectionManager.notifyPhoneCallStarted();
}
}
};
private boolean destroyed = false; private boolean destroyed = false;
@ -1288,6 +1279,8 @@ public class XmppConnectionService extends Service {
toggleSetProfilePictureActivity(hasEnabledAccounts); toggleSetProfilePictureActivity(hasEnabledAccounts);
reconfigurePushDistributor(); reconfigurePushDistributor();
CallIntegrationConnectionService.registerPhoneAccounts(this, this.accounts);
restoreFromDatabase(); restoreFromDatabase();
if (QuickConversationsService.isContactListIntegration(this) if (QuickConversationsService.isContactListIntegration(this)
@ -1351,23 +1344,10 @@ public class XmppConnectionService extends Service {
ContextCompat.RECEIVER_EXPORTED); ContextCompat.RECEIVER_EXPORTED);
mForceDuringOnCreate.set(false); mForceDuringOnCreate.set(false);
toggleForegroundService(); toggleForegroundService();
setupPhoneStateListener();
internalPingExecutor.scheduleAtFixedRate(this::manageAccountConnectionStatesInternal,10,10,TimeUnit.SECONDS); internalPingExecutor.scheduleAtFixedRate(this::manageAccountConnectionStatesInternal,10,10,TimeUnit.SECONDS);
} }
private void setupPhoneStateListener() {
final TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
if (telephonyManager == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return;
}
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
public boolean isPhoneInCall() {
return isPhoneInCall.get();
}
private void checkForDeletedFiles() { private void checkForDeletedFiles() {
if (destroyed) { if (destroyed) {
Log.d(Config.LOGTAG, "Do not check for deleted files because service has been destroyed"); Log.d(Config.LOGTAG, "Do not check for deleted files because service has been destroyed");
@ -4413,7 +4393,7 @@ public class XmppConnectionService extends Service {
} }
} }
public void notifyJingleRtpConnectionUpdate(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) { public void notifyJingleRtpConnectionUpdate(CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices) {
for (OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) { for (OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) {
listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
} }
@ -5110,7 +5090,7 @@ public class XmppConnectionService extends Service {
public interface OnJingleRtpConnectionUpdate { public interface OnJingleRtpConnectionUpdate {
void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state); void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state);
void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices); void onAudioDeviceChanged(CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices);
} }
public interface OnAccountUpdate { public interface OnAccountUpdate {

View file

@ -86,6 +86,7 @@ import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.http.HttpDownloadConnection; import eu.siacs.conversations.http.HttpDownloadConnection;
import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.CallIntegrationConnectionService;
import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
@ -1652,13 +1653,14 @@ public class ConversationFragment extends XmppFragment
} }
private void triggerRtpSession(final Account account, final Jid with, final String action) { private void triggerRtpSession(final Account account, final Jid with, final String action) {
final Intent intent = new Intent(activity, RtpSessionActivity.class); CallIntegrationConnectionService.placeCall(requireActivity(),account,with,RtpSessionActivity.actionToMedia(action));
/*final Intent intent = new Intent(activity, RtpSessionActivity.class);
intent.setAction(action); intent.setAction(action);
intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString()); intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString());
intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString()); intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent); startActivity(intent);*/
} }
private void handleAttachmentSelection(MenuItem item) { private void handleAttachmentSelection(MenuItem item) {

View file

@ -49,6 +49,8 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.services.CallIntegration;
import eu.siacs.conversations.services.CallIntegrationConnectionService;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MainThreadExecutor; import eu.siacs.conversations.ui.util.MainThreadExecutor;
@ -133,7 +135,7 @@ public class RtpSessionActivity extends XmppActivity
} }
}; };
private static Set<Media> actionToMedia(final String action) { public static Set<Media> actionToMedia(final String action) {
if (ACTION_MAKE_VIDEO_CALL.equals(action)) { if (ACTION_MAKE_VIDEO_CALL.equals(action)) {
return ImmutableSet.of(Media.AUDIO, Media.VIDEO); return ImmutableSet.of(Media.AUDIO, Media.VIDEO);
} else { } else {
@ -416,11 +418,11 @@ public class RtpSessionActivity extends XmppActivity
if (Media.audioOnly(media)) { if (Media.audioOnly(media)) {
final JingleRtpConnection rtpConnection = final JingleRtpConnection rtpConnection =
rtpConnectionReference != null ? rtpConnectionReference.get() : null; rtpConnectionReference != null ? rtpConnectionReference.get() : null;
final AppRTCAudioManager audioManager = final CallIntegration callIntegration =
rtpConnection == null ? null : rtpConnection.getAudioManager(); rtpConnection == null ? null : rtpConnection.getCallIntegration();
if (audioManager == null if (callIntegration == null
|| audioManager.getSelectedAudioDevice() || callIntegration.getSelectedAudioDevice()
== AppRTCAudioManager.AudioDevice.EARPIECE) { == CallIntegration.AudioDevice.EARPIECE) {
acquireProximityWakeLock(); acquireProximityWakeLock();
} }
} }
@ -466,8 +468,8 @@ public class RtpSessionActivity extends XmppActivity
} }
private void putProximityWakeLockInProperState( private void putProximityWakeLockInProperState(
final AppRTCAudioManager.AudioDevice audioDevice) { final CallIntegration.AudioDevice audioDevice) {
if (audioDevice == AppRTCAudioManager.AudioDevice.EARPIECE) { if (audioDevice == CallIntegration.AudioDevice.EARPIECE) {
acquireProximityWakeLock(); acquireProximityWakeLock();
} else { } else {
releaseProximityWakeLock(); releaseProximityWakeLock();
@ -581,12 +583,7 @@ public class RtpSessionActivity extends XmppActivity
.getJingleConnectionManager() .getJingleConnectionManager()
.proposeJingleRtpSession(account, with, media); .proposeJingleRtpSession(account, with, media);
} else { } else {
final String sessionId = throw new IllegalStateException("We should not be initializing direct calls from the RtpSessionActivity. Go through CallIntegrationConnectionService.placeCall instead!");
xmppConnectionService
.getJingleConnectionManager()
.initializeRtpSession(account, with, media);
initializeActivityWithRunningRtpSession(account, with, sessionId);
resetIntent(account, with, sessionId);
} }
putScreenInCallMode(media); putScreenInCallMode(media);
} }
@ -1032,10 +1029,10 @@ public class RtpSessionActivity extends XmppActivity
updateInCallButtonConfigurationVideo( updateInCallButtonConfigurationVideo(
rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable()); rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable());
} else { } else {
final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); final CallIntegration callIntegration = requireRtpConnection().getCallIntegration();
updateInCallButtonConfigurationSpeaker( updateInCallButtonConfigurationSpeaker(
audioManager.getSelectedAudioDevice(), callIntegration.getSelectedAudioDevice(),
audioManager.getAudioDevices().size()); callIntegration.getAudioDevices().size());
this.binding.inCallActionFarRight.setVisibility(View.GONE); this.binding.inCallActionFarRight.setVisibility(View.GONE);
} }
if (media.contains(Media.AUDIO)) { if (media.contains(Media.AUDIO)) {
@ -1053,7 +1050,7 @@ public class RtpSessionActivity extends XmppActivity
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private void updateInCallButtonConfigurationSpeaker( private void updateInCallButtonConfigurationSpeaker(
final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) { final CallIntegration.AudioDevice selectedAudioDevice, final int numberOfChoices) {
switch (selectedAudioDevice) { switch (selectedAudioDevice) {
case EARPIECE -> { case EARPIECE -> {
this.binding.inCallActionRight.setImageResource( this.binding.inCallActionRight.setImageResource(
@ -1294,19 +1291,19 @@ public class RtpSessionActivity extends XmppActivity
private void switchToEarpiece(View view) { private void switchToEarpiece(View view) {
requireRtpConnection() requireRtpConnection()
.getAudioManager() .getCallIntegration()
.setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE); .setAudioDevice(CallIntegration.AudioDevice.EARPIECE);
acquireProximityWakeLock(); acquireProximityWakeLock();
} }
private void switchToSpeaker(View view) { private void switchToSpeaker(View view) {
requireRtpConnection() requireRtpConnection()
.getAudioManager() .getCallIntegration()
.setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); .setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE);
releaseProximityWakeLock(); releaseProximityWakeLock();
} }
private void retry(View view) { private void retry(final View view) {
final Intent intent = getIntent(); final Intent intent = getIntent();
final Account account = extractAccount(intent); final Account account = extractAccount(intent);
final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
@ -1315,7 +1312,7 @@ public class RtpSessionActivity extends XmppActivity
final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction); final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
this.rtpConnectionReference = null; this.rtpConnectionReference = null;
Log.d(Config.LOGTAG, "attempting retry with " + with.toEscapedString()); Log.d(Config.LOGTAG, "attempting retry with " + with.toEscapedString());
proposeJingleRtpSession(account, with, media); CallIntegrationConnectionService.placeCall(this,account,with,media);
} }
private void exit(final View view) { private void exit(final View view) {
@ -1411,8 +1408,8 @@ public class RtpSessionActivity extends XmppActivity
@Override @Override
public void onAudioDeviceChanged( public void onAudioDeviceChanged(
final AppRTCAudioManager.AudioDevice selectedAudioDevice, final CallIntegration.AudioDevice selectedAudioDevice,
final Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) { final Set<CallIntegration.AudioDevice> availableAudioDevices) {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
"onAudioDeviceChanged in activity: selected:" "onAudioDeviceChanged in activity: selected:"
@ -1428,11 +1425,11 @@ public class RtpSessionActivity extends XmppActivity
"onAudioDeviceChanged() nothing to do because end card has been reached"); "onAudioDeviceChanged() nothing to do because end card has been reached");
} else { } else {
if (Media.audioOnly(media) && endUserState == RtpEndUserState.CONNECTED) { if (Media.audioOnly(media) && endUserState == RtpEndUserState.CONNECTED) {
final AppRTCAudioManager audioManager = final CallIntegration callIntegration =
requireRtpConnection().getAudioManager(); requireRtpConnection().getCallIntegration();
updateInCallButtonConfigurationSpeaker( updateInCallButtonConfigurationSpeaker(
audioManager.getSelectedAudioDevice(), callIntegration.getSelectedAudioDevice(),
audioManager.getAudioDevices().size()); callIntegration.getAudioDevices().size());
} }
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import android.os.Bundle;
import android.telecom.TelecomManager;
import android.util.Base64; import android.util.Base64;
import android.util.Log; import android.util.Log;
@ -21,6 +23,8 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.CallIntegration;
import eu.siacs.conversations.services.CallIntegrationConnectionService;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
@ -135,6 +139,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return; return;
} }
connections.put(id, connection); connections.put(id, connection);
CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id);
mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateConversationUi();
connection.deliverPacket(packet); connection.deliverPacket(packet);
} else { } else {
@ -148,12 +155,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
} }
public boolean isBusy() { public boolean isBusy() {
if (mXmppConnectionService.isPhoneInCall()) {
return true;
}
for (AbstractJingleConnection connection : this.connections.values()) { for (AbstractJingleConnection connection : this.connections.values()) {
if (connection instanceof JingleRtpConnection) { if (connection instanceof JingleRtpConnection) {
if (((JingleRtpConnection) connection).isTerminated()) { if (connection.isTerminated()) {
continue; continue;
} }
return true; return true;
@ -181,17 +185,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return false; return false;
} }
public void notifyPhoneCallStarted() {
for (AbstractJingleConnection connection : connections.values()) {
if (connection instanceof JingleRtpConnection rtpConnection) {
if (rtpConnection.isTerminated()) {
continue;
}
rtpConnection.notifyPhoneCall();
}
}
}
private Optional<RtpSessionProposal> findMatchingSessionProposal( private Optional<RtpSessionProposal> findMatchingSessionProposal(
final Account account, final Jid with, final Set<Media> media) { final Account account, final Jid with, final Set<Media> media) {
synchronized (this.rtpSessionProposals) { synchronized (this.rtpSessionProposals) {
@ -390,6 +383,8 @@ public class JingleConnectionManager extends AbstractConnectionManager {
this.connections.put(id, rtpConnection); this.connections.put(id, rtpConnection);
rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id);
// TODO actually do the automatic accept?! // TODO actually do the automatic accept?!
} else { } else {
Log.d( Log.d(
@ -439,6 +434,8 @@ public class JingleConnectionManager extends AbstractConnectionManager {
this.connections.put(id, rtpConnection); this.connections.put(id, rtpConnection);
rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id);
} }
} else { } else {
Log.d( Log.d(
@ -457,7 +454,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
if (proposal != null) { if (proposal != null) {
rtpSessionProposals.remove(proposal); rtpSessionProposals.remove(proposal);
final JingleRtpConnection rtpConnection = final JingleRtpConnection rtpConnection =
new JingleRtpConnection(this, id, account.getJid()); new JingleRtpConnection(this, id, account.getJid(), proposal.callIntegration);
rtpConnection.setProposedMedia(proposal.media); rtpConnection.setProposedMedia(proposal.media);
this.connections.put(id, rtpConnection); this.connections.put(id, rtpConnection);
rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED); rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED);
@ -490,6 +487,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
getRtpSessionProposal(account, from.asBareJid(), sessionId); getRtpSessionProposal(account, from.asBareJid(), sessionId);
synchronized (rtpSessionProposals) { synchronized (rtpSessionProposals) {
if (proposal != null && rtpSessionProposals.remove(proposal) != null) { if (proposal != null && rtpSessionProposals.remove(proposal) != null) {
proposal.callIntegration.busy();
writeLogMissedOutgoing( writeLogMissedOutgoing(
account, proposal.with, proposal.sessionId, serverMsgId, timestamp); account, proposal.with, proposal.sessionId, serverMsgId, timestamp);
toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media); toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media);
@ -628,10 +626,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return Optional.absent(); return Optional.absent();
} }
void finishConnection(final AbstractJingleConnection connection) {
this.connections.remove(connection.getId());
}
void finishConnectionOrThrow(final AbstractJingleConnection connection) { void finishConnectionOrThrow(final AbstractJingleConnection connection) {
final AbstractJingleConnection.Id id = connection.getId(); final AbstractJingleConnection.Id id = connection.getId();
if (this.connections.remove(id) == null) { if (this.connections.remove(id) == null) {
@ -680,6 +674,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
+ ": retracting rtp session proposal with " + ": retracting rtp session proposal with "
+ rtpSessionProposal.with); + rtpSessionProposal.with);
this.rtpSessionProposals.remove(rtpSessionProposal); this.rtpSessionProposals.remove(rtpSessionProposal);
rtpSessionProposal.callIntegration.retracted();
final MessagePacket messagePacket = final MessagePacket messagePacket =
mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal); mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal);
writeLogMissedOutgoing( writeLogMissedOutgoing(
@ -691,7 +686,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
mXmppConnectionService.sendMessagePacket(account, messagePacket); mXmppConnectionService.sendMessagePacket(account, messagePacket);
} }
public String initializeRtpSession( public JingleRtpConnection initializeRtpSession(
final Account account, final Jid with, final Set<Media> media) { final Account account, final Jid with, final Set<Media> media) {
final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with); final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with);
final JingleRtpConnection rtpConnection = final JingleRtpConnection rtpConnection =
@ -699,15 +694,15 @@ public class JingleConnectionManager extends AbstractConnectionManager {
rtpConnection.setProposedMedia(media); rtpConnection.setProposedMedia(media);
this.connections.put(id, rtpConnection); this.connections.put(id, rtpConnection);
rtpConnection.sendSessionInitiate(); rtpConnection.sendSessionInitiate();
return id.sessionId; return rtpConnection;
} }
public void proposeJingleRtpSession( public RtpSessionProposal proposeJingleRtpSession(
final Account account, final Jid with, final Set<Media> media) { final Account account, final Jid with, final Set<Media> media) {
synchronized (this.rtpSessionProposals) { synchronized (this.rtpSessionProposals) {
for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry : for (final Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
this.rtpSessionProposals.entrySet()) { this.rtpSessionProposals.entrySet()) {
RtpSessionProposal proposal = entry.getKey(); final RtpSessionProposal proposal = entry.getKey();
if (proposal.account == account && with.asBareJid().equals(proposal.with)) { if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
final DeviceDiscoveryState preexistingState = entry.getValue(); final DeviceDiscoveryState preexistingState = entry.getValue();
if (preexistingState != null if (preexistingState != null
@ -716,7 +711,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
toneManager.transition(endUserState, media); toneManager.transition(endUserState, media);
mXmppConnectionService.notifyJingleRtpConnectionUpdate( mXmppConnectionService.notifyJingleRtpConnectionUpdate(
account, with, proposal.sessionId, endUserState); account, with, proposal.sessionId, endUserState);
return; return proposal;
} }
} }
} }
@ -725,19 +720,23 @@ public class JingleConnectionManager extends AbstractConnectionManager {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
"ignoring request to propose jingle session because the other party already created one for us"); "ignoring request to propose jingle session because the other party already created one for us");
return; // TODO return something that we can parse the connection of of
return null;
} }
throw new IllegalStateException( throw new IllegalStateException(
"There is already a running RTP session. This should have been caught by the UI"); "There is already a running RTP session. This should have been caught by the UI");
} }
final CallIntegration callIntegration = new CallIntegration(mXmppConnectionService.getApplicationContext());
callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media));
final RtpSessionProposal proposal = final RtpSessionProposal proposal =
RtpSessionProposal.of(account, with.asBareJid(), media); RtpSessionProposal.of(account, with.asBareJid(), media, callIntegration);
this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING); this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING);
mXmppConnectionService.notifyJingleRtpConnectionUpdate( mXmppConnectionService.notifyJingleRtpConnectionUpdate(
account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE); account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE);
final MessagePacket messagePacket = final MessagePacket messagePacket =
mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
mXmppConnectionService.sendMessagePacket(account, messagePacket); mXmppConnectionService.sendMessagePacket(account, messagePacket);
return proposal;
} }
} }
@ -826,6 +825,21 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return null; return null;
} }
public JingleRtpConnection findJingleRtpConnection(final Account account, final Jid with) {
for (final AbstractJingleConnection connection : this.connections.values()) {
if (connection instanceof JingleRtpConnection rtpConnection) {
if (rtpConnection.isTerminated()) {
continue;
}
final var id = rtpConnection.getId();
if (id.account == account && account.getJid().equals(with)) {
return rtpConnection;
}
}
}
return null;
}
private void resendSessionProposals(final Account account) { private void resendSessionProposals(final Account account) {
synchronized (this.rtpSessionProposals) { synchronized (this.rtpSessionProposals) {
for (final Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry : for (final Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
@ -865,7 +879,10 @@ public class JingleConnectionManager extends AbstractConnectionManager {
} }
this.rtpSessionProposals.put(sessionProposal, target); this.rtpSessionProposals.put(sessionProposal, target);
final RtpEndUserState endUserState = target.toEndUserState(); final RtpEndUserState endUserState = target.toEndUserState();
toneManager.transition(endUserState, sessionProposal.media); if (endUserState == RtpEndUserState.RINGING) {
sessionProposal.callIntegration.setDialing();
}
//toneManager.transition(endUserState, sessionProposal.media);
mXmppConnectionService.notifyJingleRtpConnectionUpdate( mXmppConnectionService.notifyJingleRtpConnectionUpdate(
account, sessionProposal.with, sessionProposal.sessionId, endUserState); account, sessionProposal.with, sessionProposal.sessionId, endUserState);
Log.d( Log.d(
@ -994,16 +1011,18 @@ public class JingleConnectionManager extends AbstractConnectionManager {
public final String sessionId; public final String sessionId;
public final Set<Media> media; public final Set<Media> media;
private final Account account; private final Account account;
private final CallIntegration callIntegration;
private RtpSessionProposal(Account account, Jid with, String sessionId, Set<Media> media) { private RtpSessionProposal(Account account, Jid with, String sessionId, Set<Media> media, final CallIntegration callIntegration) {
this.account = account; this.account = account;
this.with = with; this.with = with;
this.sessionId = sessionId; this.sessionId = sessionId;
this.media = media; this.media = media;
this.callIntegration = callIntegration;
} }
public static RtpSessionProposal of(Account account, Jid with, Set<Media> media) { public static RtpSessionProposal of(Account account, Jid with, Set<Media> media, final CallIntegration callIntegration) {
return new RtpSessionProposal(account, with, nextRandomId(), media); return new RtpSessionProposal(account, with, nextRandomId(), media,callIntegration);
} }
@Override @Override
@ -1035,5 +1054,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
public String getSessionId() { public String getSessionId() {
return sessionId; return sessionId;
} }
public CallIntegration getCallIntegration() {
return this.callIntegration;
}
} }
} }

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import android.telecom.Call;
import android.telecom.TelecomManager;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -12,13 +14,11 @@ import com.google.common.base.Stopwatch;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -34,7 +34,7 @@ import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.utils.IP; import eu.siacs.conversations.services.CallIntegration;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
@ -67,7 +67,7 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class JingleRtpConnection extends AbstractJingleConnection public class JingleRtpConnection extends AbstractJingleConnection
implements WebRTCWrapper.EventCallback { implements WebRTCWrapper.EventCallback, CallIntegration.Callback {
public static final List<State> STATES_SHOWING_ONGOING_CALL = public static final List<State> STATES_SHOWING_ONGOING_CALL =
Arrays.asList( Arrays.asList(
@ -78,6 +78,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private final Queue<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>> private final Queue<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>>
pendingIceCandidates = new LinkedList<>(); pendingIceCandidates = new LinkedList<>();
private final OmemoVerification omemoVerification = new OmemoVerification(); private final OmemoVerification omemoVerification = new OmemoVerification();
private final CallIntegration callIntegration;
private final Message message; private final Message message;
private Set<Media> proposedMedia; private Set<Media> proposedMedia;
@ -90,7 +91,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
private final Queue<PeerConnection.PeerConnectionState> stateHistory = new LinkedList<>(); private final Queue<PeerConnection.PeerConnectionState> stateHistory = new LinkedList<>();
private ScheduledFuture<?> ringingTimeoutFuture; private ScheduledFuture<?> ringingTimeoutFuture;
JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { JingleRtpConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator) {
this(jingleConnectionManager, id, initiator, new CallIntegration(jingleConnectionManager.getXmppConnectionService().getApplicationContext()));
this.callIntegration.setAddress(CallIntegration.address(id.with.asBareJid()), TelecomManager.PRESENTATION_ALLOWED);
this.callIntegration.setInitialized();
}
JingleRtpConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator, final CallIntegration callIntegration) {
super(jingleConnectionManager, id, initiator); super(jingleConnectionManager, id, initiator);
final Conversation conversation = final Conversation conversation =
jingleConnectionManager jingleConnectionManager
@ -102,6 +109,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED, isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED,
Message.TYPE_RTP_SESSION, Message.TYPE_RTP_SESSION,
id.sessionId); id.sessionId);
this.callIntegration = callIntegration;
this.callIntegration.setCallback(this);
} }
@Override @Override
@ -1158,6 +1167,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
target = State.SESSION_INITIALIZED_PRE_APPROVED; target = State.SESSION_INITIALIZED_PRE_APPROVED;
} else { } else {
target = State.SESSION_INITIALIZED; target = State.SESSION_INITIALIZED;
setProposedMedia(contentMap.getMedia());
} }
if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) { if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
respondOk(jinglePacket); respondOk(jinglePacket);
@ -1628,7 +1638,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
+ from + from
+ " for " + " for "
+ media); + media);
this.proposedMedia = Sets.newHashSet(media); this.setProposedMedia(Sets.newHashSet(media));
})) { })) {
if (serverMsgId != null) { if (serverMsgId != null) {
this.message.setServerMsgId(serverMsgId); this.message.setServerMsgId(serverMsgId);
@ -1648,6 +1658,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
} }
private void startRinging() { private void startRinging() {
this.callIntegration.setRinging();
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
id.account.getJid().asBareJid() id.account.getJid().asBareJid()
@ -1657,6 +1668,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
ringingTimeoutFuture = ringingTimeoutFuture =
jingleConnectionManager.schedule( jingleConnectionManager.schedule(
this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS); this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS);
if (CallIntegration.selfManaged()) {
return;
}
xmppConnectionService.getNotificationService().startRinging(id, getMedia()); xmppConnectionService.getNotificationService().startRinging(id, getMedia());
} }
@ -2054,6 +2068,56 @@ public class JingleRtpConnection extends AbstractJingleConnection
}; };
} }
private boolean isPeerConnectionConnected() {
try {
return webRTCWrapper.getState() == PeerConnection.PeerConnectionState.CONNECTED;
} catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
return false;
}
}
private void updateCallIntegrationState() {
switch (this.state) {
case NULL, PROPOSED, SESSION_INITIALIZED -> {
if (isInitiator()) {
this.callIntegration.setDialing();
} else {
this.callIntegration.setRinging();
}
}
case PROCEED, SESSION_INITIALIZED_PRE_APPROVED -> {
if (isInitiator()) {
this.callIntegration.setDialing();
} else {
this.callIntegration.setInitialized();
}
}
case SESSION_ACCEPTED -> {
if (isPeerConnectionConnected()) {
this.callIntegration.setActive();
} else {
this.callIntegration.setInitialized();
}
}
case REJECTED, REJECTED_RACED, TERMINATED_DECLINED_OR_BUSY -> {
if (isInitiator()) {
this.callIntegration.busy();
} else {
this.callIntegration.rejected();
}
}
case TERMINATED_SUCCESS -> this.callIntegration.success();
case ACCEPTED -> this.callIntegration.accepted();
case RETRACTED, RETRACTED_RACED, TERMINATED_CANCEL_OR_TIMEOUT -> this.callIntegration
.retracted();
case TERMINATED_CONNECTIVITY_ERROR,
TERMINATED_APPLICATION_FAILURE,
TERMINATED_SECURITY_ERROR -> this.callIntegration.error();
default -> throw new IllegalStateException(
String.format("%s is not handled", this.state));
}
}
public ContentAddition getPendingContentAddition() { public ContentAddition getPendingContentAddition() {
final RtpContentMap in = this.incomingContentAdd; final RtpContentMap in = this.incomingContentAdd;
final RtpContentMap out = this.outgoingContentAdd; final RtpContentMap out = this.outgoingContentAdd;
@ -2135,15 +2199,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
} }
} }
public void notifyPhoneCall() {
Log.d(Config.LOGTAG, "a phone call has just been started. killing jingle rtp connections");
if (Arrays.asList(State.PROPOSED, State.SESSION_INITIALIZED).contains(this.state)) {
rejectCall();
} else {
endCall();
}
}
public synchronized void rejectCall() { public synchronized void rejectCall() {
if (isTerminated()) { if (isTerminated()) {
Log.w( Log.w(
@ -2537,8 +2592,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void modifyLocalContentMap(final RtpContentMap rtpContentMap) { private void modifyLocalContentMap(final RtpContentMap rtpContentMap) {
final RtpContentMap activeContents = rtpContentMap.activeContents(); final RtpContentMap activeContents = rtpContentMap.activeContents();
setLocalContentMap(activeContents); setLocalContentMap(activeContents);
this.webRTCWrapper.switchSpeakerPhonePreference( // TODO change audio device on callIntegration was (`switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference.of(activeContents.getMedia())`)
AppRTCAudioManager.SpeakerPhonePreference.of(activeContents.getMedia()));
updateEndUserState(); updateEndUserState();
} }
@ -2571,8 +2625,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS); return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS);
} }
public AppRTCAudioManager getAudioManager() {
return webRTCWrapper.getAudioManager(); public CallIntegration getCallIntegration() {
return this.callIntegration;
} }
public boolean isMicrophoneEnabled() { public boolean isMicrophoneEnabled() {
@ -2603,10 +2658,26 @@ public class JingleRtpConnection extends AbstractJingleConnection
return webRTCWrapper.switchCamera(); return webRTCWrapper.switchCamera();
} }
@Override
public void onCallIntegrationShowIncomingCallUi() {
xmppConnectionService.getNotificationService().startRinging(id, getMedia());
}
@Override
public void onCallIntegrationDisconnect() {
Log.d(Config.LOGTAG, "a phone call has just been started. killing jingle rtp connections");
if (Arrays.asList(State.PROPOSED, State.SESSION_INITIALIZED).contains(this.state)) {
rejectCall();
} else {
endCall();
}
}
@Override @Override
public void onAudioDeviceChanged( public void onAudioDeviceChanged(
AppRTCAudioManager.AudioDevice selectedAudioDevice, final CallIntegration.AudioDevice selectedAudioDevice,
Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) { final Set<CallIntegration.AudioDevice> availableAudioDevices) {
Log.d(Config.LOGTAG,"onAudioDeviceChanged("+selectedAudioDevice+","+availableAudioDevices+")");
xmppConnectionService.notifyJingleRtpConnectionUpdate( xmppConnectionService.notifyJingleRtpConnectionUpdate(
selectedAudioDevice, availableAudioDevices); selectedAudioDevice, availableAudioDevices);
} }
@ -2614,6 +2685,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void updateEndUserState() { private void updateEndUserState() {
final RtpEndUserState endUserState = getEndUserState(); final RtpEndUserState endUserState = getEndUserState();
jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia()); jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia());
this.updateCallIntegrationState();
xmppConnectionService.notifyJingleRtpConnectionUpdate( xmppConnectionService.notifyJingleRtpConnectionUpdate(
id.account, id.with, id.sessionId, endUserState); id.account, id.with, id.sessionId, endUserState);
} }
@ -2670,6 +2742,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
protected void finish() { protected void finish() {
if (isTerminated()) { if (isTerminated()) {
this.cancelRingingTimeout(); this.cancelRingingTimeout();
this.callIntegration.verifyDisconnected();
this.webRTCWrapper.verifyClosed(); this.webRTCWrapper.verifyClosed();
this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia()); this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
super.finish(); super.finish();
@ -2724,6 +2797,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
void setProposedMedia(final Set<Media> media) { void setProposedMedia(final Set<Media> media) {
this.proposedMedia = media; this.proposedMedia = media;
this.callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media));
} }
public void fireStateUpdate() { public void fireStateUpdate() {

View file

@ -9,7 +9,7 @@ public enum RtpEndUserState {
FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet
RINGING, //'propose' has been sent out and it has been 184 acked RINGING, //'propose' has been sent out and it has been 184 acked
ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received
ENDING_CALL, //libwebrt says 'closed' but session-terminate hasnt gone through ENDING_CALL, //libwebrt says 'closed' but session-terminate has not gone through
ENDED, //close UI ENDED, //close UI
DECLINED_OR_BUSY, //other party declined; no retry button DECLINED_OR_BUSY, //other party declined; no retry button
CONNECTIVITY_ERROR, //network error; retry button CONNECTIVITY_ERROR, //network error; retry button

View file

@ -89,7 +89,8 @@ class ToneManager {
} }
switch (state) { switch (state) {
case RINGING: case RINGING:
scheduleWaitingTone(); // ringing can be removed as this is now handled by 'CallIntegration'
//scheduleWaitingTone();
break; break;
case CONNECTED: case CONNECTED:
scheduleConnected(); scheduleConnected();

View file

@ -16,6 +16,7 @@ import com.google.common.util.concurrent.SettableFuture;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.services.CallIntegration;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import org.webrtc.AudioSource; import org.webrtc.AudioSource;
@ -83,16 +84,6 @@ public class WebRTCWrapper {
private final EventCallback eventCallback; private final EventCallback eventCallback;
private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false); private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false);
private final Queue<IceCandidate> iceCandidates = new LinkedList<>(); private final Queue<IceCandidate> iceCandidates = new LinkedList<>();
private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents =
new AppRTCAudioManager.AudioManagerEvents() {
@Override
public void onAudioDeviceChanged(
AppRTCAudioManager.AudioDevice selectedAudioDevice,
Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
}
};
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private TrackWrapper<AudioTrack> localAudioTrack = null; private TrackWrapper<AudioTrack> localAudioTrack = null;
private TrackWrapper<VideoTrack> localVideoTrack = null; private TrackWrapper<VideoTrack> localVideoTrack = null;
private VideoTrack remoteVideoTrack = null; private VideoTrack remoteVideoTrack = null;
@ -214,7 +205,6 @@ public class WebRTCWrapper {
}; };
@Nullable private PeerConnectionFactory peerConnectionFactory = null; @Nullable private PeerConnectionFactory peerConnectionFactory = null;
@Nullable private PeerConnection peerConnection = null; @Nullable private PeerConnection peerConnection = null;
private AppRTCAudioManager appRTCAudioManager = null;
private ToneManager toneManager = null; private ToneManager toneManager = null;
private Context context = null; private Context context = null;
private EglBase eglBase = null; private EglBase eglBase = null;
@ -251,15 +241,6 @@ public class WebRTCWrapper {
} }
this.context = service; this.context = service;
this.toneManager = service.getJingleConnectionManager().toneManager; this.toneManager = service.getJingleConnectionManager().toneManager;
mainHandler.post(
() -> {
appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference);
toneManager.setAppRtcAudioManagerHasControl(true);
appRTCAudioManager.start(audioManagerEvents);
eventCallback.onAudioDeviceChanged(
appRTCAudioManager.getSelectedAudioDevice(),
appRTCAudioManager.getAudioDevices());
});
} }
synchronized void initializePeerConnection( synchronized void initializePeerConnection(
@ -462,16 +443,11 @@ public class WebRTCWrapper {
final PeerConnection peerConnection = this.peerConnection; final PeerConnection peerConnection = this.peerConnection;
final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory; final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
final AppRTCAudioManager audioManager = this.appRTCAudioManager;
final EglBase eglBase = this.eglBase; final EglBase eglBase = this.eglBase;
if (peerConnection != null) { if (peerConnection != null) {
this.peerConnection = null; this.peerConnection = null;
dispose(peerConnection); dispose(peerConnection);
} }
if (audioManager != null) {
toneManager.setAppRtcAudioManagerHasControl(false);
mainHandler.post(audioManager::stop);
}
this.localVideoTrack = null; this.localVideoTrack = null;
this.remoteVideoTrack = null; this.remoteVideoTrack = null;
if (videoSourceWrapper != null) { if (videoSourceWrapper != null) {
@ -498,8 +474,8 @@ public class WebRTCWrapper {
|| this.eglBase != null || this.eglBase != null
|| this.localVideoTrack != null || this.localVideoTrack != null
|| this.remoteVideoTrack != null) { || this.remoteVideoTrack != null) {
final IllegalStateException e = final AssertionError e =
new IllegalStateException("WebRTCWrapper hasn't been closed properly"); new AssertionError("WebRTCWrapper hasn't been closed properly");
Log.e(Config.LOGTAG, "verifyClosed() failed. Going to throw", e); Log.e(Config.LOGTAG, "verifyClosed() failed. Going to throw", e);
throw e; throw e;
} }
@ -750,27 +726,15 @@ public class WebRTCWrapper {
return context; return context;
} }
AppRTCAudioManager getAudioManager() {
return appRTCAudioManager;
}
void execute(final Runnable command) { void execute(final Runnable command) {
this.executorService.execute(command); this.executorService.execute(command);
} }
public void switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference preference) {
mainHandler.post(() -> appRTCAudioManager.switchSpeakerPhonePreference(preference));
}
public interface EventCallback { public interface EventCallback {
void onIceCandidate(IceCandidate iceCandidate); void onIceCandidate(IceCandidate iceCandidate);
void onConnectionChange(PeerConnection.PeerConnectionState newState); void onConnectionChange(PeerConnection.PeerConnectionState newState);
void onAudioDeviceChanged(
AppRTCAudioManager.AudioDevice selectedAudioDevice,
Set<AppRTCAudioManager.AudioDevice> availableAudioDevices);
void onRenegotiationNeeded(); void onRenegotiationNeeded();
} }