Fix phone call from lock screen (#835)

* Adjusted microphone -> phoneCall use call which allows foreground services to work from locked screens

* Found a way to force the device to wake up & accept a call if the foreground service is stopped - but it's ugly

* WIP

* Minor cleanup

* Fix minor issue

* Cleanup

* Further cleanup

* Attempt to unlock keyguard if locked on incoming call

* Minor tidyup

* Cleaned method name

* Bump canonical version code & name for a 1.20.8 release

* Modernised fullscreen intent flags

* Prevent dangling call activity on resume after taking a call from lock screen after force close

* Prevented stale call activity hanging around when call finished having been woken from lock via fullscreen intent

* Addressed PR feedback

* Further PR feedback & fixed dead call activity in history if denied outright

* Hopefully final PR feedback / adjustments

* Corrected some things I'd accidentally missed

* Adjustments to run wakeup 'emergency exit' timer off the main thread as per PR feedback

* Removed accidentally left in commented code

* Removed no longer valid comment

* Removed unused imports

* Removed wait for screen wakeup - still seems to work okay (unlike if we wait but do it wrong) such as via a few commits ago

* Removed unused imports - always gets me, that one..

---------

Co-authored-by: alansley <aclansley@gmail.com>
pull/1709/head
AL-Session 5 months ago committed by GitHub
parent 5053dafbf3
commit b2fe382186
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -13,8 +13,8 @@ configurations.forEach {
it.exclude module: "commons-logging" it.exclude module: "commons-logging"
} }
def canonicalVersionCode = 389 def canonicalVersionCode = 390
def canonicalVersionName = "1.20.7" def canonicalVersionName = "1.20.8"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,
@ -299,7 +299,7 @@ dependencies {
implementation 'androidx.media3:media3-ui:1.4.0' implementation 'androidx.media3:media3-ui:1.4.0'
implementation 'org.conscrypt:conscrypt-android:2.5.2' implementation 'org.conscrypt:conscrypt-android:2.5.2'
implementation 'org.signal:aesgcmprovider:0.0.3' implementation 'org.signal:aesgcmprovider:0.0.3'
implementation 'io.github.webrtc-sdk:android:125.6422.04' implementation 'io.github.webrtc-sdk:android:125.6422.06.1'
implementation "me.leolin:ShortcutBadger:1.1.16" implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0' implementation 'se.emilsjolander:stickylistheaders:2.7.0'
implementation 'com.jpardogo.materialtabstrip:library:1.0.9' implementation 'com.jpardogo.materialtabstrip:library:1.0.9'

@ -29,38 +29,49 @@
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
android:required="false" /> android:required="false" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" tools:node="remove" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" /> <!-- For video calls -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" /> <!-- For calls that get audio from bluetooth headsets -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Only used on Android API 29 and lower --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- Google may (potentially) insist we implement `ConnectionService` to request MANAGE_OWN_CALLS - see:
- https://developer.android.com/reference/android/Manifest.permission#MANAGE_OWN_CALLS
- https://developer.android.com/reference/android/telecom/ConnectionService
-->
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.app.role.DIALER" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Only used on Android API 29 and lower -->
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" /> <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" /> <uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" tools:node="remove"/>
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<queries> <queries>
<intent> <intent>
@ -304,8 +315,10 @@
</activity> </activity>
<activity android:name="org.thoughtcrime.securesms.media.MediaOverviewActivity" /> <activity android:name="org.thoughtcrime.securesms.media.MediaOverviewActivity" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService" <service
android:foregroundServiceType="microphone" android:enabled="true"
android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
android:foregroundServiceType="phoneCall"
android:exported="false" /> android:exported="false" />
<service <service
android:name="org.thoughtcrime.securesms.service.KeyCachingService" android:name="org.thoughtcrime.securesms.service.KeyCachingService"

@ -20,12 +20,13 @@ import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Application; import android.app.Application;
import android.app.KeyguardManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.PowerManager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.content.pm.ShortcutManagerCompat;
@ -33,7 +34,20 @@ import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner; import androidx.lifecycle.ProcessLifecycleOwner;
import dagger.hilt.EntryPoints;
import dagger.hilt.android.HiltAndroidApp;
import java.io.IOException;
import java.io.InputStream;
import java.security.Security;
import java.util.Arrays;
import java.util.List;
import java.util.Timer;
import java.util.concurrent.Executors;
import javax.inject.Inject;
import network.loki.messenger.BuildConfig;
import network.loki.messenger.R;
import network.loki.messenger.libsession_util.ConfigBase;
import network.loki.messenger.libsession_util.UserProfile;
import org.conscrypt.Conscrypt; import org.conscrypt.Conscrypt;
import org.session.libsession.database.MessageDataProvider; import org.session.libsession.database.MessageDataProvider;
import org.session.libsession.messaging.MessagingModuleConfiguration; import org.session.libsession.messaging.MessagingModuleConfiguration;
@ -44,6 +58,7 @@ import org.session.libsession.snode.SnodeModule;
import org.session.libsession.utilities.ConfigFactoryUpdateListener; import org.session.libsession.utilities.ConfigFactoryUpdateListener;
import org.session.libsession.utilities.Device; import org.session.libsession.utilities.Device;
import org.session.libsession.utilities.Environment; import org.session.libsession.utilities.Environment;
import org.session.libsession.utilities.NonTranslatableStringConstants;
import org.session.libsession.utilities.ProfilePictureUtilities; import org.session.libsession.utilities.ProfilePictureUtilities;
import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
@ -88,25 +103,8 @@ import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
import org.thoughtcrime.securesms.util.Broadcaster; import org.thoughtcrime.securesms.util.Broadcaster;
import org.thoughtcrime.securesms.util.VersionDataFetcher; import org.thoughtcrime.securesms.util.VersionDataFetcher;
import org.thoughtcrime.securesms.webrtc.CallMessageProcessor; import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.PeerConnectionFactory.InitializationOptions; import org.webrtc.PeerConnectionFactory.InitializationOptions;
import org.webrtc.PeerConnectionFactory;
import java.io.IOException;
import java.io.InputStream;
import java.security.Security;
import java.util.Arrays;
import java.util.List;
import java.util.Timer;
import java.util.concurrent.Executors;
import javax.inject.Inject;
import dagger.hilt.EntryPoints;
import dagger.hilt.android.HiltAndroidApp;
import network.loki.messenger.BuildConfig;
import network.loki.messenger.R;
import network.loki.messenger.libsession_util.ConfigBase;
import network.loki.messenger.libsession_util.UserProfile;
/** /**
* Will be called once when the TextSecure process is created. * Will be called once when the TextSecure process is created.
@ -148,7 +146,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
CallMessageProcessor callMessageProcessor; CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration; MessagingModuleConfiguration messagingModuleConfiguration;
private volatile boolean isAppVisible; public volatile boolean isAppVisible;
public String KEYGUARD_LOCK_TAG = NonTranslatableStringConstants.APP_NAME + ":KeyguardLock";
public String WAKELOCK_TAG = NonTranslatableStringConstants.APP_NAME + ":WakeLock";
@Override @Override
public Object getSystemService(String name) { public Object getSystemService(String name) {
@ -457,11 +457,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
} }
// Method to clear the local data - returns true on success otherwise false // Method to clear the local data - returns true on success otherwise false
/**
* Clear all local profile data and message history.
* @return true on success, false otherwise.
*/
@SuppressLint("ApplySharedPref") @SuppressLint("ApplySharedPref")
public boolean clearAllData() { public boolean clearAllData() {
TextSecurePreferences.clearAll(this); TextSecurePreferences.clearAll(this);
@ -492,4 +487,35 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
} }
// endregion // endregion
// Method to wake up the screen and dismiss the keyguard
public void wakeUpDeviceAndDismissKeyguardIfRequired() {
// Get the KeyguardManager and PowerManager
KeyguardManager keyguardManager = (KeyguardManager)getSystemService(Context.KEYGUARD_SERVICE);
PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
// Check if the phone is locked & if the screen is awake
boolean isPhoneLocked = keyguardManager.isKeyguardLocked();
boolean isScreenAwake = powerManager.isInteractive();
if (!isScreenAwake) {
PowerManager.WakeLock wakeLock = powerManager.newWakeLock(
PowerManager.FULL_WAKE_LOCK
| PowerManager.ACQUIRE_CAUSES_WAKEUP
| PowerManager.ON_AFTER_RELEASE,
WAKELOCK_TAG);
// Acquire the wake lock to wake up the device
wakeLock.acquire(3000);
}
// Dismiss the keyguard.
// Note: This will not bypass any app-level (Session) lock; only the device-level keyguard.
// TODO: When moving to a minimum Android API of 27, replace these deprecated calls with new APIs.
if (isPhoneLocked) {
KeyguardManager.KeyguardLock keyguardLock = keyguardManager.newKeyguardLock(KEYGUARD_LOCK_TAG);
keyguardLock.disableKeyguard();
}
}
} }

@ -21,9 +21,9 @@ import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.style.RelativeSizeSpan; import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.List;
import java.util.Objects;
import org.session.libsession.messaging.calls.CallMessageType; import org.session.libsession.messaging.calls.CallMessageType;
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage; import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage;
import org.session.libsession.messaging.utilities.UpdateMessageBuilder; import org.session.libsession.messaging.utilities.UpdateMessageBuilder;
@ -33,9 +33,6 @@ import org.session.libsession.utilities.NetworkFailure;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import java.util.List;
import java.util.Objects;
/** /**
* The base class for message record models that are displayed in * The base class for message record models that are displayed in
* conversations, as opposed to models that are displayed in a thread list. * conversations, as opposed to models that are displayed in a thread list.

@ -103,7 +103,7 @@ public class ThreadRecord extends DisplayRecord {
@Override @Override
public CharSequence getDisplayBody(@NonNull Context context) { public CharSequence getDisplayBody(@NonNull Context context) {
// no need to display anything if there are no messages // no need to display anything if there are no messages
if(lastMessage == null){ if (lastMessage == null){
return ""; return "";
} }
else if (isGroupUpdateMessage()) { else if (isGroupUpdateMessage()) {

@ -28,8 +28,6 @@ import android.database.Cursor
import android.os.AsyncTask import android.os.AsyncTask
import android.os.Build import android.os.Build
import android.text.TextUtils import android.text.TextUtils
import android.widget.Toast
import androidx.camera.core.impl.utils.ContextUtil.getApplicationContext
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@ -47,8 +45,8 @@ import org.session.libsession.messaging.utilities.AccountId
import org.session.libsession.messaging.utilities.SodiumUtilities.blindedKeyPair import org.session.libsession.messaging.utilities.SodiumUtilities.blindedKeyPair
import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.ServiceUtil import org.session.libsession.utilities.ServiceUtil
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
import org.session.libsession.utilities.TextSecurePreferences.Companion.getNotificationPrivacy import org.session.libsession.utilities.TextSecurePreferences.Companion.getNotificationPrivacy
import org.session.libsession.utilities.TextSecurePreferences.Companion.getRepeatAlertsCount import org.session.libsession.utilities.TextSecurePreferences.Companion.getRepeatAlertsCount
@ -70,8 +68,6 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
import org.thoughtcrime.securesms.mms.SlideDeck import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.preferences.ShareLogsDialog
import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.util.SessionMetaProtocol.canUserReplyToNotification import org.thoughtcrime.securesms.util.SessionMetaProtocol.canUserReplyToNotification
import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.SpanUtil
@ -202,7 +198,6 @@ class DefaultMessageNotifier : MessageNotifier {
override fun updateNotification(context: Context, signal: Boolean, reminderCount: Int) { override fun updateNotification(context: Context, signal: Boolean, reminderCount: Int) {
var playNotificationAudio = signal // Local copy of the argument so we can modify it var playNotificationAudio = signal // Local copy of the argument so we can modify it
var telcoCursor: Cursor? = null var telcoCursor: Cursor? = null
val pushCursor: Cursor? = null
try { try {
telcoCursor = get(context).mmsSmsDatabase().unread // TODO: add a notification specific lighter query here telcoCursor = get(context).mmsSmsDatabase().unread // TODO: add a notification specific lighter query here
@ -228,14 +223,14 @@ class DefaultMessageNotifier : MessageNotifier {
sendSingleThreadNotification(context, NotificationState(notificationState.getNotificationsForThread(threadId)), false, true) sendSingleThreadNotification(context, NotificationState(notificationState.getNotificationsForThread(threadId)), false, true)
} }
sendMultipleThreadNotification(context, notificationState, playNotificationAudio) sendMultipleThreadNotification(context, notificationState, playNotificationAudio)
} else if (notificationState.messageCount > 0) { } else if (notificationState.notificationCount > 0) {
sendSingleThreadNotification(context, notificationState, playNotificationAudio, false) sendSingleThreadNotification(context, notificationState, playNotificationAudio, false)
} else { } else {
cancelActiveNotifications(context) cancelActiveNotifications(context)
} }
cancelOrphanedNotifications(context, notificationState) cancelOrphanedNotifications(context, notificationState)
updateBadge(context, notificationState.messageCount) updateBadge(context, notificationState.notificationCount)
if (playNotificationAudio) { if (playNotificationAudio) {
scheduleReminder(context, reminderCount) scheduleReminder(context, reminderCount)
@ -267,7 +262,7 @@ class DefaultMessageNotifier : MessageNotifier {
val builder = SingleRecipientNotificationBuilder(context, getNotificationPrivacy(context)) val builder = SingleRecipientNotificationBuilder(context, getNotificationPrivacy(context))
val notifications = notificationState.notifications val notifications = notificationState.notifications
val recipient = notifications[0].recipient val messageOriginator = notifications[0].recipient
val notificationId = (SUMMARY_NOTIFICATION_ID + (if (bundled) notifications[0].threadId else 0)).toInt() val notificationId = (SUMMARY_NOTIFICATION_ID + (if (bundled) notifications[0].threadId else 0)).toInt()
val messageIdTag = notifications[0].timestamp.toString() val messageIdTag = notifications[0].timestamp.toString()
@ -285,12 +280,12 @@ class DefaultMessageNotifier : MessageNotifier {
builder.putStringExtra(LATEST_MESSAGE_ID_TAG, messageIdTag) builder.putStringExtra(LATEST_MESSAGE_ID_TAG, messageIdTag)
val text = notifications[0].text val notificationText = notifications[0].text
builder.setThread(notifications[0].recipient) builder.setThread(notifications[0].recipient)
builder.setMessageCount(notificationState.messageCount) builder.setMessageCount(notificationState.notificationCount)
val builderCS = text ?: "" val builderCS = notificationText ?: ""
val ss = highlightMentions( val ss = highlightMentions(
builderCS, builderCS,
false, false,
@ -301,7 +296,7 @@ class DefaultMessageNotifier : MessageNotifier {
) )
builder.setPrimaryMessageBody( builder.setPrimaryMessageBody(
recipient, messageOriginator,
notifications[0].individualRecipient, notifications[0].individualRecipient,
ss, ss,
notifications[0].slideDeck notifications[0].slideDeck
@ -313,12 +308,12 @@ class DefaultMessageNotifier : MessageNotifier {
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
builder.setAutoCancel(true) builder.setAutoCancel(true)
val replyMethod = ReplyMethod.forRecipient(context, recipient) val replyMethod = ReplyMethod.forRecipient(context, messageOriginator)
val canReply = canUserReplyToNotification(recipient) val canReply = canUserReplyToNotification(messageOriginator)
val quickReplyIntent = if (canReply) notificationState.getQuickReplyIntent(context, recipient) else null val quickReplyIntent = if (canReply) notificationState.getQuickReplyIntent(context, messageOriginator) else null
val remoteReplyIntent = if (canReply) notificationState.getRemoteReplyIntent(context, recipient, replyMethod) else null val remoteReplyIntent = if (canReply) notificationState.getRemoteReplyIntent(context, messageOriginator, replyMethod) else null
builder.addActions( builder.addActions(
notificationState.getMarkAsReadIntent(context, notificationId), notificationState.getMarkAsReadIntent(context, notificationId),
@ -329,14 +324,13 @@ class DefaultMessageNotifier : MessageNotifier {
if (canReply) { if (canReply) {
builder.addAndroidAutoAction( builder.addAndroidAutoAction(
notificationState.getAndroidAutoReplyIntent(context, recipient), notificationState.getAndroidAutoReplyIntent(context, messageOriginator),
notificationState.getAndroidAutoHeardIntent(context, notificationId), notificationState.getAndroidAutoHeardIntent(context, notificationId),
notifications[0].timestamp notifications[0].timestamp
) )
} }
val iterator: ListIterator<NotificationItem> = notifications.listIterator(notifications.size) val iterator: ListIterator<NotificationItem> = notifications.listIterator(notifications.size)
while (iterator.hasPrevious()) { while (iterator.hasPrevious()) {
val item = iterator.previous() val item = iterator.previous()
builder.addMessageBody(item.recipient, item.individualRecipient, item.text) builder.addMessageBody(item.recipient, item.individualRecipient, item.text)
@ -368,6 +362,7 @@ class DefaultMessageNotifier : MessageNotifier {
// for ActivityCompat#requestPermissions for more details. // for ActivityCompat#requestPermissions for more details.
return return
} }
NotificationManagerCompat.from(context).notify(notificationId, notification) NotificationManagerCompat.from(context).notify(notificationId, notification)
Log.i(TAG, "Posted notification. $notification") Log.i(TAG, "Posted notification. $notification")
} }
@ -383,7 +378,7 @@ class DefaultMessageNotifier : MessageNotifier {
val builder = MultipleRecipientNotificationBuilder(context, getNotificationPrivacy(context)) val builder = MultipleRecipientNotificationBuilder(context, getNotificationPrivacy(context))
val notifications = notificationState.notifications val notifications = notificationState.notifications
builder.setMessageCount(notificationState.messageCount, notificationState.threadCount) builder.setMessageCount(notificationState.notificationCount, notificationState.threadCount)
builder.setMostRecentSender(notifications[0].individualRecipient, notifications[0].recipient) builder.setMostRecentSender(notifications[0].individualRecipient, notifications[0].recipient)
builder.setGroup(NOTIFICATION_GROUP) builder.setGroup(NOTIFICATION_GROUP)
builder.setDeleteIntent(notificationState.getDeleteIntent(context)) builder.setDeleteIntent(notificationState.getDeleteIntent(context))

@ -3,20 +3,18 @@ package org.thoughtcrime.securesms.notifications;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.Recipient.VibrateState;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import org.session.libsession.utilities.recipients.Recipient.VibrateState;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
public class NotificationState { public class NotificationState {
@ -36,68 +34,47 @@ public class NotificationState {
} }
public void addNotification(NotificationItem item) { public void addNotification(NotificationItem item) {
// Add this new notification at the beginning of the list
notifications.addFirst(item); notifications.addFirst(item);
if (threads.contains(item.getThreadId())) { // Put a notification at the front by removing it then re-adding it?
threads.remove(item.getThreadId()); threads.remove(item.getThreadId());
}
threads.add(item.getThreadId()); threads.add(item.getThreadId());
notificationCount++; notificationCount++;
} }
public @Nullable Uri getRingtone(@NonNull Context context) { public @Nullable Uri getRingtone(@NonNull Context context) {
if (!notifications.isEmpty()) { if (!notifications.isEmpty()) {
Recipient recipient = notifications.getFirst().getRecipient(); Recipient recipient = notifications.getFirst().getRecipient();
if (recipient != null) {
return NotificationChannels.getMessageRingtone(context, recipient); return NotificationChannels.getMessageRingtone(context, recipient);
} }
}
return null; return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
} }
public VibrateState getVibrate() { public VibrateState getVibrate() {
if (!notifications.isEmpty()) { if (!notifications.isEmpty()) {
Recipient recipient = notifications.getFirst().getRecipient(); Recipient recipient = notifications.getFirst().getRecipient();
if (recipient != null) {
return recipient.resolve().getMessageVibrate(); return recipient.resolve().getMessageVibrate();
} }
}
return VibrateState.DEFAULT; return VibrateState.DEFAULT;
} }
public boolean hasMultipleThreads() { public boolean hasMultipleThreads() { return threads.size() > 1; }
return threads.size() > 1; public LinkedHashSet<Long> getThreads() { return threads; }
} public int getThreadCount() { return threads.size(); }
public int getNotificationCount() { return notificationCount; }
public LinkedHashSet<Long> getThreads() { public List<NotificationItem> getNotifications() { return notifications; }
return threads;
}
public int getThreadCount() {
return threads.size();
}
public int getMessageCount() {
return notificationCount;
}
public List<NotificationItem> getNotifications() {
return notifications;
}
public List<NotificationItem> getNotificationsForThread(long threadId) { public List<NotificationItem> getNotificationsForThread(long threadId) {
LinkedList<NotificationItem> list = new LinkedList<>(); LinkedList<NotificationItem> notificationsInThread = new LinkedList<>();
for (NotificationItem item : notifications) { for (NotificationItem item : notifications) {
if (item.getThreadId() == threadId) list.addFirst(item); if (item.getThreadId() == threadId) notificationsInThread.addFirst(item);
} }
return list; return notificationsInThread;
} }
public PendingIntent getMarkAsReadIntent(Context context, int notificationId) { public PendingIntent getMarkAsReadIntent(Context context, int notificationId) {
@ -111,7 +88,7 @@ public class NotificationState {
Intent intent = new Intent(MarkReadReceiver.CLEAR_ACTION); Intent intent = new Intent(MarkReadReceiver.CLEAR_ACTION);
intent.setClass(context, MarkReadReceiver.class); intent.setClass(context, MarkReadReceiver.class);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); intent.setData((Uri.parse("custom://" + System.currentTimeMillis())));
intent.putExtra(MarkReadReceiver.THREAD_IDS_EXTRA, threadArray); intent.putExtra(MarkReadReceiver.THREAD_IDS_EXTRA, threadArray);
intent.putExtra(MarkReadReceiver.NOTIFICATION_ID_EXTRA, notificationId); intent.putExtra(MarkReadReceiver.NOTIFICATION_ID_EXTRA, notificationId);
@ -171,7 +148,7 @@ public class NotificationState {
Intent intent = new Intent(AndroidAutoHeardReceiver.HEARD_ACTION); Intent intent = new Intent(AndroidAutoHeardReceiver.HEARD_ACTION);
intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
intent.setClass(context, AndroidAutoHeardReceiver.class); intent.setClass(context, AndroidAutoHeardReceiver.class);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); intent.setData((Uri.parse("custom://" + System.currentTimeMillis())));
intent.putExtra(AndroidAutoHeardReceiver.THREAD_IDS_EXTRA, threadArray); intent.putExtra(AndroidAutoHeardReceiver.THREAD_IDS_EXTRA, threadArray);
intent.putExtra(AndroidAutoHeardReceiver.NOTIFICATION_ID_EXTRA, notificationId); intent.putExtra(AndroidAutoHeardReceiver.NOTIFICATION_ID_EXTRA, notificationId);
intent.setPackage(context.getPackageName()); intent.setPackage(context.getPackageName());
@ -223,6 +200,4 @@ public class NotificationState {
return PendingIntent.getBroadcast(context, 0, intent, intentFlags); return PendingIntent.getBroadcast(context, 0, intent, intentFlags);
} }
} }

@ -1,5 +0,0 @@
package org.thoughtcrime.securesms.notifications
interface PushManager {
fun refresh(force: Boolean)
}

@ -248,8 +248,6 @@ public class KeyCachingService extends Service {
.put(APP_NAME_KEY, c.getString(R.string.app_name)) .put(APP_NAME_KEY, c.getString(R.string.app_name))
.format().toString(); .format().toString();
builder.setContentTitle(unlockedTxt); builder.setContentTitle(unlockedTxt);
builder.setContentText(getString(R.string.lockAppUnlock));
builder.setSmallIcon(R.drawable.icon_cached); builder.setSmallIcon(R.drawable.icon_cached);
builder.setWhen(0); builder.setWhen(0);
builder.setPriority(Notification.PRIORITY_MIN); builder.setPriority(Notification.PRIORITY_MIN);

@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.service
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -19,11 +19,18 @@ import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.util.UUID
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.calls.CallMessageType
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.FutureTaskListener import org.session.libsession.utilities.FutureTaskListener
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.calls.WebRtcCallActivity import org.thoughtcrime.securesms.calls.WebRtcCallActivity
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker import org.thoughtcrime.securesms.notifications.BackgroundPollWorker
import org.thoughtcrime.securesms.util.CallNotificationBuilder import org.thoughtcrime.securesms.util.CallNotificationBuilder
@ -32,6 +39,7 @@ import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_IN
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_PRE_OFFER import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_PRE_OFFER
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_RINGING import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_RINGING
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OUTGOING_RINGING import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OUTGOING_RINGING
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.WEBRTC_NOTIFICATION
import org.thoughtcrime.securesms.webrtc.AudioManagerCommand import org.thoughtcrime.securesms.webrtc.AudioManagerCommand
import org.thoughtcrime.securesms.webrtc.CallManager import org.thoughtcrime.securesms.webrtc.CallManager
import org.thoughtcrime.securesms.webrtc.CallViewModel import org.thoughtcrime.securesms.webrtc.CallViewModel
@ -44,6 +52,7 @@ import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager
import org.thoughtcrime.securesms.webrtc.WiredHeadsetStateReceiver import org.thoughtcrime.securesms.webrtc.WiredHeadsetStateReceiver
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
import org.thoughtcrime.securesms.webrtc.data.Event import org.thoughtcrime.securesms.webrtc.data.Event
import org.thoughtcrime.securesms.webrtc.data.State as CallState
import org.thoughtcrime.securesms.webrtc.locks.LockManager import org.thoughtcrime.securesms.webrtc.locks.LockManager
import org.webrtc.DataChannel import org.webrtc.DataChannel
import org.webrtc.IceCandidate import org.webrtc.IceCandidate
@ -54,13 +63,6 @@ import org.webrtc.PeerConnection.IceConnectionState.DISCONNECTED
import org.webrtc.PeerConnection.IceConnectionState.FAILED import org.webrtc.PeerConnection.IceConnectionState.FAILED
import org.webrtc.RtpReceiver import org.webrtc.RtpReceiver
import org.webrtc.SessionDescription import org.webrtc.SessionDescription
import java.util.UUID
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import org.thoughtcrime.securesms.webrtc.data.State as CallState
@AndroidEntryPoint @AndroidEntryPoint
class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener { class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
@ -631,6 +633,20 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
} }
} }
/**
* Handles remote ICE candidates received from a signaling server.
*
* This function is called when a new ICE candidate is received for a specific call.
* It extracts the candidate information from the intent, creates IceCandidate objects,
* and passes them to the CallManager to be added to the PeerConnection.
*
* @param intent The intent containing the remote ICE candidate information.
* The intent should contain the following extras:
* - EXTRA_CALL_ID: The ID of the call.
* - EXTRA_ICE_SDP_MID: An array of SDP media stream identification strings.
* - EXTRA_ICE_SDP_LINE_INDEX: An array of SDP media line indexes.
* - EXTRA_ICE_SDP: An array of SDP candidate strings.
*/
private fun handleRemoteIceCandidate(intent: Intent) { private fun handleRemoteIceCandidate(intent: Intent) {
val callId = getCallId(intent) val callId = getCallId(intent)
val sdpMids = intent.getStringArrayExtra(EXTRA_ICE_SDP_MID) ?: return val sdpMids = intent.getStringArrayExtra(EXTRA_ICE_SDP_MID) ?: return
@ -724,24 +740,40 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
} }
} }
// Over the course of setting up a phone call this method is called multiple times with `types`
// of PRE_OFFER -> RING_INCOMING -> ICE_MESSAGE
private fun setCallInProgressNotification(type: Int, recipient: Recipient?) { private fun setCallInProgressNotification(type: Int, recipient: Recipient?) {
// Wake the device if needed
(applicationContext as ApplicationContext).wakeUpDeviceAndDismissKeyguardIfRequired()
// If notifications are enabled we'll try and start a foreground service to show the notification
var failedToStartForegroundService = false
if (CallNotificationBuilder.areNotificationsEnabled(this)) {
try { try {
ServiceCompat.startForeground( ServiceCompat.startForeground(
this, this,
CallNotificationBuilder.WEBRTC_NOTIFICATION, WEBRTC_NOTIFICATION,
CallNotificationBuilder.getCallInProgressNotification(this, type, recipient), CallNotificationBuilder.getCallInProgressNotification(this, type, recipient),
if (Build.VERSION.SDK_INT >= 30) ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE else 0 if (Build.VERSION.SDK_INT >= 30) ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL else 0
) )
return
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
Log.e(TAG, "Failed to setCallInProgressNotification as a foreground service for type: ${type}, trying to update instead", e) Log.e(TAG, "Failed to setCallInProgressNotification as a foreground service for type: ${type}, trying to update instead", e)
failedToStartForegroundService = true
}
} else {
// Notifications are NOT enabled! Skipped attempt at startForeground and going straight to fullscreen intent attempt!
} }
if (!CallNotificationBuilder.areNotificationsEnabled(this) && type == TYPE_INCOMING_PRE_OFFER) { if ((type == TYPE_INCOMING_PRE_OFFER || type == TYPE_INCOMING_RINGING) && failedToStartForegroundService) {
// Start an intent for the fullscreen call activity // Start an intent for the fullscreen call activity
val foregroundIntent = Intent(this, WebRtcCallActivity::class.java) val foregroundIntent = Intent(this, WebRtcCallActivity::class.java)
.setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_BROUGHT_TO_FRONT or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) .setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TOP)
.setAction(WebRtcCallActivity.ACTION_FULL_SCREEN_INTENT) .setAction(WebRtcCallActivity.ACTION_FULL_SCREEN_INTENT)
startActivity(foregroundIntent) startActivity(foregroundIntent)
return
} }
} }

@ -37,31 +37,6 @@ class CallNotificationBuilder {
return notificationManager.areNotificationsEnabled() return notificationManager.areNotificationsEnabled()
} }
@JvmStatic
fun getFirstCallNotification(context: Context, callerName: String): Notification {
val contentIntent = Intent(context, SettingsActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val titleTxt = context.getSubbedString(R.string.callsMissedCallFrom, NAME_KEY to callerName)
val bodyTxt = context.getSubbedCharSequence(
R.string.callsYouMissedCallPermissions,
NAME_KEY to callerName
)
val builder = NotificationCompat.Builder(context, NotificationChannels.CALLS)
.setSound(null)
.setSmallIcon(R.drawable.ic_baseline_call_24)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentTitle(titleTxt)
.setContentText(bodyTxt)
.setStyle(NotificationCompat.BigTextStyle().bigText(bodyTxt))
.setAutoCancel(true)
return builder.build()
}
@JvmStatic @JvmStatic
fun getCallInProgressNotification(context: Context, type: Int, recipient: Recipient?): Notification { fun getCallInProgressNotification(context: Context, type: Int, recipient: Recipient?): Notification {
val contentIntent = Intent(context, WebRtcCallActivity::class.java) val contentIntent = Intent(context, WebRtcCallActivity::class.java)
@ -98,9 +73,7 @@ class CallNotificationBuilder {
R.string.decline R.string.decline
)) ))
// If notifications aren't enabled, we will trigger the intent from WebRtcCallService // If notifications aren't enabled, we will trigger the intent from WebRtcCallService
builder.setFullScreenIntent(getFullScreenPendingIntent( builder.setFullScreenIntent(getFullScreenPendingIntent(context), true)
context
), true)
builder.addAction(getActivityNotificationAction( builder.addAction(getActivityNotificationAction(
context, context,
if (type == TYPE_INCOMING_PRE_OFFER) WebRtcCallActivity.ACTION_PRE_OFFER else WebRtcCallActivity.ACTION_ANSWER, if (type == TYPE_INCOMING_PRE_OFFER) WebRtcCallActivity.ACTION_PRE_OFFER else WebRtcCallActivity.ACTION_ANSWER,
@ -143,9 +116,10 @@ class CallNotificationBuilder {
private fun getFullScreenPendingIntent(context: Context): PendingIntent { private fun getFullScreenPendingIntent(context: Context): PendingIntent {
val intent = Intent(context, WebRtcCallActivity::class.java) val intent = Intent(context, WebRtcCallActivity::class.java)
.setFlags(FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) // When launching the call activity do NOT keep it in the history when finished, as it does not pass through CALL_DISCONNECTED
// if the call was denied outright, and without this the "dead" activity will sit around in the history when the device is unlocked.
.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
.setAction(WebRtcCallActivity.ACTION_FULL_SCREEN_INTENT) .setAction(WebRtcCallActivity.ACTION_FULL_SCREEN_INTENT)
return PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE) return PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)
} }

@ -1,9 +1,10 @@
package org.thoughtcrime.securesms.webrtc package org.thoughtcrime.securesms.webrtc
import android.Manifest import android.Manifest
import android.app.NotificationManager import android.app.KeyguardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.PowerManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
@ -16,6 +17,7 @@ import org.session.libsession.messaging.messages.control.CallMessage
import org.session.libsession.messaging.utilities.WebRtcUtils import org.session.libsession.messaging.utilities.WebRtcUtils
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.NonTranslatableStringConstants
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ANSWER import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ANSWER
@ -25,27 +27,34 @@ import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.OFFER
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PRE_OFFER import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PRE_OFFER
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PROVISIONAL_ANSWER import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PROVISIONAL_ANSWER
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.service.WebRtcCallService import org.thoughtcrime.securesms.service.WebRtcCallService
import org.thoughtcrime.securesms.util.CallNotificationBuilder
import org.webrtc.IceCandidate import org.webrtc.IceCandidate
class CallMessageProcessor(private val context: Context, private val textSecurePreferences: TextSecurePreferences, lifecycle: Lifecycle, private val storage: StorageProtocol) { class CallMessageProcessor(private val context: Context, private val textSecurePreferences: TextSecurePreferences, lifecycle: Lifecycle, private val storage: StorageProtocol) {
companion object { companion object {
private const val TAG = "CallMessageProcessor"
private const val VERY_EXPIRED_TIME = 15 * 60 * 1000L private const val VERY_EXPIRED_TIME = 15 * 60 * 1000L
fun safeStartService(context: Context, intent: Intent) { fun safeStartForegroundService(context: Context, intent: Intent) {
// If the foreground service crashes then it's possible for one of these intents to // Wake up the device (if required) before attempting to start any services - otherwise on Android 12 and above we get
// be started in the background (in which case 'startService' will throw a // a BackgroundServiceStartNotAllowedException such as:
// 'BackgroundServiceStartNotAllowedException' exception) so catch that case and try // Unable to start CallMessage intent: startForegroundService() not allowed due to mAllowStartForeground false:
// to re-start the service in the foreground // service network.loki.messenger/org.thoughtcrime.securesms.service.WebRtcCallService
try { context.startService(intent) } (context as ApplicationContext).wakeUpDeviceAndDismissKeyguardIfRequired()
catch(e: Exception) {
try { ContextCompat.startForegroundService(context, intent) } // Attempt to start the call service..
catch (e2: Exception) { try {
Log.e("Loki", "Unable to start CallMessage intent: ${e2.message}") context.startService(intent)
} catch (e: Exception) {
Log.e("Loki", "Unable to start service: ${e.message}", e)
try {
ContextCompat.startForegroundService(context, intent)
} catch (e2: Exception) {
Log.e(TAG, "Unable to start CallMessage intent: ${e2.message}", e2)
} }
} }
} }
@ -61,8 +70,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
Log.i("Loki", "Contact is approved?: $approvedContact") Log.i("Loki", "Contact is approved?: $approvedContact")
if (!approvedContact && storage.getUserPublicKey() != sender) continue if (!approvedContact && storage.getUserPublicKey() != sender) continue
// if the user has not enabled voice/video calls // If the user has not enabled voice/video calls or if the user has not granted audio/microphone permissions
// or if the user has not granted audio/microphone permissions
if ( if (
!textSecurePreferences.isCallNotificationsEnabled() || !textSecurePreferences.isCallNotificationsEnabled() ||
!Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO) !Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO)
@ -101,21 +109,20 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
private fun incomingHangup(callMessage: CallMessage) { private fun incomingHangup(callMessage: CallMessage) {
val callId = callMessage.callId ?: return val callId = callMessage.callId ?: return
val hangupIntent = WebRtcCallService.remoteHangupIntent(context, callId) val hangupIntent = WebRtcCallService.remoteHangupIntent(context, callId)
safeStartService(context, hangupIntent) safeStartForegroundService(context, hangupIntent)
} }
private fun incomingAnswer(callMessage: CallMessage) { private fun incomingAnswer(callMessage: CallMessage) {
val recipientAddress = callMessage.sender ?: return val recipientAddress = callMessage.sender ?: return Log.w(TAG, "Cannot answer incoming call without sender")
val callId = callMessage.callId ?: return val callId = callMessage.callId ?: return Log.w(TAG, "Cannot answer incoming call without callId" )
val sdp = callMessage.sdps.firstOrNull() ?: return val sdp = callMessage.sdps.firstOrNull() ?: return Log.w(TAG, "Cannot answer incoming call without sdp")
val answerIntent = WebRtcCallService.incomingAnswer( val answerIntent = WebRtcCallService.incomingAnswer(
context = context, context = context,
address = Address.fromSerialized(recipientAddress), address = Address.fromSerialized(recipientAddress),
sdp = sdp, sdp = sdp,
callId = callId callId = callId
) )
safeStartForegroundService(context, answerIntent)
safeStartService(context, answerIntent)
} }
private fun handleIceCandidates(callMessage: CallMessage) { private fun handleIceCandidates(callMessage: CallMessage) {
@ -131,7 +138,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
callId = callId, callId = callId,
address = Address.fromSerialized(sender) address = Address.fromSerialized(sender)
) )
safeStartService(context, iceIntent) safeStartForegroundService(context, iceIntent)
} }
private fun incomingPreOffer(callMessage: CallMessage) { private fun incomingPreOffer(callMessage: CallMessage) {
@ -144,7 +151,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
callId = callId, callId = callId,
callTime = callMessage.sentTimestamp!! callTime = callMessage.sentTimestamp!!
) )
safeStartService(context, incomingIntent) safeStartForegroundService(context, incomingIntent)
} }
private fun incomingCall(callMessage: CallMessage) { private fun incomingCall(callMessage: CallMessage) {
@ -158,7 +165,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
callId = callId, callId = callId,
callTime = callMessage.sentTimestamp!! callTime = callMessage.sentTimestamp!!
) )
safeStartService(context, incomingIntent) safeStartForegroundService(context, incomingIntent)
} }
private fun CallMessage.iceCandidates(): List<IceCandidate> { private fun CallMessage.iceCandidates(): List<IceCandidate> {

@ -1 +1 @@
Subproject commit 0193c36e0dad461385d6407a00f33b7314e6d740 Subproject commit 43b1c6c341ee8739a8678c631d0713136dbfd05f

@ -9,6 +9,10 @@ import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.UserGroupsConfig import network.loki.messenger.libsession_util.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile import network.loki.messenger.libsession_util.UserProfile
import java.util.Timer
import java.util.TimerTask
import kotlin.time.Duration.Companion.days
import kotlinx.coroutines.GlobalScope
import nl.komponents.kovenant.Deferred import nl.komponents.kovenant.Deferred
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred import nl.komponents.kovenant.deferred
@ -28,9 +32,6 @@ import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.Util.SECURE_RANDOM import org.session.libsignal.utilities.Util.SECURE_RANDOM
import java.util.Timer
import java.util.TimerTask
import kotlin.time.Duration.Companion.days
private const val TAG = "Poller" private const val TAG = "Poller"
@ -44,8 +45,9 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
// region Settings // region Settings
companion object { companion object {
private const val retryInterval: Long = 2 * 1000 private const val RETRY_INTERVAL_MS: Long = 2 * 1000
private const val maxInterval: Long = 15 * 1000 private const val MAX_RETRY_INTERVAL_MS: Long = 15 * 1000
private const val NEXT_RETRY_MULTIPLIER: Float = 1.2f // If we fail to poll we multiply our current retry interval by this (up to the above max) then try again
} }
// endregion // endregion
@ -54,7 +56,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
if (hasStarted) { return } if (hasStarted) { return }
Log.d(TAG, "Started polling.") Log.d(TAG, "Started polling.")
hasStarted = true hasStarted = true
setUpPolling(retryInterval) setUpPolling(RETRY_INTERVAL_MS)
} }
fun stopIfNeeded() { fun stopIfNeeded() {
@ -67,9 +69,11 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
Log.d(TAG, "Retrieving user profile.") Log.d(TAG, "Retrieving user profile.")
SnodeAPI.getSwarm(userPublicKey).bind { SnodeAPI.getSwarm(userPublicKey).bind {
usedSnodes.clear() usedSnodes.clear()
deferred<Unit, Exception>().also { deferred<Unit, Exception>().also { exception ->
pollNextSnode(userProfileOnly = true, it) pollNextSnode(userProfileOnly = true, exception)
}.promise }.promise
}.fail { exception ->
Log.e(TAG, "Failed to retrieve user profile.", exception)
} }
} }
// endregion // endregion
@ -84,14 +88,14 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
pollNextSnode(deferred = deferred) pollNextSnode(deferred = deferred)
deferred.promise deferred.promise
}.success { }.success {
val nextDelay = if (isCaughtUp) retryInterval else 0 val nextDelay = if (isCaughtUp) RETRY_INTERVAL_MS else 0
Timer().schedule(object : TimerTask() { Timer().schedule(object : TimerTask() {
override fun run() { override fun run() {
thread.run { setUpPolling(retryInterval) } thread.run { setUpPolling(RETRY_INTERVAL_MS) }
} }
}, nextDelay) }, nextDelay)
}.fail { }.fail {
val nextDelay = minOf(maxInterval, (delay * 1.2).toLong()) val nextDelay = minOf(MAX_RETRY_INTERVAL_MS, (delay * NEXT_RETRY_MULTIPLIER).toLong())
Timer().schedule(object : TimerTask() { Timer().schedule(object : TimerTask() {
override fun run() { override fun run() {
thread.run { setUpPolling(nextDelay) } thread.run { setUpPolling(nextDelay) }

Loading…
Cancel
Save