mirror of
https://codeberg.org/monocles/monocles_chat.git
synced 2025-01-29 00:14:12 +01:00
use call integration via MANAGE_OWN_CALLS
This commit is contained in:
parent
3fb80c0d8f
commit
34eabe7377
14 changed files with 981 additions and 298 deletions
|
@ -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": [
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue