use call integration via MANAGE_OWN_CALLS

This commit is contained in:
Daniel Gultsch 2024-01-14 10:58:00 +01:00 committed by Arne
parent 3fb80c0d8f
commit 34eabe7377
14 changed files with 981 additions and 298 deletions

View file

@ -15,19 +15,6 @@
"versionName": "1.7.10",
"outputFile": "monocles chat-1.7.10-git-universal-release.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "x86_64"
}
],
"attributes": [],
"versionCode": 17303,
"versionName": "1.7.10",
"outputFile": "monocles chat-1.7.10-git-x86_64-release.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
@ -41,6 +28,19 @@
"versionName": "1.7.10",
"outputFile": "monocles chat-1.7.10-git-x86-release.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "x86_64"
}
],
"attributes": [],
"versionCode": 17303,
"versionName": "1.7.10",
"outputFile": "monocles chat-1.7.10-git-x86_64-release.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [

View file

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

View file

@ -45,9 +45,9 @@ import io.michaelrocks.libphonenumber.android.NumberParseException;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.EventReceiver;
import eu.siacs.conversations.services.CallIntegration;
import eu.siacs.conversations.services.CallIntegrationConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.RtpSessionActivity;
@ -56,16 +56,17 @@ import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE;
@RequiresApi(Build.VERSION_CODES.M)
public class ConnectionService extends android.telecom.ConnectionService {
public XmppConnectionService xmppConnectionService = null;
protected ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
XmppConnectionBinder binder = (XmppConnectionBinder) service;
xmppConnectionService = binder.getService();
}
public void onServiceConnected(ComponentName className, IBinder service) {
XmppConnectionBinder binder = (XmppConnectionBinder) service;
xmppConnectionService = binder.getService();
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
@ -78,11 +79,11 @@ public class ConnectionService extends android.telecom.ConnectionService {
// From XmppActivity.connectToBackend
Intent intent = new Intent(this, XmppConnectionService.class);
intent.setAction(XmppConnectionService.ACTION_STARTING_CALL);
intent.putExtra(EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, true);
intent.putExtra(EXTRA_NEEDS_FOREGROUND_SERVICE, true);
try {
startService(intent);
} catch (IllegalStateException e) {
Log.w("de.monocles.chat.ConnectionService", "unable to start service from " + getClass().getSimpleName());
Log.w("com.cheogram.android.ConnectionService", "unable to start service from " + getClass().getSimpleName());
}
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@ -94,8 +95,8 @@ public class ConnectionService extends android.telecom.ConnectionService {
@Override
public Connection onCreateOutgoingConnection(
PhoneAccountHandle phoneAccountHandle,
ConnectionRequest request
PhoneAccountHandle phoneAccountHandle,
ConnectionRequest request
) {
String[] gateway = phoneAccountHandle.getId().split("/", 2);
@ -110,13 +111,13 @@ public class ConnectionService extends android.telecom.ConnectionService {
tel = PhoneNumberUtilWrapper.normalize(this, tel, true);
} catch (IllegalArgumentException | NumberParseException e) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR)
new DisconnectCause(DisconnectCause.ERROR)
);
}
if (xmppConnectionService == null) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR)
new DisconnectCause(DisconnectCause.ERROR)
);
}
@ -129,18 +130,18 @@ public class ConnectionService extends android.telecom.ConnectionService {
Account account = xmppConnectionService.findAccountByJid(Jid.of(gateway[0]));
if (account == null) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR)
new DisconnectCause(DisconnectCause.ERROR)
);
}
Jid with = Jid.ofLocalAndDomain(tel, gateway[1]);
monoclesConnection connection = new monoclesConnection(account, with, postDial);
CheogramConnection connection = new CheogramConnection(account, with, postDial);
PermissionManager permissionManager = PermissionManager.getInstance(this);
permissionManager.setNotificationSettings(
new NotificationSettings.Builder()
.withMessage(R.string.microphone_permission_for_call)
.withSmallIcon(R.drawable.ic_notification).build()
new NotificationSettings.Builder()
.withMessage(R.string.microphone_permission_for_call)
.withSmallIcon(R.drawable.ic_notification).build()
);
Set<String> permissions = new HashSet<>();
@ -165,12 +166,12 @@ public class ConnectionService extends android.telecom.ConnectionService {
connection.setInitializing();
connection.setAddress(
Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI
TelecomManager.PRESENTATION_ALLOWED
Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI
TelecomManager.PRESENTATION_ALLOWED
);
xmppConnectionService.setOnRtpConnectionUpdateListener(
(XmppConnectionService.OnJingleRtpConnectionUpdate) connection
(XmppConnectionService.OnJingleRtpConnectionUpdate) connection
);
xmppConnectionService.setDiallerIntegrationActive(true);
@ -179,29 +180,30 @@ public class ConnectionService extends android.telecom.ConnectionService {
@Override
public Connection onCreateIncomingConnection(PhoneAccountHandle handle, ConnectionRequest request) {
Bundle extras = request.getExtras();
String accountJid = extras.getString("account");
String withJid = extras.getString("with");
String sessionId = extras.getString("sessionId");
final var extras = request.getExtras();
final var extraExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
final var accountJid = extraExtras == null ? null : extraExtras.getString("account");
final var withJid = extraExtras == null ? null : extraExtras.getString("with");
final String sessionId = extraExtras == null ? null : extraExtras.getString(CallIntegrationConnectionService.EXTRA_SESSION_ID);
if (xmppConnectionService == null) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR)
new DisconnectCause(DisconnectCause.ERROR)
);
}
Account account = xmppConnectionService.findAccountByJid(Jid.of(accountJid));
Jid with = Jid.of(withJid);
monoclesConnection connection = new monoclesConnection(account, with, null);
CheogramConnection connection = new CheogramConnection(account, with, null);
connection.setSessionId(sessionId);
connection.setAddress(
Uri.fromParts("tel", with.getLocal(), null),
TelecomManager.PRESENTATION_ALLOWED
Uri.fromParts("tel", with.getLocal(), null),
TelecomManager.PRESENTATION_ALLOWED
);
connection.setCallerDisplayName(
account.getRoster().getContact(with).getDisplayName(),
TelecomManager.PRESENTATION_ALLOWED
account.getRoster().getContact(with).getDisplayName(),
TelecomManager.PRESENTATION_ALLOWED
);
connection.setRinging();
@ -210,7 +212,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
return connection;
}
public class monoclesConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate {
public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate {
protected Account account;
protected Jid with;
protected String sessionId = null;
@ -219,15 +221,15 @@ public class ConnectionService extends android.telecom.ConnectionService {
protected CallAudioState pendingState = null;
protected WeakReference<JingleRtpConnection> rtpConnection = null;
monoclesConnection(Account account, Jid with, String postDialString) {
CheogramConnection(Account account, Jid with, String postDialString) {
super();
this.account = account;
this.with = with;
gatewayIcon = Icon.createWithBitmap(FileBackend.drawDrawable(xmppConnectionService.getAvatarService().get(
account.getRoster().getContact(Jid.of(with.getDomain())),
AvatarService.getSystemUiAvatarSize(xmppConnectionService),
false
account.getRoster().getContact(Jid.of(with.getDomain())),
AvatarService.getSystemUiAvatarSize(xmppConnectionService),
false
)));
if (postDialString != null) {
@ -237,14 +239,15 @@ public class ConnectionService extends android.telecom.ConnectionService {
}
setCallerDisplayName(
account.getDisplayName(),
TelecomManager.PRESENTATION_ALLOWED
account.getDisplayName(),
TelecomManager.PRESENTATION_ALLOWED
);
setAudioModeIsVoip(true);
setConnectionCapabilities(
Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION |
Connection.CAPABILITY_MUTE
Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION |
Connection.CAPABILITY_MUTE
);
setRingbackRequested(true);
}
public void setSessionId(final String sessionId) {
@ -253,7 +256,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
@Override
public void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) {
Log.d("de.monocles.chat.monoclesConnection", "onJingleRtpConnectionUpdate: " + with + " " + sessionId + " (== " + this.sessionId + " )? " + state);
Log.d("com.cheogram.android.CheogramConnection", "onJingleRtpConnectionUpdate: " + with + " " + sessionId + " (== " + this.sessionId + " )? " + state);
if (sessionId == null || !sessionId.equals(this.sessionId)) return;
if (rtpConnection == null) {
this.with = with; // Store full JID of connection
@ -290,16 +293,16 @@ public class ConnectionService extends android.telecom.ConnectionService {
}
@Override
public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
public void onAudioDeviceChanged(CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices) {
if (Build.VERSION.SDK_INT < 26) return;
if (pendingState != null) {
Log.d("de.monocles.chat.monoclesConnection", "Try with pendingState: " + pendingState);
Log.d("com.cheogram.android.CheogramConnection", "Try with pendingState: " + pendingState);
onCallAudioStateChanged(pendingState);
return;
}
Log.d("de.monocles.chat.monoclesConnection", "onAudioDeviceChanged: " + selectedAudioDevice);
Log.d("com.cheogram.android.CheogramConnection", "onAudioDeviceChanged: " + selectedAudioDevice);
switch(selectedAudioDevice) {
case SPEAKER_PHONE:
@ -322,35 +325,19 @@ public class ConnectionService extends android.telecom.ConnectionService {
@Override
public void onCallAudioStateChanged(CallAudioState state) {
pendingState = null;
if (rtpConnection == null || rtpConnection.get() == null || rtpConnection.get().getAudioManager() == null) {
if (rtpConnection == null || rtpConnection.get() == null) {
pendingState = state;
return;
}
Log.d("de.monocles.chat.monoclesConnection", "onCallAudioStateChanged: " + state);
switch(state.getRoute()) {
case CallAudioState.ROUTE_SPEAKER:
rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
break;
case CallAudioState.ROUTE_WIRED_HEADSET:
rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.WIRED_HEADSET);
break;
case CallAudioState.ROUTE_EARPIECE:
rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
break;
case CallAudioState.ROUTE_BLUETOOTH:
rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.BLUETOOTH);
break;
default:
rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.NONE);
}
Log.d("com.cheogram.android.CheogramConnection", "onCallAudioStateChanged: " + state);
rtpConnection.get().callIntegration.onCallAudioStateChanged(state);
try {
rtpConnection.get().setMicrophoneEnabled(!state.isMuted());
} catch (final IllegalStateException e) {
pendingState = state;
Log.w("de.monocles.chat.monoclesConnection", "Could not set microphone mute to " + (state.isMuted() ? "true" : "false") + ": " + e.toString());
Log.w("com.cheogram.android.CheogramConnection", "Could not set microphone mute to " + (state.isMuted() ? "true" : "false") + ": " + e.toString());
}
}
@ -373,7 +360,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
try {
rtpConnection.get().rejectCall();
} catch (final IllegalStateException e) {
Log.w("de.monocles.chat.monoclesConnection", e.toString());
Log.w("com.cheogram.android.CheogramConnection", e.toString());
}
}
close(new DisconnectCause(DisconnectCause.LOCAL));

View file

@ -21,7 +21,7 @@ import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.Log;
import eu.siacs.conversations.xmpp.jingle.Media;
import androidx.annotation.Nullable;
import org.webrtc.ThreadUtils;
@ -33,6 +33,7 @@ import java.util.concurrent.CountDownLatch;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.AppRTCUtils;
import eu.siacs.conversations.xmpp.jingle.Media;
/**
* AppRTCAudioManager manages all audio related parts of the AppRTC demo.
@ -48,7 +49,7 @@ public class AppRTCAudioManager {
// Handles all tasks related to Bluetooth headset devices.
private final AppRTCBluetoothManager bluetoothManager;
@Nullable
private AudioManager audioManager;
private final AudioManager audioManager;
@Nullable
private AudioManagerEvents audioManagerEvents;
private AudioManagerState amState;
@ -57,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
@ -77,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<AudioDevice> audioDevices = new HashSet<>();
private Set<CallIntegration.AudioDevice> audioDevices = new HashSet<>();
// Broadcast receiver for wired headset intent broadcasts.
private BroadcastReceiver wiredHeadsetReceiver;
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.
@ -109,24 +109,17 @@ public class AppRTCAudioManager {
Log.d(Config.LOGTAG, "defaultAudioDevice: " + defaultAudioDevice);
AppRTCUtils.logDeviceInfo(Config.LOGTAG);
}
public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) {
this.speakerPhonePreference = speakerPhonePreference;
if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) {
defaultAudioDevice = 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;
@ -173,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);
}
}
}
@ -257,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.
@ -299,11 +292,7 @@ public class AppRTCAudioManager {
// Restore previously stored audio states.
setSpeakerphoneOn(savedIsSpeakerPhoneOn);
setMicrophoneMute(savedIsMicrophoneMute);
try {
audioManager.setMode(AudioManager.MODE_NORMAL);
} catch (final SecurityException e) {
Log.e(Config.LOGTAG, "Could not set mode on audio manager: " + audioManager);
}
audioManager.setMode(AudioManager.MODE_NORMAL);
// Abandon audio focus. Gives the previous focus owner, if any, focus.
audioManager.abandonAudioFocus(audioFocusChangeListener);
audioFocusChangeListener = null;
@ -318,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) {
@ -341,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:
@ -351,7 +340,7 @@ public class AppRTCAudioManager {
if (hasEarpiece()) {
defaultAudioDevice = defaultDevice;
} else {
defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
}
break;
default:
@ -365,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);
@ -377,7 +366,7 @@ public class AppRTCAudioManager {
/**
* Returns current set of available/selectable audio devices.
*/
public Set<AudioDevice> getAudioDevices() {
public Set<CallIntegration.AudioDevice> getAudioDevices() {
ThreadUtils.checkIsOnMainThread();
return Collections.unmodifiableSet(new HashSet<>(audioDevices));
}
@ -385,7 +374,7 @@ public class AppRTCAudioManager {
/**
* Returns the currently selected audio device.
*/
public AudioDevice getSelectedAudioDevice() {
public CallIntegration.AudioDevice getSelectedAudioDevice() {
ThreadUtils.checkIsOnMainThread();
return selectedAudioDevice;
}
@ -482,21 +471,21 @@ public class AppRTCAudioManager {
bluetoothManager.updateDevice();
}
// Update the set of available audio devices.
Set<AudioDevice> newAudioDevices = new HashSet<>();
Set<CallIntegration.AudioDevice> newAudioDevices = new HashSet<>();
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|| 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.
@ -505,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) {
@ -548,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).
@ -585,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.
*/
@ -618,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<AudioDevice> availableAudioDevices);
CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices);
}
/* Receiver which handles changes in wired headset availability. */

View file

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

View file

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

View file

@ -267,6 +267,7 @@ public class XmppConnectionService extends Service {
public static final String ACTION_END_CALL = "end_call";
public static final String ACTION_STARTING_CALL = "starting_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";
@ -392,16 +393,7 @@ public class XmppConnectionService extends Service {
};
private final AtomicBoolean isPhoneInCall = new AtomicBoolean(false);
private final AtomicBoolean diallerIntegrationActive = new AtomicBoolean(false);
private final PhoneStateListener phoneStateListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(final int state, final String phoneNumber) {
if (diallerIntegrationActive.get()) return;
isPhoneInCall.set(state != TelephonyManager.CALL_STATE_IDLE);
if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
mJingleConnectionManager.notifyPhoneCallStarted();
}
}
};
private LruCache<String, Drawable> mDrawableCache;
public void setDiallerIntegrationActive(boolean active) {
@ -1754,6 +1746,9 @@ public class XmppConnectionService extends Service {
editor.apply();
toggleSetProfilePictureActivity(hasEnabledAccounts);
reconfigurePushDistributor();
CallIntegrationConnectionService.registerPhoneAccounts(this, this.accounts);
restoreFromDatabase();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
@ -1813,7 +1808,6 @@ public class XmppConnectionService extends Service {
ContextCompat.RECEIVER_EXPORTED);
mForceDuringOnCreate.set(false);
toggleForegroundService();
setupPhoneStateListener();
internalPingExecutor.scheduleAtFixedRate(this::manageAccountConnectionStatesInternal,120,120,TimeUnit.SECONDS);
//start export log service every day at given time
ScheduleAutomaticExport();
@ -1837,19 +1831,7 @@ public class XmppConnectionService extends Service {
}
}).start();
}
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");
@ -5571,7 +5553,7 @@ public class XmppConnectionService extends Service {
}
}
public void notifyJingleRtpConnectionUpdate(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
public void notifyJingleRtpConnectionUpdate(CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices) {
for (OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) {
listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
}
@ -6601,7 +6583,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<AppRTCAudioManager.AudioDevice> availableAudioDevices);
void onAudioDeviceChanged(CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices);
}
public interface OnAccountUpdate {

View file

@ -174,6 +174,7 @@ import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.http.HttpDownloadConnection;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AttachFileToConversationRunnable;
import eu.siacs.conversations.services.CallIntegrationConnectionService;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.adapter.CommandAdapter;
@ -2808,13 +2809,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) {

View file

@ -60,6 +60,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;
@ -140,7 +142,7 @@ public class RtpSessionActivity extends XmppActivity
}
};
private static Set<Media> actionToMedia(final String action) {
public static Set<Media> actionToMedia(final String action) {
if (ACTION_MAKE_VIDEO_CALL.equals(action)) {
return ImmutableSet.of(Media.AUDIO, Media.VIDEO);
} else {
@ -461,11 +463,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();
}
}
@ -511,8 +513,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();
@ -618,7 +620,7 @@ public class RtpSessionActivity extends XmppActivity
}
}
private void proposeJingleRtpSession(
public void proposeJingleRtpSession(
final Account account, final Jid with, final Set<Media> media) {
checkMicrophoneAvailabilityAsync();
if (with.isBareJid()) {
@ -626,12 +628,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);
}
@ -664,7 +661,7 @@ public class RtpSessionActivity extends XmppActivity
} else {
throw new IllegalStateException("Invalid permission result request");
}
Toast.makeText(this, getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT)
Toast.makeText(this, R.string.app_name, Toast.LENGTH_SHORT)
.show();
}
}
@ -1104,10 +1101,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)) {
@ -1125,7 +1122,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(
@ -1366,19 +1363,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));
@ -1387,7 +1384,7 @@ public class RtpSessionActivity extends XmppActivity
final Set<Media> 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) {
@ -1483,8 +1480,8 @@ public class RtpSessionActivity extends XmppActivity
@Override
public void onAudioDeviceChanged(
final AppRTCAudioManager.AudioDevice selectedAudioDevice,
final Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
final CallIntegration.AudioDevice selectedAudioDevice,
final Set<CallIntegration.AudioDevice> availableAudioDevices) {
Log.d(
Config.LOGTAG,
"onAudioDeviceChanged in activity: selected:"
@ -1500,11 +1497,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,

View file

@ -1,10 +1,10 @@
package eu.siacs.conversations.xmpp.jingle;
import android.os.Bundle;
import android.telecom.TelecomManager;
import android.util.Base64;
import android.util.Log;
import androidx.annotation.Nullable;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
@ -23,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;
@ -137,6 +139,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return;
}
connections.put(id, connection);
CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id);
mXmppConnectionService.updateConversationUi();
connection.deliverPacket(packet);
} else {
@ -150,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;
@ -183,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<RtpSessionProposal> findMatchingSessionProposal(
final Account account, final Jid with, final Set<Media> media) {
synchronized (this.rtpSessionProposals) {
@ -392,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(
@ -441,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(
@ -459,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);
@ -492,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);
@ -630,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) {
@ -682,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(
@ -693,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> media) {
final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with);
final JingleRtpConnection rtpConnection =
@ -701,15 +694,15 @@ public class JingleConnectionManager extends AbstractConnectionManager {
rtpConnection.setProposedMedia(media);
this.connections.put(id, rtpConnection);
rtpConnection.sendSessionInitiate();
return id.sessionId;
return rtpConnection;
}
public @Nullable RtpSessionProposal proposeJingleRtpSession(
public RtpSessionProposal proposeJingleRtpSession(
final Account account, final Jid with, final Set<Media> media) {
synchronized (this.rtpSessionProposals) {
for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
for (final Map.Entry<RtpSessionProposal, DeviceDiscoveryState> 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
@ -727,13 +720,16 @@ public class JingleConnectionManager extends AbstractConnectionManager {
Log.d(
Config.LOGTAG,
"ignoring request to propose jingle session because the other party already created one for us");
// 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);
@ -829,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<RtpSessionProposal, DeviceDiscoveryState> entry :
@ -868,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(
@ -997,16 +1011,18 @@ public class JingleConnectionManager extends AbstractConnectionManager {
public final String sessionId;
public final Set<Media> media;
private final Account account;
private final CallIntegration callIntegration;
private RtpSessionProposal(Account account, Jid with, String sessionId, Set<Media> media) {
private RtpSessionProposal(Account account, Jid with, String sessionId, Set<Media> media, final CallIntegration callIntegration) {
this.account = account;
this.with = with;
this.sessionId = sessionId;
this.media = media;
this.callIntegration = callIntegration;
}
public static RtpSessionProposal of(Account account, Jid with, Set<Media> media) {
return new RtpSessionProposal(account, with, nextRandomId(), media);
public static RtpSessionProposal of(Account account, Jid with, Set<Media> media, final CallIntegration callIntegration) {
return new RtpSessionProposal(account, with, nextRandomId(), media,callIntegration);
}
@Override
@ -1038,5 +1054,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
public String getSessionId() {
return sessionId;
}
public CallIntegration getCallIntegration() {
return this.callIntegration;
}
}
}

View file

@ -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<State> STATES_SHOWING_ONGOING_CALL =
Arrays.asList(
@ -78,6 +78,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private final Queue<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>>
pendingIceCandidates = new LinkedList<>();
private final OmemoVerification omemoVerification = new OmemoVerification();
public final CallIntegration callIntegration;
private final Message message;
private Set<Media> proposedMedia;
@ -90,7 +91,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
private final Queue<PeerConnection.PeerConnectionState> 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
@ -1162,6 +1171,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);
@ -1632,7 +1642,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);
@ -1652,6 +1662,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void startRinging() {
this.callIntegration.setRinging();
Log.d(
Config.LOGTAG,
id.account.getJid().asBareJid()
@ -1661,6 +1672,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());
}
@ -2058,6 +2072,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;
@ -2139,15 +2203,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(
@ -2541,8 +2596,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();
}
@ -2575,8 +2629,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() {
@ -2607,10 +2662,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<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
final CallIntegration.AudioDevice selectedAudioDevice,
final Set<CallIntegration.AudioDevice> availableAudioDevices) {
Log.d(Config.LOGTAG,"onAudioDeviceChanged("+selectedAudioDevice+","+availableAudioDevices+")");
xmppConnectionService.notifyJingleRtpConnectionUpdate(
selectedAudioDevice, availableAudioDevices);
}
@ -2618,6 +2689,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);
}
@ -2674,6 +2746,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();
@ -2728,6 +2801,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
void setProposedMedia(final Set<Media> media) {
this.proposedMedia = media;
this.callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media));
}
public void fireStateUpdate() {

View file

@ -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

View file

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

View file

@ -18,6 +18,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;
@ -107,16 +108,6 @@ public class WebRTCWrapper {
private final EventCallback eventCallback;
private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false);
private final Queue<IceCandidate> iceCandidates = new LinkedList<>();
private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents =
new AppRTCAudioManager.AudioManagerEvents() {
@Override
public void onAudioDeviceChanged(
AppRTCAudioManager.AudioDevice selectedAudioDevice,
Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
}
};
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private TrackWrapper<AudioTrack> localAudioTrack = null;
private TrackWrapper<VideoTrack> localVideoTrack = null;
private VideoTrack remoteVideoTrack = null;
@ -238,7 +229,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;
@ -275,15 +265,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(
@ -486,16 +467,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) {
@ -522,8 +498,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;
}
@ -799,27 +775,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<AppRTCAudioManager.AudioDevice> availableAudioDevices);
void onRenegotiationNeeded();
}