diff --git a/app/build.gradle b/app/build.gradle index d72e1409f7..5fd4809d96 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,6 +43,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.paging:paging-runtime-ktx:$pagingVersion" implementation 'androidx.activity:activity-ktx:1.5.1' implementation 'androidx.fragment:fragment-ktx:1.5.3' diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index a280b8012c..8af45e7a92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -287,7 +287,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO if (poller != null) { poller.stopIfNeeded(); } - ClosedGroupPollerV2.getShared().stop(); + ClosedGroupPollerV2.getShared().stopAll(); } @Override @@ -467,11 +467,15 @@ public class ApplicationContext extends Application implements DefaultLifecycleO String token = task.getResult().getToken(); String userPublicKey = TextSecurePreferences.getLocalNumber(this); if (userPublicKey == null) return Unit.INSTANCE; - if (TextSecurePreferences.isUsingFCM(this)) { - LokiPushNotificationManager.register(token, userPublicKey, this, force); - } else { - LokiPushNotificationManager.unregister(token, this); - } + + AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { + if (TextSecurePreferences.isUsingFCM(this)) { + LokiPushNotificationManager.register(token, userPublicKey, this, force); + } else { + LokiPushNotificationManager.unregister(token, this); + } + }); + return Unit.INSTANCE; }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java index 53a909c5ac..b36fdb3ca1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java @@ -54,6 +54,7 @@ import com.google.android.material.tabs.TabLayout; import org.session.libsession.messaging.messages.control.DataExtractionNotification; import org.session.libsession.messaging.sending_receiving.MessageSender; +import org.session.libsession.snode.SnodeAPI; import org.session.libsession.utilities.Address; import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; import org.thoughtcrime.securesms.database.MediaDatabase; @@ -366,7 +367,7 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity { private void sendMediaSavedNotificationIfNeeded() { if (recipient.isGroupRecipient()) return; - DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis())); + DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(SnodeAPI.getNowWithOffset())); MessageSender.send(message, recipient.getAddress()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index b21c7dac8c..6544c2ab89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -60,6 +60,7 @@ import androidx.viewpager.widget.ViewPager; import org.session.libsession.messaging.messages.control.DataExtractionNotification; import org.session.libsession.messaging.sending_receiving.MessageSender; import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; +import org.session.libsession.snode.SnodeAPI; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Util; import org.session.libsession.utilities.recipients.Recipient; @@ -423,7 +424,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im .onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()) .onAllGranted(() -> { SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this); - long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis(); + long saveDate = (mediaItem.date > 0) ? mediaItem.date : SnodeAPI.getNowWithOffset(); saveTask.executeOnExecutor( AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null)); @@ -437,7 +438,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im private void sendMediaSavedNotificationIfNeeded() { if (conversationRecipient.isGroupRecipient()) return; - DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis())); + DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(SnodeAPI.getNowWithOffset())); MessageSender.send(message, conversationRecipient.getAddress()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java index 195c066d45..1ac4f8442b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java @@ -13,6 +13,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.session.libsession.snode.SnodeAPI; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView; import org.thoughtcrime.securesms.database.model.MessageRecord; @@ -106,7 +107,7 @@ public class ConversationItemFooter extends LinearLayout { messageRecord.getExpiresIn()); this.timerView.startAnimation(); - if (messageRecord.getExpireStarted() + messageRecord.getExpiresIn() <= System.currentTimeMillis()) { + if (messageRecord.getExpireStarted() + messageRecord.getExpiresIn() <= SnodeAPI.getNowWithOffset()) { ApplicationContext.getInstance(getContext()).getExpiringMessageManager().checkSchedule(); } } else if (!messageRecord.isOutgoing() && !messageRecord.isMediaPending()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index e8ec3de35c..37db70effe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -166,6 +166,7 @@ import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.SaveAttachmentTask import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.toPx +import java.lang.ref.WeakReference import java.util.Locale import java.util.concurrent.ExecutionException import java.util.concurrent.atomic.AtomicLong @@ -364,12 +365,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // messageIdToScroll messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1)) messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR)) - val thread = threadDb.getRecipientForThreadId(viewModel.threadId) - if (thread == null) { + val recipient = viewModel.recipient + val openGroup = recipient.let { viewModel.openGroup } + if (recipient == null || (recipient.isOpenGroupRecipient && openGroup == null)) { Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show() return finish() } - setUpRecyclerView() + setUpToolBar() setUpInputBar() setUpLinkPreviewObserver() @@ -396,22 +398,31 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } } } - unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId) + updateUnreadCountIndicator() - setUpTypingObserver() - setUpRecipientObserver() updateSubtitle() - getLatestOpenGroupInfoIfNeeded() setUpBlockedBanner() binding!!.searchBottomBar.setEventListener(this) - setUpSearchResultObserver() - scrollToFirstUnreadMessageIfNeeded() showOrHideInputIfNeeded() setUpMessageRequestsBar() - viewModel.recipient?.let { recipient -> - if (recipient.isOpenGroupRecipient && viewModel.openGroup == null) { - Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show() - return finish() + + val weakActivity = WeakReference(this) + + lifecycleScope.launch(Dispatchers.IO) { + unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId) + + // Note: We are accessing the `adapter` property because we want it to be loaded on + // the background thread to avoid blocking the UI thread and potentially hanging when + // transitioning to the activity + weakActivity.get()?.adapter ?: return@launch + + withContext(Dispatchers.Main) { + setUpRecyclerView() + setUpTypingObserver() + setUpRecipientObserver() + getLatestOpenGroupInfoIfNeeded() + setUpSearchResultObserver() + scrollToFirstUnreadMessageIfNeeded() } } @@ -709,6 +720,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // region Animation & Updating override fun onModified(recipient: Recipient) { + viewModel.updateRecipient() + runOnUiThread { val threadRecipient = viewModel.recipient ?: return@runOnUiThread Log.d("Loki-DBG", "Recipient no longer here, go back?") if (threadRecipient.isContactRecipient) { @@ -1063,7 +1076,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe storage.setExpirationTimer(thread.address.serialize(), expirationTime) val message = ExpirationTimerUpdate(expirationTime) message.recipient = thread.address.serialize() - message.sentTimestamp = System.currentTimeMillis() + message.sentTimestamp = SnodeAPI.nowWithOffset val expiringMessageManager = ApplicationContext.getInstance(this).expiringMessageManager expiringMessageManager.setExpirationTimer(message) MessageSender.send(message, thread.address) @@ -1192,7 +1205,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // Create the message val recipient = viewModel.recipient ?: return val reactionMessage = VisibleMessage() - val emojiTimestamp = System.currentTimeMillis() + val emojiTimestamp = SnodeAPI.nowWithOffset reactionMessage.sentTimestamp = emojiTimestamp val author = textSecurePreferences.getLocalNumber()!! // Put the message in the database @@ -1225,7 +1238,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private fun sendEmojiRemoval(emoji: String, originalMessage: MessageRecord) { val recipient = viewModel.recipient ?: return val message = VisibleMessage() - val emojiTimestamp = System.currentTimeMillis() + val emojiTimestamp = SnodeAPI.nowWithOffset message.sentTimestamp = emojiTimestamp val author = textSecurePreferences.getLocalNumber()!! reactionDb.deleteReaction(emoji, MessageId(originalMessage.id, originalMessage.isMms), author, false) @@ -1454,7 +1467,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } // Create the message val message = VisibleMessage() - message.sentTimestamp = System.currentTimeMillis() + message.sentTimestamp = SnodeAPI.nowWithOffset message.text = text val outgoingTextMessage = OutgoingTextMessage.from(message, recipient) // Clear the input bar @@ -1478,7 +1491,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe processMessageRequestApproval() // Create the message val message = VisibleMessage() - message.sentTimestamp = System.currentTimeMillis() + message.sentTimestamp = SnodeAPI.nowWithOffset message.text = body val quote = quotedMessage?.let { val quotedAttachments = (it as? MmsMessageRecord)?.slideDeck?.asAttachments() ?: listOf() @@ -1875,7 +1888,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private fun sendMediaSavedNotification() { val recipient = viewModel.recipient ?: return if (recipient.isGroupRecipient) { return } - val timestamp = System.currentTimeMillis() + val timestamp = SnodeAPI.nowWithOffset val kind = DataExtractionNotification.Kind.MediaSaved(timestamp) val message = DataExtractionNotification(kind) MessageSender.send(message, recipient.address) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index c7fc4d0056..5e4aa8d3a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -38,11 +38,17 @@ class ConversationViewModel( private val _uiState = MutableStateFlow(ConversationUiState(conversationExists = recipient != null)) val uiState: StateFlow = _uiState + private var _recipient: RetrieveOnce = RetrieveOnce { + repository.maybeGetRecipientForThreadId(threadId) + } val recipient: Recipient? - get() = repository.maybeGetRecipientForThreadId(threadId) + get() = _recipient.value + private var _openGroup: RetrieveOnce = RetrieveOnce { + storage.getOpenGroup(threadId) + } val openGroup: OpenGroup? - get() = storage.getOpenGroup(threadId) + get() = _openGroup.value val serverCapabilities: List get() = openGroup?.let { storage.getServerCapabilities(it.server) } ?: listOf() @@ -186,6 +192,10 @@ class ConversationViewModel( return repository.hasReceived(threadId) } + fun updateRecipient() { + _recipient.updateTo(repository.maybeGetRecipientForThreadId(threadId)) + } + @dagger.assisted.AssistedFactory interface AssistedFactory { fun create(threadId: Long, edKeyPair: KeyPair?, contentResolver: ContentResolver): Factory @@ -213,3 +223,19 @@ data class ConversationUiState( val isMessageRequestAccepted: Boolean? = null, val conversationExists: Boolean ) + +data class RetrieveOnce(val retrieval: () -> T?) { + private var triedToRetrieve: Boolean = false + private var _value: T? = null + + val value: T? + get() { + if (triedToRetrieve) { return _value } + + triedToRetrieve = true + _value = retrieval() + return _value + } + + fun updateTo(value: T?) { _value = value } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 104c9851b0..0938b21dd9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -10,6 +10,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SodiumUtilities +import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.ExpirationUtil import org.session.libsession.utilities.TextSecurePreferences @@ -88,7 +89,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { binding.expiresContainer.visibility = View.GONE } else { binding.expiresContainer.visibility = View.VISIBLE - val elapsed = System.currentTimeMillis() - messageRecord!!.expireStarted + val elapsed = SnodeAPI.nowWithOffset - messageRecord!!.expireStarted val remaining = messageRecord!!.expiresIn - elapsed val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 5805164623..1890e604a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -26,6 +26,7 @@ import network.loki.messenger.databinding.ViewVisibleMessageBinding import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact.ContactContext import org.session.libsession.messaging.open_groups.OpenGroupApi +import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.getColorFromAttr @@ -315,7 +316,7 @@ class VisibleMessageView : LinearLayout { if (message.expireStarted > 0) { binding.expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn) binding.expirationTimerView.startAnimation() - if (message.expireStarted + message.expiresIn <= System.currentTimeMillis()) { + if (message.expireStarted + message.expiresIn <= SnodeAPI.nowWithOffset) { ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule() } } else if (!message.isMediaPending) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt index c6ba12c087..eb42de5363 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -33,6 +33,7 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel +import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.UNKNOWN import org.session.libsession.utilities.Address.Companion.fromExternal @@ -340,7 +341,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa } override fun markExpireStarted(messageId: Long) { - markExpireStarted(messageId, System.currentTimeMillis()) + markExpireStarted(messageId, SnodeAPI.nowWithOffset) } override fun markExpireStarted(messageId: Long, startedTimestamp: Long) { @@ -731,7 +732,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa // In open groups messages should be sorted by their server timestamp var receivedTimestamp = serverTimestamp if (serverTimestamp == 0L) { - receivedTimestamp = System.currentTimeMillis() + receivedTimestamp = SnodeAPI.nowWithOffset } contentValues.put(DATE_RECEIVED, receivedTimestamp) contentValues.put(SUBSCRIPTION_ID, message.subscriptionId) @@ -1210,7 +1211,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val slideDeck = SlideDeck(context, message!!.attachments) return MediaMmsMessageRecord( id, message.recipient, message.recipient, - 1, System.currentTimeMillis(), System.currentTimeMillis(), + 1, SnodeAPI.nowWithOffset, SnodeAPI.nowWithOffset, 0, threadId, message.body, slideDeck, slideDeck.slides.size, if (message.isSecure) MmsSmsColumns.Types.getOutgoingEncryptedMessageType() else MmsSmsColumns.Types.getOutgoingSmsMessageType(), @@ -1218,7 +1219,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa LinkedList(), message.subscriptionId, message.expiresIn, - System.currentTimeMillis(), 0, + SnodeAPI.nowWithOffset, 0, if (message.outgoingQuote != null) Quote( message.outgoingQuote!!.id, message.outgoingQuote!!.author, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index a1cb7fc556..542e56b43d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -36,6 +36,7 @@ import org.session.libsession.messaging.calls.CallMessageType; import org.session.libsession.messaging.messages.signal.IncomingGroupMessage; import org.session.libsession.messaging.messages.signal.IncomingTextMessage; import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; +import org.session.libsession.snode.SnodeAPI; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.IdentityKeyMismatch; import org.session.libsession.utilities.IdentityKeyMismatchList; @@ -223,7 +224,7 @@ public class SmsDatabase extends MessagingDatabase { @Override public void markExpireStarted(long id) { - markExpireStarted(id, System.currentTimeMillis()); + markExpireStarted(id, SnodeAPI.getNowWithOffset()); } @Override @@ -525,7 +526,7 @@ public class SmsDatabase extends MessagingDatabase { contentValues.put(ADDRESS, address.serialize()); contentValues.put(THREAD_ID, threadId); contentValues.put(BODY, message.getMessageBody()); - contentValues.put(DATE_RECEIVED, System.currentTimeMillis()); + contentValues.put(DATE_RECEIVED, SnodeAPI.getNowWithOffset()); contentValues.put(DATE_SENT, message.getSentTimestampMillis()); contentValues.put(READ, 1); contentValues.put(TYPE, type); @@ -760,11 +761,11 @@ public class SmsDatabase extends MessagingDatabase { public MessageRecord getCurrent() { return new SmsMessageRecord(id, message.getMessageBody(), message.getRecipient(), message.getRecipient(), - System.currentTimeMillis(), System.currentTimeMillis(), + SnodeAPI.getNowWithOffset(), SnodeAPI.getNowWithOffset(), 0, message.isSecureMessage() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(), threadId, 0, new LinkedList(), message.getExpiresIn(), - System.currentTimeMillis(), 0, false, Collections.emptyList(), false); + SnodeAPI.getNowWithOffset(), 0, false, Collections.emptyList(), false); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 7375e6096f..6244763661 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -156,7 +156,7 @@ public class ThreadDatabase extends Database { private long createThreadForRecipient(Address address, boolean group, int distributionType) { ContentValues contentValues = new ContentValues(4); - long date = System.currentTimeMillis(); + long date = SnodeAPI.getNowWithOffset(); contentValues.put(DATE, date - date % 1000); contentValues.put(ADDRESS, address.serialize()); @@ -335,7 +335,7 @@ public class ThreadDatabase extends Database { contentValues.put(UNREAD_MENTION_COUNT, 0); if (lastSeen) { - contentValues.put(LAST_SEEN, System.currentTimeMillis()); + contentValues.put(LAST_SEEN, SnodeAPI.getNowWithOffset()); } SQLiteDatabase db = databaseHelper.getWritableDatabase(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java index 884d6d7bed..ddff9f52b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java @@ -30,6 +30,7 @@ import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; import org.session.libsession.messaging.messages.visible.VisibleMessage; import org.session.libsession.messaging.sending_receiving.MessageSender; +import org.session.libsession.snode.SnodeAPI; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.Log; @@ -82,7 +83,7 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver { VisibleMessage message = new VisibleMessage(); message.setText(responseText.toString()); - message.setSentTimestamp(System.currentTimeMillis()); + message.setSentTimestamp(SnodeAPI.getNowWithOffset()); MessageSender.send(message, recipient.getAddress()); if (recipient.isGroupRecipient()) { @@ -96,7 +97,7 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver { } else { Log.w("AndroidAutoReplyReceiver", "Sending regular message "); OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient); - DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(replyThreadId, reply, false, System.currentTimeMillis(), null, true); + DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(replyThreadId, reply, false, SnodeAPI.getNowWithOffset(), null, true); } List messageIds = DatabaseComponent.get(context).threadDatabase().setRead(replyThreadId, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt index 9b8ea5824d..e583fb0ca5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt @@ -4,9 +4,11 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.work.Constraints +import androidx.work.Data import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.Worker import androidx.work.WorkerParameters @@ -21,19 +23,35 @@ import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPolle import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.recover import org.thoughtcrime.securesms.dependencies.DatabaseComponent import java.util.concurrent.TimeUnit class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { + enum class Targets { + DMS, CLOSED_GROUPS, OPEN_GROUPS + } companion object { const val TAG = "BackgroundPollWorker" + const val INITIAL_SCHEDULE_TIME = "INITIAL_SCHEDULE_TIME" + const val REQUEST_TARGETS = "REQUEST_TARGETS" @JvmStatic - fun schedulePeriodic(context: Context) { + fun schedulePeriodic(context: Context) = schedulePeriodic(context, targets = Targets.values()) + + @JvmStatic + fun schedulePeriodic(context: Context, targets: Array) { Log.v(TAG, "Scheduling periodic work.") - val builder = PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES) + val durationMinutes: Long = 15 + val builder = PeriodicWorkRequestBuilder(durationMinutes, TimeUnit.MINUTES) builder.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) + + val dataBuilder = Data.Builder() + dataBuilder.putLong(INITIAL_SCHEDULE_TIME, System.currentTimeMillis() + (durationMinutes * 60 * 1000)) + dataBuilder.putStringArray(REQUEST_TARGETS, targets.map { it.name }.toTypedArray()) + builder.setInputData(dataBuilder.build()) + val workRequest = builder.build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( TAG, @@ -41,6 +59,20 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor workRequest ) } + + @JvmStatic + fun scheduleOnce(context: Context, targets: Array = Targets.values()) { + Log.v(TAG, "Scheduling single run.") + val builder = OneTimeWorkRequestBuilder() + builder.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) + + val dataBuilder = Data.Builder() + dataBuilder.putStringArray(REQUEST_TARGETS, targets.map { it.name }.toTypedArray()) + builder.setInputData(dataBuilder.build()) + + val workRequest = builder.build() + WorkManager.getInstance(context).enqueue(workRequest) + } } override fun doWork(): Result { @@ -49,41 +81,89 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor return Result.failure() } + // If this is a scheduled run and it is happening before the initial scheduled time (as + // periodic background tasks run immediately when scheduled) then don't actually do anything + // because this might slow requests on initial startup or triggered by PNs + val initialScheduleTime = inputData.getLong(INITIAL_SCHEDULE_TIME, -1) + + if (initialScheduleTime != -1L && System.currentTimeMillis() < (initialScheduleTime - (60 * 1000))) { + Log.v(TAG, "Skipping initial run.") + return Result.success() + } + + // Retrieve the desired targets (defaulting to all if not provided or empty) + val requestTargets: List = (inputData.getStringArray(REQUEST_TARGETS) ?: emptyArray()) + .map { + try { Targets.valueOf(it) } + catch(e: Exception) { null } + } + .filterNotNull() + .ifEmpty { Targets.values().toList() } + try { - Log.v(TAG, "Performing background poll.") + Log.v(TAG, "Performing background poll for ${requestTargets.joinToString { it.name }}.") val promises = mutableListOf>() // DMs - val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - val dmsPromise = SnodeAPI.getMessages(userPublicKey).bind { envelopes -> - val params = envelopes.map { (envelope, serverHash) -> - // FIXME: Using a job here seems like a bad idea... - MessageReceiveParameters(envelope.toByteArray(), serverHash, null) + var dmsPromise: Promise = Promise.ofSuccess(Unit) + + if (requestTargets.contains(Targets.DMS)) { + val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! + dmsPromise = SnodeAPI.getMessages(userPublicKey).bind { envelopes -> + val params = envelopes.map { (envelope, serverHash) -> + // FIXME: Using a job here seems like a bad idea... + MessageReceiveParameters(envelope.toByteArray(), serverHash, null) + } + BatchMessageReceiveJob(params).executeAsync("background") } - BatchMessageReceiveJob(params).executeAsync("background") + promises.add(dmsPromise) } - promises.add(dmsPromise) // Closed groups - val closedGroupPoller = ClosedGroupPollerV2() // Intentionally don't use shared - val storage = MessagingModuleConfiguration.shared.storage - val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys() - allGroupPublicKeys.iterator().forEach { closedGroupPoller.poll(it) } + if (requestTargets.contains(Targets.CLOSED_GROUPS)) { + val closedGroupPoller = ClosedGroupPollerV2() // Intentionally don't use shared + val storage = MessagingModuleConfiguration.shared.storage + val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys() + allGroupPublicKeys.iterator().forEach { closedGroupPoller.poll(it) } + } // Open Groups - val threadDB = DatabaseComponent.get(context).lokiThreadDatabase() - val openGroups = threadDB.getAllOpenGroups() - val openGroupServers = openGroups.map { it.value.server }.toSet() + var ogPollError: Exception? = null - for (server in openGroupServers) { - val poller = OpenGroupPoller(server, null) - poller.hasStarted = true - promises.add(poller.poll()) + if (requestTargets.contains(Targets.OPEN_GROUPS)) { + val threadDB = DatabaseComponent.get(context).lokiThreadDatabase() + val openGroups = threadDB.getAllOpenGroups() + val openGroupServers = openGroups.map { it.value.server }.toSet() + + for (server in openGroupServers) { + val poller = OpenGroupPoller(server, null) + poller.hasStarted = true + + // If one of the open group pollers fails we don't want it to cancel the DM + // poller so just hold on to the error for later + promises.add( + poller.poll().recover { + if (dmsPromise.isDone()) { + throw it + } + + ogPollError = it + } + ) + } } // Wait until all the promises are resolved all(promises).get() + // If the Open Group pollers threw an exception then re-throw it here (now that + // the DM promise has completed) + val localOgPollException = ogPollError + + if (localOgPollException != null) { + throw localOgPollException + } + return Result.success() } catch (exception: Exception) { Log.e(TAG, "Background poll failed due to error: ${exception.message}.", exception) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index 7284629033..327492e95c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -44,6 +44,7 @@ import org.session.libsession.messaging.open_groups.OpenGroup; import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; import org.session.libsession.messaging.utilities.SessionId; import org.session.libsession.messaging.utilities.SodiumUtilities; +import org.session.libsession.snode.SnodeAPI; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Contact; import org.session.libsession.utilities.ServiceUtil; @@ -137,7 +138,7 @@ public class DefaultMessageNotifier implements MessageNotifier { Intent intent = new Intent(context, ConversationActivityV2.class); intent.putExtra(ConversationActivityV2.ADDRESS, recipient.getAddress()); intent.putExtra(ConversationActivityV2.THREAD_ID, threadId); - intent.setData((Uri.parse("custom://" + System.currentTimeMillis()))); + intent.setData((Uri.parse("custom://" + SnodeAPI.getNowWithOffset()))); FailedNotificationBuilder builder = new FailedNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context), intent); ((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index c1f8bae62f..f15ce9c11c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -82,7 +82,7 @@ public class MarkReadReceiver extends BroadcastReceiver { List timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList(); if (!SessionMetaProtocol.shouldSendReadReceipt(Recipient.from(context, address, false))) { continue; } ReadReceipt readReceipt = new ReadReceipt(timestamps); - readReceipt.setSentTimestamp(System.currentTimeMillis()); + readReceipt.setSentTimestamp(SnodeAPI.getNowWithOffset()); MessageSender.send(readReceipt, address); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt index 4e08a4510a..c0400b720b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt @@ -115,7 +115,7 @@ class DefaultConversationRepository @Inject constructor( val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return for (contact in contacts) { val message = VisibleMessage() - message.sentTimestamp = System.currentTimeMillis() + message.sentTimestamp = SnodeAPI.nowWithOffset val openGroupInvitation = OpenGroupInvitation() openGroupInvitation.name = openGroup.name openGroupInvitation.url = openGroup.joinURL diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java b/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java index 0f5a3f17a6..a56bc8c0de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java @@ -9,6 +9,7 @@ import android.widget.Toast; import network.loki.messenger.R; import org.session.libsession.messaging.messages.visible.VisibleMessage; +import org.session.libsession.snode.SnodeAPI; import org.session.libsession.utilities.Address; import org.session.libsignal.utilities.Log; import org.session.libsession.messaging.sending_receiving.MessageSender; @@ -50,7 +51,7 @@ public class QuickResponseService extends IntentService { if (!TextUtils.isEmpty(content)) { VisibleMessage message = new VisibleMessage(); message.setText(content); - message.setSentTimestamp(System.currentTimeMillis()); + message.setSentTimestamp(SnodeAPI.getNowWithOffset()); MessageSender.send(message, Address.fromExternal(this, number)); } } catch (URISyntaxException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt index d09933ab8c..b5e9b78a74 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt @@ -1,6 +1,6 @@ package org.thoughtcrime.securesms.service -import android.app.Service +import android.app.ForegroundServiceStartNotAllowedException import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -17,6 +17,8 @@ import android.telephony.PhoneStateListener.LISTEN_NONE import android.telephony.TelephonyManager import androidx.core.content.ContextCompat import androidx.core.os.bundleOf +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import dagger.hilt.android.AndroidEntryPoint import org.session.libsession.messaging.calls.CallMessageType @@ -25,6 +27,7 @@ import org.session.libsession.utilities.FutureTaskListener import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.calls.WebRtcCallActivity +import org.thoughtcrime.securesms.notifications.BackgroundPollWorker import org.thoughtcrime.securesms.util.CallNotificationBuilder import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_ESTABLISHED import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_CONNECTING @@ -46,7 +49,7 @@ import javax.inject.Inject import org.thoughtcrime.securesms.webrtc.data.State as CallState @AndroidEntryPoint -class WebRtcCallService : Service(), CallManager.WebRtcListener { +class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener { companion object { @@ -238,7 +241,10 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { scheduledReconnect?.cancel(false) scheduledTimeout = null scheduledReconnect = null - stopForeground(true) + + lifecycleScope.launchWhenCreated { + stopForeground(true) + } } private fun isSameCall(intent: Intent): Boolean { @@ -253,7 +259,9 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { private fun isIdle() = callManager.isIdle() - override fun onBind(intent: Intent?): IBinder? = null + override fun onBind(intent: Intent): IBinder? { + return super.onBind(intent) + } override fun onHangup() { serviceExecutor.execute { @@ -272,7 +280,8 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { if (intent == null || intent.action == null) return START_NOT_STICKY serviceExecutor.execute { val action = intent.action - Log.i("Loki", "Handling ${intent.action}") + val callId = ((intent.getSerializableExtra(EXTRA_CALL_ID) as? UUID)?.toString() ?: "No callId") + Log.i("Loki", "Handling ${intent.action} for call: ${callId}") when { action == ACTION_INCOMING_RING && isSameCall(intent) && callManager.currentConnectionState == CallState.Reconnecting -> handleNewOffer( intent @@ -361,7 +370,9 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { insertMissedCall(recipient, false) if (callState == CallState.Idle) { - stopForeground(true) + lifecycleScope.launchWhenCreated { + stopForeground(true) + } } } @@ -409,6 +420,11 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { callManager.initializeAudioForCall() callManager.startIncomingRinger() callManager.setAudioEnabled(true) + + BackgroundPollWorker.scheduleOnce( + this, + arrayOf(BackgroundPollWorker.Targets.DMS) + ) } } @@ -573,7 +589,9 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { private fun handleRemoteHangup(intent: Intent) { if (callManager.callId != getCallId(intent)) { Log.e(TAG, "Hangup for non-active call...") - stopForeground(true) + lifecycleScope.launchWhenCreated { + stopForeground(true) + } return } @@ -717,10 +735,16 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { } private fun setCallInProgressNotification(type: Int, recipient: Recipient?) { - startForeground( - CallNotificationBuilder.WEBRTC_NOTIFICATION, - CallNotificationBuilder.getCallInProgressNotification(this, type, recipient) - ) + try { + startForeground( + CallNotificationBuilder.WEBRTC_NOTIFICATION, + CallNotificationBuilder.getCallInProgressNotification(this, type, recipient) + ) + } + catch(e: ForegroundServiceStartNotAllowedException) { + Log.e(TAG, "Failed to setCallInProgressNotification as a foreground service for type: ${type}, trying to update instead") + } + if (!CallNotificationBuilder.areNotificationsEnabled(this) && type == TYPE_INCOMING_PRE_OFFER) { // start an intent for the fullscreen val foregroundIntent = Intent(this, WebRtcCallActivity::class.java) @@ -769,10 +793,14 @@ class WebRtcCallService : Service(), CallManager.WebRtcListener { callReceiver?.let { receiver -> unregisterReceiver(receiver) } + wiredHeadsetStateReceiver?.let { unregisterReceiver(it) } + powerButtonReceiver?.let { unregisterReceiver(it) } networkChangedReceiver?.unregister(this) wantsToAnswerReceiver?.let { receiver -> LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) } + powerButtonReceiver = null + wiredHeadsetStateReceiver = null networkChangedReceiver = null callReceiver = null uncaughtExceptionHandlerManager?.unregister() diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt index b7a9b6fd65..07e73bedd2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt @@ -18,6 +18,7 @@ import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.messages.control.CallMessage import org.session.libsession.messaging.sending_receiving.MessageSender +import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.Debouncer import org.session.libsession.utilities.Util @@ -574,7 +575,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va } } - fun insertCallMessage(threadPublicKey: String, callMessageType: CallMessageType, signal: Boolean = false, sentTimestamp: Long = System.currentTimeMillis()) { + fun insertCallMessage(threadPublicKey: String, callMessageType: CallMessageType, signal: Boolean = false, sentTimestamp: Long = SnodeAPI.nowWithOffset) { storage.insertCallMessage(threadPublicKey, callMessageType, sentTimestamp) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt index bb41c7c971..3d40b5f746 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.webrtc import android.app.NotificationManager import android.content.Context +import android.content.Intent import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope @@ -32,6 +33,20 @@ class CallMessageProcessor(private val context: Context, private val textSecureP companion object { private const val VERY_EXPIRED_TIME = 15 * 60 * 1000L + + fun safeStartService(context: Context, intent: Intent) { + // If the foreground service crashes then it's possible for one of these intents to + // be started in the background (in which case 'startService' will throw a + // 'BackgroundServiceStartNotAllowedException' exception) so catch that case and try + // to re-start the service in the foreground + try { context.startService(intent) } + catch(e: Exception) { + try { ContextCompat.startForegroundService(context, intent) } + catch (e2: Exception) { + Log.e("Loki", "Unable to start CallMessage intent: ${e2.message}") + } + } + } } init { @@ -90,7 +105,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP private fun incomingHangup(callMessage: CallMessage) { val callId = callMessage.callId ?: return val hangupIntent = WebRtcCallService.remoteHangupIntent(context, callId) - context.startService(hangupIntent) + safeStartService(context, hangupIntent) } private fun incomingAnswer(callMessage: CallMessage) { @@ -103,7 +118,8 @@ class CallMessageProcessor(private val context: Context, private val textSecureP sdp = sdp, callId = callId ) - context.startService(answerIntent) + + safeStartService(context, answerIntent) } private fun handleIceCandidates(callMessage: CallMessage) { @@ -119,7 +135,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP callId = callId, address = Address.fromSerialized(sender) ) - context.startService(iceIntent) + safeStartService(context, iceIntent) } private fun incomingPreOffer(callMessage: CallMessage) { @@ -132,7 +148,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP callId = callId, callTime = callMessage.sentTimestamp!! ) - context.startService(incomingIntent) + safeStartService(context, incomingIntent) } private fun incomingCall(callMessage: CallMessage) { @@ -146,7 +162,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP callId = callId, callTime = callMessage.sentTimestamp!! ) - context.startService(incomingIntent) + safeStartService(context, incomingIntent) } private fun CallMessage.iceCandidates(): List { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index 9e876fd329..ec70a940b9 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -98,19 +98,19 @@ class BatchMessageReceiveJob( } catch (e: Exception) { when (e) { is MessageReceiver.Error.DuplicateMessage, MessageReceiver.Error.SelfSend -> { - Log.i(TAG, "Couldn't receive message, failed with error: ${e.message}") + Log.i(TAG, "Couldn't receive message, failed with error: ${e.message} (id: $id)") } is MessageReceiver.Error -> { if (!e.isRetryable) { - Log.e(TAG, "Couldn't receive message, failed permanently", e) + Log.e(TAG, "Couldn't receive message, failed permanently (id: $id)", e) } else { - Log.e(TAG, "Couldn't receive message, failed", e) + Log.e(TAG, "Couldn't receive message, failed (id: $id)", e) failures += messageParameters } } else -> { - Log.e(TAG, "Couldn't receive message, failed", e) + Log.e(TAG, "Couldn't receive message, failed (id: $id)", e) failures += messageParameters } } @@ -166,11 +166,11 @@ class BatchMessageReceiveJob( else -> MessageReceiver.handle(message, proto, openGroupID) } } catch (e: Exception) { - Log.e(TAG, "Couldn't process message.", e) + Log.e(TAG, "Couldn't process message (id: $id)", e) if (e is MessageReceiver.Error && !e.isRetryable) { - Log.e(TAG, "Message failed permanently",e) + Log.e(TAG, "Message failed permanently (id: $id)", e) } else { - Log.e(TAG, "Message failed",e) + Log.e(TAG, "Message failed (id: $id)", e) failures += parameters } } @@ -199,12 +199,12 @@ class BatchMessageReceiveJob( } private fun handleSuccess(dispatcherName: String) { - Log.i(TAG, "Completed processing of ${messages.size} messages") + Log.i(TAG, "Completed processing of ${messages.size} messages (id: $id)") this.delegate?.handleJobSucceeded(this, dispatcherName) } private fun handleFailure(dispatcherName: String) { - Log.i(TAG, "Handling failure of ${failures.size} messages (${messages.size - failures.size} processed successfully)") + Log.i(TAG, "Handling failure of ${failures.size} messages (${messages.size - failures.size} processed successfully) (id: $id)") this.delegate?.handleJobFailed(this, dispatcherName, Exception("One or more jobs resulted in failure")) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt index 476798ff5e..7ce995195f 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt @@ -3,6 +3,7 @@ package org.session.libsession.messaging.jobs import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.utilities.Data +import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.GroupUtil class GroupAvatarDownloadJob(val server: String, val room: String, val imageId: String?) : Job { @@ -43,7 +44,7 @@ class GroupAvatarDownloadJob(val server: String, val room: String, val imageId: val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray()) storage.updateProfilePicture(groupId, bytes) - storage.updateTimestampUpdated(groupId, System.currentTimeMillis()) + storage.updateTimestampUpdated(groupId, SnodeAPI.nowWithOffset) delegate?.handleJobSucceeded(this, dispatcherName) } catch (e: Exception) { delegate?.handleJobFailed(this, dispatcherName, e) diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt index 46eff4b03b..ca60fd3cbf 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt @@ -23,6 +23,7 @@ import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionResponse +import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Base64.decode import org.session.libsignal.utilities.Base64.encodeBytes @@ -303,7 +304,7 @@ object OpenGroupApi { val headers = request.headers.toMutableMap() if (request.isAuthRequired) { val nonce = sodium.nonce(16) - val timestamp = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + val timestamp = TimeUnit.MILLISECONDS.toSeconds(SnodeAPI.nowWithOffset) var pubKey = "" var signature = ByteArray(Sign.BYTES) var bodyHash = ByteArray(0) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index a97e83a2de..7cb5b95b11 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -15,6 +15,7 @@ import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SodiumUtilities +import org.session.libsession.snode.SnodeAPI import org.session.libsignal.crypto.PushTransportDetails import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.utilities.IdPrefix @@ -156,7 +157,7 @@ object MessageReceiver { message.sender = sender message.recipient = userPublicKey message.sentTimestamp = envelope.timestamp - message.receivedTimestamp = if (envelope.hasServerTimestamp()) envelope.serverTimestamp else System.currentTimeMillis() + message.receivedTimestamp = if (envelope.hasServerTimestamp()) envelope.serverTimestamp else SnodeAPI.nowWithOffset message.groupPublicKey = groupPublicKey message.openGroupServerMessageID = openGroupServerID // Validate diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index 8b97b50dff..b5cfdd9264 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -199,7 +199,20 @@ object MessageSender { val hash = it["hash"] as? String message.serverHash = hash handleSuccessfulMessageSend(message, destination, isSyncMessage) - val shouldNotify = ((message is VisibleMessage || message is UnsendRequest || message is CallMessage) && !isSyncMessage) + + val shouldNotify: Boolean = when (message) { + is VisibleMessage, is UnsendRequest -> !isSyncMessage + is CallMessage -> { + // Note: Other 'CallMessage' types are too big to send as push notifications + // so only send the 'preOffer' message as a notification + when (message.type) { + SignalServiceProtos.CallMessage.Type.PRE_OFFER -> true + else -> false + } + } + else -> false + } + /* if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) { shouldNotify = true @@ -229,7 +242,7 @@ object MessageSender { val deferred = deferred() val storage = MessagingModuleConfiguration.shared.storage if (message.sentTimestamp == null) { - message.sentTimestamp = System.currentTimeMillis() + message.sentTimestamp = SnodeAPI.nowWithOffset } val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!! var serverCapabilities = listOf() diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt index d3c9effd35..89c5d68340 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt @@ -50,11 +50,11 @@ fun MessageSender.create(name: String, members: Collection): Promise) val name = group.title // Send the update to the group val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersAdded(newMembersAsData) - val sentTime = System.currentTimeMillis() + val sentTime = SnodeAPI.nowWithOffset val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind) closedGroupControlMessage.sentTimestamp = sentTime send(closedGroupControlMessage, Address.fromSerialized(groupID)) @@ -151,7 +151,7 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List) // updates from before that timestamp. By setting the timestamp of the message below to a value // greater than that of the `MembersAdded` message, we ensure that newly added members ignore // the `MembersAdded` message. - closedGroupControlMessage.sentTimestamp = System.currentTimeMillis() + closedGroupControlMessage.sentTimestamp = SnodeAPI.nowWithOffset send(closedGroupControlMessage, Address.fromSerialized(member)) } // Notify the user @@ -192,7 +192,7 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List @@ -43,13 +44,27 @@ object OnionRequestAPI { private val snodeFailureCount = mutableMapOf() var guardSnodes = setOf() + var _paths: AtomicReference?> = AtomicReference(null) var paths: List // Not a set to ensure we consistently show the same path to the user - get() = database.getOnionRequestPaths() + get() { + val paths = _paths.get() + + if (paths != null) { return paths } + + // Storing this in an atomic variable as it was causing a number of background + // ANRs when this value was accessed via the main thread after tapping on + // a notification) + val result = database.getOnionRequestPaths() + _paths.set(result) + return result + } set(newValue) { if (newValue.isEmpty()) { database.clearOnionRequestPaths() + _paths.set(null) } else { database.setOnionRequestPaths(newValue) + _paths.set(newValue) } } diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index ce09ff31f1..08b028085a 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -57,7 +57,7 @@ object SnodeAPI { internal var clockOffset = 0L @JvmStatic - val nowWithOffset + public val nowWithOffset get() = System.currentTimeMillis() + clockOffset internal var forkInfo by observable(database.getForkInfo()) { _, oldValue, newValue -> @@ -590,7 +590,7 @@ object SnodeAPI { val parameters = message.toJSON().toMutableMap() // Construct signature if (requiresAuth) { - val sigTimestamp = System.currentTimeMillis() + SnodeAPI.clockOffset + val sigTimestamp = nowWithOffset val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString val signature = ByteArray(Sign.BYTES) // assume namespace here is non-zero, as zero namespace doesn't require auth diff --git a/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt b/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt index cba7b1f16d..d9a86f1745 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt @@ -26,7 +26,7 @@ class SSKEnvironment( interface ProfileManagerProtocol { companion object { - const val NAME_PADDED_LENGTH = 26 + const val NAME_PADDED_LENGTH = 64 } fun setNickname(context: Context, recipient: Recipient, nickname: String?) @@ -55,4 +55,4 @@ class SSKEnvironment( shared = SSKEnvironment(typingIndicators, readReceiptManager, profileManager, notificationManager, messageExpirationManager) } } -} \ No newline at end of file +} diff --git a/libsignal/src/main/java/org/session/libsignal/messages/SignalServiceDataMessage.java b/libsignal/src/main/java/org/session/libsignal/messages/SignalServiceDataMessage.java index de9d8d9a31..8f908284eb 100644 --- a/libsignal/src/main/java/org/session/libsignal/messages/SignalServiceDataMessage.java +++ b/libsignal/src/main/java/org/session/libsignal/messages/SignalServiceDataMessage.java @@ -319,8 +319,8 @@ public class SignalServiceDataMessage { return this; } - public SignalServiceDataMessage build() { - if (timestamp == 0) timestamp = System.currentTimeMillis(); + public SignalServiceDataMessage build(long fallbackTimestamp) { + if (timestamp == 0) timestamp = fallbackTimestamp; // closedGroupUpdate is always null because we don't use SignalServiceDataMessage to send them (we use ClosedGroupUpdateMessageSendJob) return new SignalServiceDataMessage(timestamp, group, attachments, body, expiresInSeconds, expirationUpdate, profileKey, quote, sharedContacts, previews, null, syncTarget);