diff --git a/build.gradle b/build.gradle
index b0e622754..e3fb55a76 100644
--- a/build.gradle
+++ b/build.gradle
@@ -95,7 +95,7 @@ android {
compileSdk 34
defaultConfig {
- minSdkVersion 21
+ minSdkVersion 23
targetSdkVersion 34
versionCode 42094
versionName "2.13.4"
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index c01009862..6233770df 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -5,9 +5,6 @@
-
@@ -50,6 +47,8 @@
+
+
@@ -133,6 +132,14 @@
+
+
+
+
+
+
diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java
index 3bed4eaba..a472445d3 100644
--- a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java
+++ b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java
@@ -58,18 +58,18 @@ public class AppRTCAudioManager {
private boolean hasWiredHeadset;
// Default audio device; speaker phone for video calls or earpiece for audio
// only calls.
- private AudioDevice defaultAudioDevice;
+ private CallIntegration.AudioDevice defaultAudioDevice;
// Contains the currently selected audio device.
// This device is changed automatically using a certain scheme where e.g.
// a wired headset "wins" over speaker phone. It is also possible for a
// user to explicitly select a device (and overrid any predefined scheme).
// See |userSelectedAudioDevice| for details.
- private AudioDevice selectedAudioDevice;
+ private CallIntegration.AudioDevice selectedAudioDevice;
// Contains the user-selected audio device which overrides the predefined
// selection scheme.
// TODO(henrika): always set to AudioDevice.NONE today. Add support for
// explicit selection based on choice by userSelectedAudioDevice.
- private AudioDevice userSelectedAudioDevice;
+ private CallIntegration.AudioDevice userSelectedAudioDevice;
// Proximity sensor object. It measures the proximity of an object in cm
// relative to the view screen of a device and can therefore be used to
// assist device switching (close to ear <=> use headset earpiece if
@@ -78,26 +78,25 @@ public class AppRTCAudioManager {
private AppRTCProximitySensor proximitySensor;
// Contains a list of available audio devices. A Set collection is used to
// avoid duplicate elements.
- private Set audioDevices = new HashSet<>();
+ private Set audioDevices = new HashSet<>();
// Broadcast receiver for wired headset intent broadcasts.
private final BroadcastReceiver wiredHeadsetReceiver;
// Callback method for changes in audio focus.
@Nullable
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
- private AppRTCAudioManager(Context context, final SpeakerPhonePreference speakerPhonePreference) {
- Log.d(Config.LOGTAG, "ctor");
+ public AppRTCAudioManager(final Context context) {
ThreadUtils.checkIsOnMainThread();
apprtcContext = context;
audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
bluetoothManager = AppRTCBluetoothManager.create(context, this);
wiredHeadsetReceiver = new WiredHeadsetReceiver();
amState = AudioManagerState.UNINITIALIZED;
- this.speakerPhonePreference = speakerPhonePreference;
- if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) {
- defaultAudioDevice = AudioDevice.EARPIECE;
+ // CallIntegration / Connection uses Earpiece as default too
+ if (hasEarpiece()) {
+ defaultAudioDevice = CallIntegration.AudioDevice.EARPIECE;
} else {
- defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
+ defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
}
// Create and initialize the proximity sensor.
// Tablet devices (e.g. Nexus 7) does not support proximity sensors.
@@ -114,20 +113,13 @@ public class AppRTCAudioManager {
public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) {
this.speakerPhonePreference = speakerPhonePreference;
if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) {
- defaultAudioDevice = AudioDevice.EARPIECE;
+ defaultAudioDevice = CallIntegration.AudioDevice.EARPIECE;
} else {
- defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
+ defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
}
updateAudioDeviceState();
}
- /**
- * Construction.
- */
- public static AppRTCAudioManager create(Context context, SpeakerPhonePreference speakerPhonePreference) {
- return new AppRTCAudioManager(context, speakerPhonePreference);
- }
-
public static boolean isMicrophoneAvailable() {
microphoneLatch = new CountDownLatch(1);
AudioRecord audioRecord = null;
@@ -174,16 +166,16 @@ public class AppRTCAudioManager {
}
// The proximity sensor should only be activated when there are exactly two
// available audio devices.
- if (audioDevices.size() == 2 && audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE)
- && audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) {
+ if (audioDevices.size() == 2 && audioDevices.contains(CallIntegration.AudioDevice.EARPIECE)
+ && audioDevices.contains(CallIntegration.AudioDevice.SPEAKER_PHONE)) {
if (proximitySensor.sensorReportsNearState()) {
// Sensor reports that a "handset is being held up to a person's ear",
// or "something is covering the light sensor".
- setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.EARPIECE);
+ setAudioDeviceInternal(CallIntegration.AudioDevice.EARPIECE);
} else {
// Sensor reports that a "handset is removed from a person's ear", or
// "the light sensor is no longer covered".
- setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
+ setAudioDeviceInternal(CallIntegration.AudioDevice.SPEAKER_PHONE);
}
}
}
@@ -258,8 +250,8 @@ public class AppRTCAudioManager {
// Always disable microphone mute during a WebRTC call.
setMicrophoneMute(false);
// Set initial device states.
- userSelectedAudioDevice = AudioDevice.NONE;
- selectedAudioDevice = AudioDevice.NONE;
+ userSelectedAudioDevice = CallIntegration.AudioDevice.NONE;
+ selectedAudioDevice = CallIntegration.AudioDevice.NONE;
audioDevices.clear();
// Initialize and start Bluetooth if a BT device is available or initiate
// detection of new (enabled) BT devices.
@@ -315,7 +307,7 @@ public class AppRTCAudioManager {
/**
* 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 + ")");
AppRTCUtils.assertIsTrue(audioDevices.contains(device));
switch (device) {
@@ -338,7 +330,7 @@ public class AppRTCAudioManager {
* Changes default audio device.
* TODO(henrika): add usage of this method in the AppRTCMobile client.
*/
- public void setDefaultAudioDevice(AudioDevice defaultDevice) {
+ public void setDefaultAudioDevice(CallIntegration.AudioDevice defaultDevice) {
ThreadUtils.checkIsOnMainThread();
switch (defaultDevice) {
case SPEAKER_PHONE:
@@ -348,7 +340,7 @@ public class AppRTCAudioManager {
if (hasEarpiece()) {
defaultAudioDevice = defaultDevice;
} else {
- defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
+ defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
}
break;
default:
@@ -362,7 +354,7 @@ public class AppRTCAudioManager {
/**
* Changes selection of the currently active audio device.
*/
- public void selectAudioDevice(AudioDevice device) {
+ public void selectAudioDevice(CallIntegration.AudioDevice device) {
ThreadUtils.checkIsOnMainThread();
if (!audioDevices.contains(device)) {
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.
*/
- public Set getAudioDevices() {
+ public Set getAudioDevices() {
ThreadUtils.checkIsOnMainThread();
return Collections.unmodifiableSet(new HashSet<>(audioDevices));
}
@@ -382,7 +374,7 @@ public class AppRTCAudioManager {
/**
* Returns the currently selected audio device.
*/
- public AudioDevice getSelectedAudioDevice() {
+ public CallIntegration.AudioDevice getSelectedAudioDevice() {
ThreadUtils.checkIsOnMainThread();
return selectedAudioDevice;
}
@@ -479,21 +471,21 @@ public class AppRTCAudioManager {
bluetoothManager.updateDevice();
}
// Update the set of available audio devices.
- Set newAudioDevices = new HashSet<>();
+ Set newAudioDevices = new HashSet<>();
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
- newAudioDevices.add(AudioDevice.BLUETOOTH);
+ newAudioDevices.add(CallIntegration.AudioDevice.BLUETOOTH);
}
if (hasWiredHeadset) {
// 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 {
// No wired headset, hence the audio-device list can contain speaker
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
- newAudioDevices.add(AudioDevice.SPEAKER_PHONE);
+ newAudioDevices.add(CallIntegration.AudioDevice.SPEAKER_PHONE);
if (hasEarpiece()) {
- newAudioDevices.add(AudioDevice.EARPIECE);
+ newAudioDevices.add(CallIntegration.AudioDevice.EARPIECE);
}
}
// Store state which is set to true if the device list has changed.
@@ -502,33 +494,33 @@ public class AppRTCAudioManager {
audioDevices = newAudioDevices;
// Correct user selected audio devices if needed.
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.
- 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
// 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
// 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
// user did not select any output device.
boolean needBluetoothAudioStart =
bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
- && (userSelectedAudioDevice == AudioDevice.NONE
- || userSelectedAudioDevice == AudioDevice.BLUETOOTH);
+ && (userSelectedAudioDevice == CallIntegration.AudioDevice.NONE
+ || userSelectedAudioDevice == CallIntegration.AudioDevice.BLUETOOTH);
// Need to stop Bluetooth audio if user selected different device and
// Bluetooth SCO connection is established or in the process.
boolean needBluetoothAudioStop =
(bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING)
- && (userSelectedAudioDevice != AudioDevice.NONE
- && userSelectedAudioDevice != AudioDevice.BLUETOOTH);
+ && (userSelectedAudioDevice != CallIntegration.AudioDevice.NONE
+ && userSelectedAudioDevice != CallIntegration.AudioDevice.BLUETOOTH);
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|| 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).
if (!bluetoothManager.startScoAudio()) {
// Remove BLUETOOTH from list of available devices since SCO failed.
- audioDevices.remove(AudioDevice.BLUETOOTH);
+ audioDevices.remove(CallIntegration.AudioDevice.BLUETOOTH);
audioDeviceSetUpdated = true;
}
}
// Update selected audio device.
- final AudioDevice newAudioDevice;
+ final CallIntegration.AudioDevice newAudioDevice;
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
// If a Bluetooth is connected, then it should be used as output audio
// device. Note that it is not sufficient that a headset is available;
// an active SCO channel must also be up and running.
- newAudioDevice = AudioDevice.BLUETOOTH;
+ newAudioDevice = CallIntegration.AudioDevice.BLUETOOTH;
} else if (hasWiredHeadset) {
// If a wired headset is connected, but Bluetooth is not, then wired headset is used as
// audio device.
- newAudioDevice = AudioDevice.WIRED_HEADSET;
+ newAudioDevice = CallIntegration.AudioDevice.WIRED_HEADSET;
} else {
// No wired headset and no Bluetooth, hence the audio-device list can contain speaker
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
@@ -582,12 +574,6 @@ public class AppRTCAudioManager {
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.
*/
@@ -615,7 +601,7 @@ public class AppRTCAudioManager {
public interface AudioManagerEvents {
// Callback fired once audio device is changed or list of available audio devices changed.
void onAudioDeviceChanged(
- AudioDevice selectedAudioDevice, Set availableAudioDevices);
+ CallIntegration.AudioDevice selectedAudioDevice, Set availableAudioDevices);
}
/* Receiver which handles changes in wired headset availability. */
diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java
new file mode 100644
index 000000000..bf12f5f4b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java
@@ -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 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 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 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 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 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 getAudioDevicesOreo(final CallAudioState callAudioState) {
+ final ImmutableSet.Builder 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 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 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) {
+ 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 availableAudioDevices);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java
new file mode 100644
index 000000000..cfd6ae603
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java
@@ -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 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 =
+ 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 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) {
+ 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 future) {
+ try {
+ return future.get(2, TimeUnit.SECONDS).service;
+ } catch (final ExecutionException | InterruptedException | TimeoutException e) {
+ return null;
+ }
+ }
+
+ public static ListenableFuture bindService(
+ final Context context) {
+ final SettableFuture 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;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index 584237156..5f48fca58 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -198,6 +198,7 @@ public class XmppConnectionService extends Service {
public static final String ACTION_DISMISS_CALL = "dismiss_call";
public static final String ACTION_END_CALL = "end_call";
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";
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";
@@ -303,16 +304,6 @@ public class XmppConnectionService extends Service {
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;
@@ -1288,6 +1279,8 @@ public class XmppConnectionService extends Service {
toggleSetProfilePictureActivity(hasEnabledAccounts);
reconfigurePushDistributor();
+ CallIntegrationConnectionService.registerPhoneAccounts(this, this.accounts);
+
restoreFromDatabase();
if (QuickConversationsService.isContactListIntegration(this)
@@ -1351,23 +1344,10 @@ public class XmppConnectionService extends Service {
ContextCompat.RECEIVER_EXPORTED);
mForceDuringOnCreate.set(false);
toggleForegroundService();
- setupPhoneStateListener();
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() {
if (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 availableAudioDevices) {
+ public void notifyJingleRtpConnectionUpdate(CallIntegration.AudioDevice selectedAudioDevice, Set availableAudioDevices) {
for (OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) {
listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
}
@@ -5110,7 +5090,7 @@ public class XmppConnectionService extends Service {
public interface OnJingleRtpConnectionUpdate {
void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state);
- void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices);
+ void onAudioDeviceChanged(CallIntegration.AudioDevice selectedAudioDevice, Set availableAudioDevices);
}
public interface OnAccountUpdate {
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
index d19bc4a55..ccb08f95f 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -86,6 +86,7 @@ import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.http.HttpDownloadConnection;
import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.CallIntegrationConnectionService;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.QuickConversationsService;
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) {
- 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.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString());
intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
- startActivity(intent);
+ startActivity(intent);*/
}
private void handleAttachmentSelection(MenuItem item) {
diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java
index 5b5c82bae..8f546918a 100644
--- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java
@@ -49,6 +49,8 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
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.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MainThreadExecutor;
@@ -133,7 +135,7 @@ public class RtpSessionActivity extends XmppActivity
}
};
- private static Set actionToMedia(final String action) {
+ public static Set actionToMedia(final String action) {
if (ACTION_MAKE_VIDEO_CALL.equals(action)) {
return ImmutableSet.of(Media.AUDIO, Media.VIDEO);
} else {
@@ -416,11 +418,11 @@ public class RtpSessionActivity extends XmppActivity
if (Media.audioOnly(media)) {
final JingleRtpConnection rtpConnection =
rtpConnectionReference != null ? rtpConnectionReference.get() : null;
- final AppRTCAudioManager audioManager =
- rtpConnection == null ? null : rtpConnection.getAudioManager();
- if (audioManager == null
- || audioManager.getSelectedAudioDevice()
- == AppRTCAudioManager.AudioDevice.EARPIECE) {
+ final CallIntegration callIntegration =
+ rtpConnection == null ? null : rtpConnection.getCallIntegration();
+ if (callIntegration == null
+ || callIntegration.getSelectedAudioDevice()
+ == CallIntegration.AudioDevice.EARPIECE) {
acquireProximityWakeLock();
}
}
@@ -466,8 +468,8 @@ public class RtpSessionActivity extends XmppActivity
}
private void putProximityWakeLockInProperState(
- final AppRTCAudioManager.AudioDevice audioDevice) {
- if (audioDevice == AppRTCAudioManager.AudioDevice.EARPIECE) {
+ final CallIntegration.AudioDevice audioDevice) {
+ if (audioDevice == CallIntegration.AudioDevice.EARPIECE) {
acquireProximityWakeLock();
} else {
releaseProximityWakeLock();
@@ -581,12 +583,7 @@ public class RtpSessionActivity extends XmppActivity
.getJingleConnectionManager()
.proposeJingleRtpSession(account, with, media);
} else {
- final String sessionId =
- xmppConnectionService
- .getJingleConnectionManager()
- .initializeRtpSession(account, with, media);
- initializeActivityWithRunningRtpSession(account, with, sessionId);
- resetIntent(account, with, sessionId);
+ throw new IllegalStateException("We should not be initializing direct calls from the RtpSessionActivity. Go through CallIntegrationConnectionService.placeCall instead!");
}
putScreenInCallMode(media);
}
@@ -1032,10 +1029,10 @@ public class RtpSessionActivity extends XmppActivity
updateInCallButtonConfigurationVideo(
rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable());
} else {
- final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
+ final CallIntegration callIntegration = requireRtpConnection().getCallIntegration();
updateInCallButtonConfigurationSpeaker(
- audioManager.getSelectedAudioDevice(),
- audioManager.getAudioDevices().size());
+ callIntegration.getSelectedAudioDevice(),
+ callIntegration.getAudioDevices().size());
this.binding.inCallActionFarRight.setVisibility(View.GONE);
}
if (media.contains(Media.AUDIO)) {
@@ -1053,7 +1050,7 @@ public class RtpSessionActivity extends XmppActivity
@SuppressLint("RestrictedApi")
private void updateInCallButtonConfigurationSpeaker(
- final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) {
+ final CallIntegration.AudioDevice selectedAudioDevice, final int numberOfChoices) {
switch (selectedAudioDevice) {
case EARPIECE -> {
this.binding.inCallActionRight.setImageResource(
@@ -1294,19 +1291,19 @@ public class RtpSessionActivity extends XmppActivity
private void switchToEarpiece(View view) {
requireRtpConnection()
- .getAudioManager()
- .setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
+ .getCallIntegration()
+ .setAudioDevice(CallIntegration.AudioDevice.EARPIECE);
acquireProximityWakeLock();
}
private void switchToSpeaker(View view) {
requireRtpConnection()
- .getAudioManager()
- .setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
+ .getCallIntegration()
+ .setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE);
releaseProximityWakeLock();
}
- private void retry(View view) {
+ private void retry(final View view) {
final Intent intent = getIntent();
final Account account = extractAccount(intent);
final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
@@ -1315,7 +1312,7 @@ public class RtpSessionActivity extends XmppActivity
final Set media = actionToMedia(lastAction == null ? action : lastAction);
this.rtpConnectionReference = null;
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) {
@@ -1411,8 +1408,8 @@ public class RtpSessionActivity extends XmppActivity
@Override
public void onAudioDeviceChanged(
- final AppRTCAudioManager.AudioDevice selectedAudioDevice,
- final Set availableAudioDevices) {
+ final CallIntegration.AudioDevice selectedAudioDevice,
+ final Set availableAudioDevices) {
Log.d(
Config.LOGTAG,
"onAudioDeviceChanged in activity: selected:"
@@ -1428,11 +1425,11 @@ public class RtpSessionActivity extends XmppActivity
"onAudioDeviceChanged() nothing to do because end card has been reached");
} else {
if (Media.audioOnly(media) && endUserState == RtpEndUserState.CONNECTED) {
- final AppRTCAudioManager audioManager =
- requireRtpConnection().getAudioManager();
+ final CallIntegration callIntegration =
+ requireRtpConnection().getCallIntegration();
updateInCallButtonConfigurationSpeaker(
- audioManager.getSelectedAudioDevice(),
- audioManager.getAudioDevices().size());
+ callIntegration.getSelectedAudioDevice(),
+ callIntegration.getAudioDevices().size());
}
Log.d(
Config.LOGTAG,
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
index 23d4f175b..d4c9189eb 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
@@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle;
+import android.os.Bundle;
+import android.telecom.TelecomManager;
import android.util.Base64;
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.Transferable;
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.xml.Element;
import eu.siacs.conversations.xml.Namespace;
@@ -135,6 +139,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return;
}
connections.put(id, connection);
+
+ CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id);
+
mXmppConnectionService.updateConversationUi();
connection.deliverPacket(packet);
} else {
@@ -148,12 +155,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
public boolean isBusy() {
- if (mXmppConnectionService.isPhoneInCall()) {
- return true;
- }
for (AbstractJingleConnection connection : this.connections.values()) {
if (connection instanceof JingleRtpConnection) {
- if (((JingleRtpConnection) connection).isTerminated()) {
+ if (connection.isTerminated()) {
continue;
}
return true;
@@ -181,17 +185,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return false;
}
- public void notifyPhoneCallStarted() {
- for (AbstractJingleConnection connection : connections.values()) {
- if (connection instanceof JingleRtpConnection rtpConnection) {
- if (rtpConnection.isTerminated()) {
- continue;
- }
- rtpConnection.notifyPhoneCall();
- }
- }
- }
-
private Optional findMatchingSessionProposal(
final Account account, final Jid with, final Set media) {
synchronized (this.rtpSessionProposals) {
@@ -390,6 +383,8 @@ public class JingleConnectionManager extends AbstractConnectionManager {
this.connections.put(id, rtpConnection);
rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
+
+ CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id);
// TODO actually do the automatic accept?!
} else {
Log.d(
@@ -439,6 +434,8 @@ public class JingleConnectionManager extends AbstractConnectionManager {
this.connections.put(id, rtpConnection);
rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
+
+ CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id);
}
} else {
Log.d(
@@ -457,7 +454,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
if (proposal != null) {
rtpSessionProposals.remove(proposal);
final JingleRtpConnection rtpConnection =
- new JingleRtpConnection(this, id, account.getJid());
+ new JingleRtpConnection(this, id, account.getJid(), proposal.callIntegration);
rtpConnection.setProposedMedia(proposal.media);
this.connections.put(id, rtpConnection);
rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED);
@@ -490,6 +487,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
getRtpSessionProposal(account, from.asBareJid(), sessionId);
synchronized (rtpSessionProposals) {
if (proposal != null && rtpSessionProposals.remove(proposal) != null) {
+ proposal.callIntegration.busy();
writeLogMissedOutgoing(
account, proposal.with, proposal.sessionId, serverMsgId, timestamp);
toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media);
@@ -628,10 +626,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return Optional.absent();
}
- void finishConnection(final AbstractJingleConnection connection) {
- this.connections.remove(connection.getId());
- }
-
void finishConnectionOrThrow(final AbstractJingleConnection connection) {
final AbstractJingleConnection.Id id = connection.getId();
if (this.connections.remove(id) == null) {
@@ -680,6 +674,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
+ ": retracting rtp session proposal with "
+ rtpSessionProposal.with);
this.rtpSessionProposals.remove(rtpSessionProposal);
+ rtpSessionProposal.callIntegration.retracted();
final MessagePacket messagePacket =
mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal);
writeLogMissedOutgoing(
@@ -691,7 +686,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
mXmppConnectionService.sendMessagePacket(account, messagePacket);
}
- public String initializeRtpSession(
+ public JingleRtpConnection initializeRtpSession(
final Account account, final Jid with, final Set media) {
final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with);
final JingleRtpConnection rtpConnection =
@@ -699,15 +694,15 @@ public class JingleConnectionManager extends AbstractConnectionManager {
rtpConnection.setProposedMedia(media);
this.connections.put(id, rtpConnection);
rtpConnection.sendSessionInitiate();
- return id.sessionId;
+ return rtpConnection;
}
- public void proposeJingleRtpSession(
+ public RtpSessionProposal proposeJingleRtpSession(
final Account account, final Jid with, final Set media) {
synchronized (this.rtpSessionProposals) {
- for (Map.Entry entry :
+ for (final Map.Entry entry :
this.rtpSessionProposals.entrySet()) {
- RtpSessionProposal proposal = entry.getKey();
+ final RtpSessionProposal proposal = entry.getKey();
if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
final DeviceDiscoveryState preexistingState = entry.getValue();
if (preexistingState != null
@@ -716,7 +711,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
toneManager.transition(endUserState, media);
mXmppConnectionService.notifyJingleRtpConnectionUpdate(
account, with, proposal.sessionId, endUserState);
- return;
+ return proposal;
}
}
}
@@ -725,19 +720,23 @@ public class JingleConnectionManager extends AbstractConnectionManager {
Log.d(
Config.LOGTAG,
"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(
"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 =
- RtpSessionProposal.of(account, with.asBareJid(), media);
+ RtpSessionProposal.of(account, with.asBareJid(), media, callIntegration);
this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING);
mXmppConnectionService.notifyJingleRtpConnectionUpdate(
account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE);
final MessagePacket messagePacket =
mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
mXmppConnectionService.sendMessagePacket(account, messagePacket);
+ return proposal;
}
}
@@ -826,6 +825,21 @@ public class JingleConnectionManager extends AbstractConnectionManager {
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) {
synchronized (this.rtpSessionProposals) {
for (final Map.Entry entry :
@@ -865,7 +879,10 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
this.rtpSessionProposals.put(sessionProposal, target);
final RtpEndUserState endUserState = target.toEndUserState();
- toneManager.transition(endUserState, sessionProposal.media);
+ if (endUserState == RtpEndUserState.RINGING) {
+ sessionProposal.callIntegration.setDialing();
+ }
+ //toneManager.transition(endUserState, sessionProposal.media);
mXmppConnectionService.notifyJingleRtpConnectionUpdate(
account, sessionProposal.with, sessionProposal.sessionId, endUserState);
Log.d(
@@ -994,16 +1011,18 @@ public class JingleConnectionManager extends AbstractConnectionManager {
public final String sessionId;
public final Set media;
private final Account account;
+ private final CallIntegration callIntegration;
- private RtpSessionProposal(Account account, Jid with, String sessionId, Set media) {
+ private RtpSessionProposal(Account account, Jid with, String sessionId, Set media, final CallIntegration callIntegration) {
this.account = account;
this.with = with;
this.sessionId = sessionId;
this.media = media;
+ this.callIntegration = callIntegration;
}
- public static RtpSessionProposal of(Account account, Jid with, Set media) {
- return new RtpSessionProposal(account, with, nextRandomId(), media);
+ public static RtpSessionProposal of(Account account, Jid with, Set media, final CallIntegration callIntegration) {
+ return new RtpSessionProposal(account, with, nextRandomId(), media,callIntegration);
}
@Override
@@ -1035,5 +1054,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
public String getSessionId() {
return sessionId;
}
+
+ public CallIntegration getCallIntegration() {
+ return this.callIntegration;
+ }
}
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java
index 80d7d2118..8d5aa8dfd 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java
@@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle;
+import android.telecom.Call;
+import android.telecom.TelecomManager;
import android.util.Log;
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.Throwables;
import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
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.Futures;
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.RtpSessionStatus;
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.Namespace;
import eu.siacs.conversations.xmpp.Jid;
@@ -67,7 +67,7 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class JingleRtpConnection extends AbstractJingleConnection
- implements WebRTCWrapper.EventCallback {
+ implements WebRTCWrapper.EventCallback, CallIntegration.Callback {
public static final List STATES_SHOWING_ONGOING_CALL =
Arrays.asList(
@@ -78,6 +78,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private final Queue>>
pendingIceCandidates = new LinkedList<>();
private final OmemoVerification omemoVerification = new OmemoVerification();
+ private final CallIntegration callIntegration;
private final Message message;
private Set proposedMedia;
@@ -90,7 +91,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
private final Queue stateHistory = new LinkedList<>();
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);
final Conversation conversation =
jingleConnectionManager
@@ -102,6 +109,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED,
Message.TYPE_RTP_SESSION,
id.sessionId);
+ this.callIntegration = callIntegration;
+ this.callIntegration.setCallback(this);
}
@Override
@@ -1158,6 +1167,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
target = State.SESSION_INITIALIZED_PRE_APPROVED;
} else {
target = State.SESSION_INITIALIZED;
+ setProposedMedia(contentMap.getMedia());
}
if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
respondOk(jinglePacket);
@@ -1628,7 +1638,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
+ from
+ " for "
+ media);
- this.proposedMedia = Sets.newHashSet(media);
+ this.setProposedMedia(Sets.newHashSet(media));
})) {
if (serverMsgId != null) {
this.message.setServerMsgId(serverMsgId);
@@ -1648,6 +1658,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void startRinging() {
+ this.callIntegration.setRinging();
Log.d(
Config.LOGTAG,
id.account.getJid().asBareJid()
@@ -1657,6 +1668,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
ringingTimeoutFuture =
jingleConnectionManager.schedule(
this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS);
+ if (CallIntegration.selfManaged()) {
+ return;
+ }
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() {
final RtpContentMap in = this.incomingContentAdd;
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() {
if (isTerminated()) {
Log.w(
@@ -2537,8 +2592,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void modifyLocalContentMap(final RtpContentMap rtpContentMap) {
final RtpContentMap activeContents = rtpContentMap.activeContents();
setLocalContentMap(activeContents);
- this.webRTCWrapper.switchSpeakerPhonePreference(
- AppRTCAudioManager.SpeakerPhonePreference.of(activeContents.getMedia()));
+ // TODO change audio device on callIntegration was (`switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference.of(activeContents.getMedia())`)
updateEndUserState();
}
@@ -2571,8 +2625,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS);
}
- public AppRTCAudioManager getAudioManager() {
- return webRTCWrapper.getAudioManager();
+
+ public CallIntegration getCallIntegration() {
+ return this.callIntegration;
}
public boolean isMicrophoneEnabled() {
@@ -2603,10 +2658,26 @@ public class JingleRtpConnection extends AbstractJingleConnection
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
public void onAudioDeviceChanged(
- AppRTCAudioManager.AudioDevice selectedAudioDevice,
- Set availableAudioDevices) {
+ final CallIntegration.AudioDevice selectedAudioDevice,
+ final Set availableAudioDevices) {
+ Log.d(Config.LOGTAG,"onAudioDeviceChanged("+selectedAudioDevice+","+availableAudioDevices+")");
xmppConnectionService.notifyJingleRtpConnectionUpdate(
selectedAudioDevice, availableAudioDevices);
}
@@ -2614,6 +2685,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void updateEndUserState() {
final RtpEndUserState endUserState = getEndUserState();
jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia());
+ this.updateCallIntegrationState();
xmppConnectionService.notifyJingleRtpConnectionUpdate(
id.account, id.with, id.sessionId, endUserState);
}
@@ -2670,6 +2742,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
protected void finish() {
if (isTerminated()) {
this.cancelRingingTimeout();
+ this.callIntegration.verifyDisconnected();
this.webRTCWrapper.verifyClosed();
this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
super.finish();
@@ -2724,6 +2797,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
void setProposedMedia(final Set media) {
this.proposedMedia = media;
+ this.callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media));
}
public void fireStateUpdate() {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java
index 24ed790dd..885820460 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java
@@ -9,7 +9,7 @@ public enum RtpEndUserState {
FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet
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
- 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
DECLINED_OR_BUSY, //other party declined; no retry button
CONNECTIVITY_ERROR, //network error; retry button
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java
index da5b9ab2b..fb82b7219 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java
@@ -89,7 +89,8 @@ class ToneManager {
}
switch (state) {
case RINGING:
- scheduleWaitingTone();
+ // ringing can be removed as this is now handled by 'CallIntegration'
+ //scheduleWaitingTone();
break;
case CONNECTED:
scheduleConnected();
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java
index cb0c8579d..fa504ed19 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java
@@ -16,6 +16,7 @@ import com.google.common.util.concurrent.SettableFuture;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.services.AppRTCAudioManager;
+import eu.siacs.conversations.services.CallIntegration;
import eu.siacs.conversations.services.XmppConnectionService;
import org.webrtc.AudioSource;
@@ -83,16 +84,6 @@ public class WebRTCWrapper {
private final EventCallback eventCallback;
private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false);
private final Queue iceCandidates = new LinkedList<>();
- private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents =
- new AppRTCAudioManager.AudioManagerEvents() {
- @Override
- public void onAudioDeviceChanged(
- AppRTCAudioManager.AudioDevice selectedAudioDevice,
- Set availableAudioDevices) {
- eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
- }
- };
- private final Handler mainHandler = new Handler(Looper.getMainLooper());
private TrackWrapper localAudioTrack = null;
private TrackWrapper localVideoTrack = null;
private VideoTrack remoteVideoTrack = null;
@@ -214,7 +205,6 @@ public class WebRTCWrapper {
};
@Nullable private PeerConnectionFactory peerConnectionFactory = null;
@Nullable private PeerConnection peerConnection = null;
- private AppRTCAudioManager appRTCAudioManager = null;
private ToneManager toneManager = null;
private Context context = null;
private EglBase eglBase = null;
@@ -251,15 +241,6 @@ public class WebRTCWrapper {
}
this.context = service;
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(
@@ -462,16 +443,11 @@ public class WebRTCWrapper {
final PeerConnection peerConnection = this.peerConnection;
final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
- final AppRTCAudioManager audioManager = this.appRTCAudioManager;
final EglBase eglBase = this.eglBase;
if (peerConnection != null) {
this.peerConnection = null;
dispose(peerConnection);
}
- if (audioManager != null) {
- toneManager.setAppRtcAudioManagerHasControl(false);
- mainHandler.post(audioManager::stop);
- }
this.localVideoTrack = null;
this.remoteVideoTrack = null;
if (videoSourceWrapper != null) {
@@ -498,8 +474,8 @@ public class WebRTCWrapper {
|| this.eglBase != null
|| this.localVideoTrack != null
|| this.remoteVideoTrack != null) {
- final IllegalStateException e =
- new IllegalStateException("WebRTCWrapper hasn't been closed properly");
+ final AssertionError e =
+ new AssertionError("WebRTCWrapper hasn't been closed properly");
Log.e(Config.LOGTAG, "verifyClosed() failed. Going to throw", e);
throw e;
}
@@ -750,27 +726,15 @@ public class WebRTCWrapper {
return context;
}
- AppRTCAudioManager getAudioManager() {
- return appRTCAudioManager;
- }
-
void execute(final Runnable command) {
this.executorService.execute(command);
}
- public void switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference preference) {
- mainHandler.post(() -> appRTCAudioManager.switchSpeakerPhonePreference(preference));
- }
-
public interface EventCallback {
void onIceCandidate(IceCandidate iceCandidate);
void onConnectionChange(PeerConnection.PeerConnectionState newState);
- void onAudioDeviceChanged(
- AppRTCAudioManager.AudioDevice selectedAudioDevice,
- Set availableAudioDevices);
-
void onRenegotiationNeeded();
}