run RtpSessionService during phone calls

This commit is contained in:
Daniel Gultsch 2023-02-24 15:52:23 +01:00
parent 1be1334794
commit 49b4f54285
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
14 changed files with 289 additions and 59 deletions

View file

@ -81,6 +81,8 @@
android:name=".service.ForegroundService"
android:exported="false" />
<service android:name=".service.RtpSessionService" android:exported="false" android:foregroundServiceType="phoneCall"/>
<receiver
android:name=".receiver.EventReceiver"
android:exported="true">

View file

@ -1,6 +1,7 @@
package eu.siacs.conversations.xmpp.jingle;
import android.content.Context;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
@ -84,6 +85,14 @@ public abstract class AbstractJingleConnection extends XmppConnection.Delegate {
public int hashCode() {
return Objects.hashCode(with, sessionId);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("with", with)
.add("sessionId", sessionId)
.toString();
}
}
public enum State {

View file

@ -30,7 +30,9 @@ import im.conversations.android.BuildConfig;
import im.conversations.android.axolotl.AxolotlEncryptionException;
import im.conversations.android.axolotl.AxolotlService;
import im.conversations.android.dns.IP;
import im.conversations.android.notification.OngoingCall;
import im.conversations.android.notification.RtpSessionNotification;
import im.conversations.android.service.RtpSessionService;
import im.conversations.android.transformer.CallLogTransformation;
import im.conversations.android.util.BooleanFutures;
import im.conversations.android.xml.Element;
@ -592,7 +594,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
this.incomingContentAdd = null;
updateEndUserState();
} else {
LOGGER.info("content add summary {} did not match remove summary {}", contentAddSummary, removeSummary);
LOGGER.info(
"content add summary {} did not match remove summary {}",
contentAddSummary,
removeSummary);
webRTCWrapper.close();
sendSessionTerminate(
Reason.FAILED_APPLICATION,
@ -2424,6 +2429,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
this.webRTCWrapper.switchSpeakerPhonePreference(
AppRTCAudioManager.SpeakerPhonePreference.of(activeContents.getMedia()));
updateEndUserState();
updateOngoingCallNotification();
}
private SessionDescription setLocalSessionDescription()
@ -2505,21 +2511,26 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void updateOngoingCallNotification() {
final State state = this.state;
if (STATES_SHOWING_ONGOING_CALL.contains(state)) {
RtpSessionService.updateOngoingCall(context, getAccount().id, id);
} else {
RtpSessionService.stop(context);
}
}
public OngoingCall getOngoingCall() {
final State state = this.state;
final boolean reconnecting;
if (STATES_SHOWING_ONGOING_CALL.contains(state)) {
if (state == State.SESSION_ACCEPTED) {
reconnecting =
getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING;
} else {
reconnecting = false;
}
// TODO decide what we want to do with ongoing call? create a foreground service of
// RtpSessionService?
// xmppConnectionService.setOngoingCall(id, getMedia(), reconnecting);
} else {
// xmppConnectionService.removeOngoingCall();
reconnecting = false;
}
return new OngoingCall(id, getMedia(), reconnecting);
}
private void discoverIceServers(final OnIceServersDiscovered onIceServersDiscovered) {

View file

@ -38,7 +38,9 @@ class TrackWrapper<T extends MediaStreamTrack> {
final RtpTransceiver transceiver =
peerConnection == null ? null : getTransceiver(peerConnection, trackWrapper);
if (transceiver == null) {
Log.w(Config.LOGTAG, "unable to detect transceiver for " + trackWrapper.getRtpSenderId());
Log.w(
Config.LOGTAG,
"unable to detect transceiver for " + trackWrapper.getRtpSenderId());
return Optional.of(trackWrapper.track);
}
final RtpTransceiver.RtpTransceiverDirection direction = transceiver.getDirection();
@ -62,6 +64,9 @@ class TrackWrapper<T extends MediaStreamTrack> {
public static <T extends MediaStreamTrack> RtpTransceiver getTransceiver(
@Nonnull final PeerConnection peerConnection, final TrackWrapper<T> trackWrapper) {
final String rtpSenderId = trackWrapper.getRtpSenderId();
if (rtpSenderId == null) {
return null;
}
for (final RtpTransceiver transceiver : peerConnection.getTransceivers()) {
if (transceiver.getSender().id().equals(rtpSenderId)) {
return transceiver;

View file

@ -12,7 +12,8 @@ import im.conversations.android.R;
public final class Channels {
static final String CHANNEL_FOREGROUND = "foreground";
static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel";
static final String CHANNEL_INCOMING_CALL = "incoming_calls_channel";
static final String CHANNEL_ONGOING_CALL = "ongoing_call";
static final String CHANNEL_GROUP_STATUS = "status";
static final String CHANNEL_GROUP_CALLS = "calls";
private final Application application;
@ -32,9 +33,22 @@ public final class Channels {
this.initializeForegroundChannel(notificationManager);
this.initializeIncomingCallChannel(notificationManager);
this.initializeOngoingCallChannel(notificationManager);
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void initializeOngoingCallChannel(NotificationManager notificationManager) {
final NotificationChannel ongoingCallsChannel =
new NotificationChannel(
CHANNEL_ONGOING_CALL,
application.getString(R.string.ongoing_calls_channel_name),
NotificationManager.IMPORTANCE_LOW);
ongoingCallsChannel.setShowBadge(false);
ongoingCallsChannel.setGroup(CHANNEL_GROUP_CALLS);
notificationManager.createNotificationChannel(ongoingCallsChannel);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void initializeGroups(NotificationManager notificationManager) {
notificationManager.createNotificationChannelGroup(
@ -67,7 +81,7 @@ public final class Channels {
private void initializeIncomingCallChannel(final NotificationManager notificationManager) {
final NotificationChannel incomingCallsChannel =
new NotificationChannel(
INCOMING_CALLS_NOTIFICATION_CHANNEL,
CHANNEL_INCOMING_CALL,
application.getString(R.string.incoming_calls_channel_name),
NotificationManager.IMPORTANCE_HIGH);
incomingCallsChannel.setSound(null, null);

View file

@ -1,5 +1,6 @@
package im.conversations.android.notification;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
@ -31,4 +32,13 @@ public class OngoingCall {
public int hashCode() {
return Objects.hashCode(id, media, reconnecting);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("media", media)
.add("reconnecting", reconnecting)
.toString();
}
}

View file

@ -15,6 +15,7 @@ import android.net.Uri;
import android.os.Build;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
@ -40,6 +41,7 @@ public class RtpSessionNotification extends AbstractNotification {
private static final Logger LOGGER = LoggerFactory.getLogger(RtpSessionNotification.class);
public static final int INCOMING_CALL_ID = 2;
public static final int ONGOING_CALL_ID = 3;
public static final int LED_COLOR = 0xff00ff00;
@ -149,8 +151,7 @@ public class RtpSessionNotification extends AbstractNotification {
fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
final NotificationCompat.Builder builder =
new NotificationCompat.Builder(
context, Channels.INCOMING_CALLS_NOTIFICATION_CHANNEL);
new NotificationCompat.Builder(context, Channels.CHANNEL_INCOMING_CALL);
if (media.contains(Media.VIDEO)) {
builder.setSmallIcon(R.drawable.ic_videocam_24dp);
builder.setContentTitle(context.getString(R.string.rtp_state_incoming_video_call));
@ -181,7 +182,7 @@ public class RtpSessionNotification extends AbstractNotification {
R.drawable.ic_call_end_24dp,
context.getString(R.string.dismiss_call),
createCallAction(
id.sessionId, RtpSessionService.ACTION_DISMISS_CALL, 102))
account, id, RtpSessionService.ACTION_REJECT_CALL, 102))
.build());
builder.addAction(
new NotificationCompat.Action.Builder(
@ -199,7 +200,7 @@ public class RtpSessionNotification extends AbstractNotification {
public Notification getOngoingCallNotification(final Account account, OngoingCall ongoingCall) {
final AbstractJingleConnection.Id id = ongoingCall.id;
final NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, "ongoing_calls");
new NotificationCompat.Builder(context, Channels.CHANNEL_ONGOING_CALL);
if (ongoingCall.media.contains(Media.VIDEO)) {
builder.setSmallIcon(R.drawable.ic_videocam_24dp);
if (ongoingCall.reconnecting) {
@ -216,7 +217,8 @@ public class RtpSessionNotification extends AbstractNotification {
}
}
// TODO fix me when we have a Contact model
// builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
builder.setContentText(
"Contact Name"); // id.account.getRoster().getContact(id.with).getDisplayName());
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
builder.setPriority(NotificationCompat.PRIORITY_HIGH);
builder.setCategory(NotificationCompat.CATEGORY_CALL);
@ -227,11 +229,26 @@ public class RtpSessionNotification extends AbstractNotification {
R.drawable.ic_call_end_24dp,
context.getString(R.string.hang_up),
createCallAction(
id.sessionId, RtpSessionService.ACTION_END_CALL, 104))
account, id, RtpSessionService.ACTION_END_CALL, 104))
.build());
return builder.build();
}
public static boolean isShowingOngoingCallNotification(final Context context) {
final var notificationManager =
ContextCompat.getSystemService(context, NotificationManager.class);
if (notificationManager == null) {
return false;
}
for (final StatusBarNotification statusBarNotification :
notificationManager.getActiveNotifications()) {
if (statusBarNotification.getId() == ONGOING_CALL_ID) {
return true;
}
}
return false;
}
private PendingIntent createPendingRtpSession(
final Account account,
final AbstractJingleConnection.Id id,
@ -249,11 +266,14 @@ public class RtpSessionNotification extends AbstractNotification {
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
}
private PendingIntent createCallAction(String sessionId, final String action, int requestCode) {
private PendingIntent createCallAction(
Account account, AbstractJingleConnection.Id id, final String action, int requestCode) {
final Intent intent = new Intent(context, RtpSessionService.class);
intent.setAction(action);
intent.setPackage(context.getPackageName());
intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.id);
intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toString());
return PendingIntent.getService(
context,
requestCode,
@ -273,6 +293,8 @@ public class RtpSessionNotification extends AbstractNotification {
public void pushMissedCallNow(CallLogTransformation message) {}
public void cancelOngoingCallNotification() {}
private class VibrationRunnable implements Runnable {
@Override

View file

@ -5,6 +5,7 @@ import android.content.Intent;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleService;
import im.conversations.android.notification.ForegroundServiceNotification;
import im.conversations.android.notification.RtpSessionNotification;
import im.conversations.android.xmpp.ConnectionPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -19,7 +20,6 @@ public class ForegroundService extends LifecycleService {
@Override
public void onCreate() {
super.onCreate();
LOGGER.info("Creating service");
final var pool = ConnectionPool.getInstance(this);
startForeground(
ForegroundServiceNotification.ID,
@ -39,6 +39,14 @@ public class ForegroundService extends LifecycleService {
}
public static void start(final Context context) {
if (RtpSessionNotification.isShowingOngoingCallNotification(context)) {
LOGGER.info("Do not start foreground service. Ongoing call.");
return;
}
startForegroundService(context);
}
static void startForegroundService(final Context context) {
try {
ContextCompat.startForegroundService(
context, new Intent(context, ForegroundService.class));
@ -46,4 +54,9 @@ public class ForegroundService extends LifecycleService {
LOGGER.error("Could not start foreground service", e);
}
}
public static void stop(final Context context) {
final var intent = new Intent(context, ForegroundService.class);
context.stopService(intent);
}
}

View file

@ -1,16 +1,153 @@
package im.conversations.android.service;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleService;
import com.google.common.base.Strings;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import im.conversations.android.notification.RtpSessionNotification;
import im.conversations.android.ui.activity.RtpSessionActivity;
import im.conversations.android.xmpp.ConnectionPool;
import im.conversations.android.xmpp.manager.JingleConnectionManager;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RtpSessionService extends LifecycleService {
private static final Logger LOGGER = LoggerFactory.getLogger(RtpSessionService.class);
public static final String ACTION_DISMISS_CALL = "dismiss_call";
public static final String ACTION_REJECT_CALL = "dismiss_call";
public static final String ACTION_END_CALL = "end_call";
public static final String ACTION_UPDATE_ONGOING_CALL = "update_ongoing_call";
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (intent == null) {
LOGGER.info("Intent was null");
return Service.START_NOT_STICKY;
}
final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
final long accountId = intent.getLongExtra(RtpSessionActivity.EXTRA_ACCOUNT, -1);
final String with = intent.getStringExtra(RtpSessionActivity.EXTRA_WITH);
if (Strings.isNullOrEmpty(sessionId) || accountId < 0 || Strings.isNullOrEmpty(with)) {
LOGGER.warn("intent was missing mandatory extras");
return Service.START_NOT_STICKY;
}
final String action = intent.getAction();
switch (Strings.nullToEmpty(action)) {
case ACTION_UPDATE_ONGOING_CALL:
updateOngoingCall(accountId, JidCreate.fromOrThrowUnchecked(with), sessionId);
break;
case ACTION_REJECT_CALL:
rejectCall(accountId, JidCreate.fromOrThrowUnchecked(with), sessionId);
break;
case ACTION_END_CALL:
endCall(accountId, JidCreate.fromOrThrowUnchecked(with), sessionId);
break;
default:
LOGGER.error("Service does not know how to handle {} action", action);
}
return super.onStartCommand(intent, flags, startId);
}
private void endCall(final long account, final Jid with, final String sessionId) {
final var jmc =
ConnectionPool.getInstance(this)
.getOptional(account)
.transform(xc -> xc.getManager(JingleConnectionManager.class));
if (jmc.isPresent()) {
endCall(jmc.get(), with, sessionId);
} else {
LOGGER.error("Could not end call. JingleConnectionManager not found");
}
}
private void endCall(
final JingleConnectionManager jingleConnectionManager,
final Jid with,
final String sessionId) {
final var rtpConnection = jingleConnectionManager.getJingleRtpConnection(with, sessionId);
if (rtpConnection.isPresent()) {
rtpConnection.get().endCall();
} else {
LOGGER.error("Could not end {} with {}", sessionId, with);
}
}
private void rejectCall(long account, Jid with, String sessionId) {
final var jmc =
ConnectionPool.getInstance(this)
.getOptional(account)
.transform(xc -> xc.getManager(JingleConnectionManager.class));
if (jmc.isPresent()) {
rejectCall(jmc.get(), with, sessionId);
} else {
LOGGER.error("Could not reject call. JingleConnectionManager not found");
}
}
private void rejectCall(
JingleConnectionManager jingleConnectionManager, Jid with, String sessionId) {
final var rtpConnection = jingleConnectionManager.getJingleRtpConnection(with, sessionId);
if (rtpConnection.isPresent()) {
rtpConnection.get().rejectCall();
} else {
LOGGER.error("Could not reject call {} with {}", sessionId, with);
}
}
private void updateOngoingCall(final long account, final Jid with, final String sessionId) {
final var jmc =
ConnectionPool.getInstance(this)
.getOptional(account)
.transform(xc -> xc.getManager(JingleConnectionManager.class));
if (jmc.isPresent()) {
updateOngoingCall(jmc.get(), with, sessionId);
} else {
LOGGER.error("JingleConnectionManager not found for {}", account);
}
}
private void updateOngoingCall(
final JingleConnectionManager jingleConnectionManager,
final Jid with,
final String sessionId) {
final var ongoingCall =
jingleConnectionManager
.getJingleRtpConnection(with, sessionId)
.transform(JingleRtpConnection::getOngoingCall);
if (ongoingCall.isPresent()) {
LOGGER.info("Updating notification for {}", ongoingCall.get());
ForegroundService.stop(this);
startForeground(
RtpSessionNotification.ONGOING_CALL_ID,
jingleConnectionManager
.getNotificationService()
.getOngoingCallNotification(
jingleConnectionManager.getAccount(), ongoingCall.get()));
} else {
LOGGER.error("JingleRtpConnection not found for {}", sessionId);
}
}
public static void updateOngoingCall(
final Context context, final long account, final AbstractJingleConnection.Id id) {
final var intent = new Intent(context, RtpSessionService.class);
intent.setAction(ACTION_UPDATE_ONGOING_CALL);
intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account);
intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toString());
intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
ContextCompat.startForegroundService(context, intent);
}
public static void stop(final Context context) {
final var intent = new Intent(context, RtpSessionService.class);
context.stopService(intent);
ForegroundService.startForegroundService(context);
}
}

View file

@ -15,7 +15,6 @@ public class MainActivity extends BaseActivity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ForegroundService.start(this);
final ActivityMainBinding binding =
DataBindingUtil.setContentView(this, R.layout.activity_main);
final ViewModelProvider viewModelProvider =
@ -33,4 +32,10 @@ public class MainActivity extends BaseActivity {
});
Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
}
@Override
public void onStart() {
super.onStart();
ForegroundService.start(this);
}
}

View file

@ -732,7 +732,7 @@ public class RtpSessionActivity extends BaseActivity
private boolean initializeActivityWithRunningRtpSession(
final Account account, Jid with, String sessionId) {
final WeakReference<JingleRtpConnection> reference =
requireJingleConnectionManager().findJingleRtpConnection(with, sessionId);
requireJingleConnectionManager().getWeakJingleRtpConnection(with, sessionId);
if (reference == null || reference.get() == null) {
final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession =
requireJingleConnectionManager().getTerminalSessionState(with, sessionId);

View file

@ -13,7 +13,6 @@ public class SettingsActivity extends BaseActivity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ForegroundService.start(this);
final ActivitySettingsBinding binding =
DataBindingUtil.setContentView(this, R.layout.activity_settings);
setSupportActionBar(binding.materialToolbar);
@ -36,4 +35,10 @@ public class SettingsActivity extends BaseActivity {
}
});
}
@Override
public void onStart() {
super.onStart();
ForegroundService.start(this);
}
}

View file

@ -109,6 +109,10 @@ public class ConnectionPool {
reconfigurationExecutor);
}
public synchronized Optional<XmppConnection> getOptional(final long id) {
return Iterables.tryFind(this.connections, c -> id == c.getAccount().id);
}
public synchronized ListenableFuture<XmppConnection> get(final long id) {
final var configured = Iterables.tryFind(this.connections, c -> id == c.getAccount().id);
if (configured.isPresent()) {

View file

@ -26,6 +26,7 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
import im.conversations.android.IDs;
import im.conversations.android.database.model.Account;
import im.conversations.android.notification.OngoingCall;
import im.conversations.android.notification.RtpSessionNotification;
import im.conversations.android.xml.Element;
import im.conversations.android.xml.Namespace;
@ -522,6 +523,18 @@ public class JingleConnectionManager extends AbstractManager {
return Optional.absent();
}
public OngoingCall getOngoingCall(final String sessionId) {
for (final Map.Entry<AbstractJingleConnection.Id, AbstractJingleConnection> entry :
this.connections.entrySet()) {
if (entry.getValue() instanceof JingleRtpConnection
&& entry.getKey().sessionId.equals(sessionId)) {
final var jingleRtpConnection = (JingleRtpConnection) entry.getValue();
return jingleRtpConnection.getOngoingCall();
}
}
return null;
}
void finishConnection(final AbstractJingleConnection connection) {
this.connections.remove(connection.getId());
}
@ -646,13 +659,21 @@ public class JingleConnectionManager extends AbstractManager {
resendSessionProposals();
}
public WeakReference<JingleRtpConnection> findJingleRtpConnection(Jid with, String sessionId) {
public WeakReference<JingleRtpConnection> getWeakJingleRtpConnection(
Jid with, String sessionId) {
final var jingleRtpConnection = getJingleRtpConnection(with, sessionId);
return jingleRtpConnection.isPresent()
? new WeakReference<>(jingleRtpConnection.get())
: null;
}
public Optional<JingleRtpConnection> getJingleRtpConnection(Jid with, String sessionId) {
final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(with, sessionId);
final AbstractJingleConnection connection = connections.get(id);
if (connection instanceof JingleRtpConnection) {
return new WeakReference<>((JingleRtpConnection) connection);
return Optional.of((JingleRtpConnection) connection);
}
return null;
return Optional.absent();
}
private void resendSessionProposals() {
@ -701,34 +722,6 @@ public class JingleConnectionManager extends AbstractManager {
}
}
public void rejectRtpSession(final String sessionId) {
for (final AbstractJingleConnection connection : this.connections.values()) {
if (connection.getId().sessionId.equals(sessionId)) {
if (connection instanceof JingleRtpConnection) {
try {
((JingleRtpConnection) connection).rejectCall();
return;
} catch (final IllegalStateException e) {
Log.w(
Config.LOGTAG,
"race condition on rejecting call from notification",
e);
}
}
}
}
}
public void endRtpSession(final String sessionId) {
for (final AbstractJingleConnection connection : this.connections.values()) {
if (connection.getId().sessionId.equals(sessionId)) {
if (connection instanceof JingleRtpConnection) {
((JingleRtpConnection) connection).endCall();
}
}
}
}
public void failProceed(final Jid with, final String sessionId, final String message) {
final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(with, sessionId);
final AbstractJingleConnection existingJingleConnection = connections.get(id);