From 19c634f3d2d089b4ed5eefcfda36cc0dd4d11c93 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 14 Jan 2024 10:58:00 +0100 Subject: [PATCH] use call integration via MANAGE_OWN_CALLS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- build.gradle | 2 +- src/main/AndroidManifest.xml | 13 +- .../services/AppRTCAudioManager.java | 100 ++--- .../services/CallIntegration.java | 408 ++++++++++++++++++ .../CallIntegrationConnectionService.java | 255 +++++++++++ .../services/XmppConnectionService.java | 30 +- .../ui/ConversationFragment.java | 6 +- .../conversations/ui/RtpSessionActivity.java | 57 ++- .../xmpp/jingle/JingleConnectionManager.java | 87 ++-- .../xmpp/jingle/JingleRtpConnection.java | 116 ++++- .../xmpp/jingle/RtpEndUserState.java | 2 +- .../xmpp/jingle/ToneManager.java | 3 +- .../xmpp/jingle/WebRTCWrapper.java | 42 +- 13 files changed, 909 insertions(+), 212 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/services/CallIntegration.java create mode 100644 src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java 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(); }