Merge remote-tracking branch 'upstream/dev' into feature/unread-mention-indicator

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt
#	libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt
This commit is contained in:
Morgan Pretty 2023-01-24 14:02:08 +11:00
commit dc9458f313
28 changed files with 330 additions and 41 deletions

View File

@ -47,6 +47,7 @@ import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.WindowDebouncer; import org.session.libsession.utilities.WindowDebouncer;
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper; import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
import org.session.libsession.utilities.dynamiclanguage.LocaleParser; import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
import org.session.libsignal.utilities.HTTP;
import org.session.libsignal.utilities.JsonUtil; import org.session.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.ThreadUtils; import org.session.libsignal.utilities.ThreadUtils;
@ -67,6 +68,7 @@ import org.thoughtcrime.securesms.groups.OpenGroupMigrator;
import org.thoughtcrime.securesms.home.HomeActivity; import org.thoughtcrime.securesms.home.HomeActivity;
import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer; import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.jobs.FastJobStorage; import org.thoughtcrime.securesms.jobs.FastJobStorage;
import org.thoughtcrime.securesms.jobs.JobManagerFactories; import org.thoughtcrime.securesms.jobs.JobManagerFactories;
import org.thoughtcrime.securesms.logging.AndroidLogger; import org.thoughtcrime.securesms.logging.AndroidLogger;
@ -237,6 +239,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
resubmitProfilePictureIfNeeded(); resubmitProfilePictureIfNeeded();
loadEmojiSearchIndexIfNeeded(); loadEmojiSearchIndexIfNeeded();
EmojiSource.refresh(); EmojiSource.refresh();
NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create();
HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
} }
@Override @Override

View File

@ -176,6 +176,11 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
return messageDB.getMessageID(serverId, threadId) return messageDB.getMessageID(serverId, threadId)
} }
override fun getMessageIDs(serverIds: List<Long>, threadId: Long): Pair<List<Long>, List<Long>> {
val messageDB = DatabaseComponent.get(context).lokiMessageDatabase()
return messageDB.getMessageIDs(serverIds, threadId)
}
override fun deleteMessage(messageID: Long, isSms: Boolean) { override fun deleteMessage(messageID: Long, isSms: Boolean) {
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase() val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase() else DatabaseComponent.get(context).mmsDatabase()
@ -184,6 +189,15 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID) DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID)
} }
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase()
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs)
}
override fun updateMessageAsDeleted(timestamp: Long, author: String): Long? { override fun updateMessageAsDeleted(timestamp: Long, author: String): Long? {
val database = DatabaseComponent.get(context).mmsSmsDatabase() val database = DatabaseComponent.get(context).mmsSmsDatabase()
val address = Address.fromSerialized(author) val address = Address.fromSerialized(author)

View File

@ -13,6 +13,11 @@ class ScreenshotObserver(private val context: Context, handler: Handler, private
override fun onChange(selfChange: Boolean, uri: Uri?) { override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri) super.onChange(selfChange, uri)
uri ?: return uri ?: return
// There is an odd bug where we can get notified for changes to 'content://media/external'
// directly which is a protected folder, this code is to prevent that crash
if (uri.scheme == "content" && uri.host == "media" && uri.path == "/external") { return }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
queryRelativeDataColumn(uri) queryRelativeDataColumn(uri)
} else { } else {

View File

@ -315,13 +315,26 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
restoreDraftIfNeeded() restoreDraftIfNeeded()
setUpUiStateObserver() setUpUiStateObserver()
binding!!.scrollToBottomButton.setOnClickListener { binding!!.scrollToBottomButton.setOnClickListener {
val layoutManager = binding?.conversationRecyclerView?.layoutManager ?: return@setOnClickListener val layoutManager = (binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager) ?: return@setOnClickListener
if (layoutManager.isSmoothScrolling) { if (layoutManager.isSmoothScrolling) {
binding?.conversationRecyclerView?.scrollToPosition(0) binding?.conversationRecyclerView?.scrollToPosition(0)
} else { } else {
// It looks like 'smoothScrollToPosition' will actually load all intermediate items in
// order to do the scroll, this can be very slow if there are a lot of messages so
// instead we check the current position and if there are more than 10 items to scroll
// we jump instantly to the 10th item and scroll from there (this should happen quick
// enough to give a similar scroll effect without having to load everything)
val position = layoutManager.findFirstVisibleItemPosition()
if (position > 10) {
binding?.conversationRecyclerView?.scrollToPosition(10)
}
binding?.conversationRecyclerView?.post {
binding?.conversationRecyclerView?.smoothScrollToPosition(0) binding?.conversationRecyclerView?.smoothScrollToPosition(0)
} }
} }
}
unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId) unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
updateUnreadCountIndicator() updateUnreadCountIndicator()
setUpTypingObserver() setUpTypingObserver()

View File

@ -35,6 +35,7 @@ import com.bumptech.glide.Glide;
import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
@ -318,6 +319,28 @@ public class AttachmentDatabase extends Database {
notifyAttachmentListeners(); notifyAttachmentListeners();
} }
@SuppressWarnings("ResultOfMethodCallIgnored")
void deleteAttachmentsForMessages(long[] mmsIds) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
Cursor cursor = null;
String mmsIdString = StringUtils.join(mmsIds, ',');
try {
cursor = database.query(TABLE_NAME, new String[] {DATA, THUMBNAIL, CONTENT_TYPE}, MMS_ID + " IN (?)",
new String[] {mmsIdString}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
deleteAttachmentOnDisk(cursor.getString(0), cursor.getString(1), cursor.getString(2));
}
} finally {
if (cursor != null)
cursor.close();
}
database.delete(TABLE_NAME, MMS_ID + " IN (?)", new String[] {mmsIdString});
notifyAttachmentListeners();
}
public void deleteAttachment(@NonNull AttachmentId id) { public void deleteAttachment(@NonNull AttachmentId id) {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();

View File

@ -318,6 +318,19 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
notifyConversationListListeners(); notifyConversationListListeners();
} }
public boolean hasDownloadedProfilePicture(String groupId) {
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[]{AVATAR}, GROUP_ID + " = ?",
new String[] {groupId},
null, null, null))
{
if (cursor != null && cursor.moveToNext()) {
return !cursor.isNull(0);
}
return false;
}
}
public void updateMembers(String groupId, List<Address> members) { public void updateMembers(String groupId, List<Address> members) {
Collections.sort(members); Collections.sort(members);

View File

@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.apache.commons.lang3.StringUtils;
import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Address;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
@ -109,6 +110,11 @@ public class GroupReceiptDatabase extends Database {
db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)}); db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)});
} }
void deleteRowsForMessages(long[] mmsIds) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, MMS_ID + " IN (?)", new String[] {StringUtils.join(mmsIds, ',')});
}
void deleteAllRows() { void deleteAllRows() {
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, null, null); db.delete(TABLE_NAME, null, null);

View File

@ -77,6 +77,25 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
database.endTransaction() database.endTransaction()
} }
fun deleteMessages(messageIDs: List<Long>) {
val database = databaseHelper.writableDatabase
database.beginTransaction()
database.delete(
messageIDTable,
"${Companion.messageID} IN (${messageIDs.map { "?" }.joinToString(",")})",
messageIDs.map { "$it" }.toTypedArray()
)
database.delete(
messageThreadMappingTable,
"${Companion.messageID} IN (${messageIDs.map { "?" }.joinToString(",")})",
messageIDs.map { "$it" }.toTypedArray()
)
database.setTransactionSuccessful()
database.endTransaction()
}
/** /**
* @return pair of sms or mms table-specific ID and whether it is in SMS table * @return pair of sms or mms table-specific ID and whether it is in SMS table
*/ */
@ -96,6 +115,37 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
} }
} }
fun getMessageIDs(serverIDs: List<Long>, threadID: Long): Pair<List<Long>, List<Long>> {
val database = databaseHelper.readableDatabase
// Retrieve the message ids
val messageIdCursor = database
.rawQuery(
"""
SELECT ${messageThreadMappingTable}.${messageID}, ${messageIDTable}.${messageType}
FROM ${messageThreadMappingTable}
JOIN ${messageIDTable} ON ${messageIDTable}.message_id = ${messageThreadMappingTable}.${messageID}
WHERE (
${messageThreadMappingTable}.${Companion.threadID} = $threadID AND
${messageThreadMappingTable}.${Companion.serverID} IN (${serverIDs.joinToString(",")})
)
"""
)
val smsMessageIds: MutableList<Long> = mutableListOf()
val mmsMessageIds: MutableList<Long> = mutableListOf()
while (messageIdCursor.moveToNext()) {
if (messageIdCursor.getInt(1) == SMS_TYPE) {
smsMessageIds.add(messageIdCursor.getLong(0))
}
else {
mmsMessageIds.add(messageIdCursor.getLong(0))
}
}
return Pair(smsMessageIds, mmsMessageIds)
}
override fun setServerID(messageID: Long, serverID: Long, isSms: Boolean) { override fun setServerID(messageID: Long, serverID: Long, isSms: Boolean) {
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
val contentValues = ContentValues(3) val contentValues = ContentValues(3)
@ -183,6 +233,15 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
database.delete(messageHashTable, "${Companion.messageID} = ?", arrayOf(messageID.toString())) database.delete(messageHashTable, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
} }
fun deleteMessageServerHashes(messageIDs: List<Long>) {
val database = databaseHelper.writableDatabase
database.delete(
messageHashTable,
"${Companion.messageID} IN (${messageIDs.map { "?" }.joinToString(",")})",
messageIDs.map { "$it" }.toTypedArray()
)
}
fun migrateThreadId(legacyThreadId: Long, newThreadId: Long) { fun migrateThreadId(legacyThreadId: Long, newThreadId: Long) {
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
val contentValues = ContentValues(1) val contentValues = ContentValues(1)

View File

@ -42,6 +42,7 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
public abstract void markAsDeleted(long messageId, boolean read, boolean hasMention); public abstract void markAsDeleted(long messageId, boolean read, boolean hasMention);
public abstract boolean deleteMessage(long messageId); public abstract boolean deleteMessage(long messageId);
public abstract boolean deleteMessages(long[] messageId, long threadId);
public abstract void updateThreadId(long fromId, long toId); public abstract void updateThreadId(long fromId, long toId);

View File

@ -999,6 +999,23 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
return threadDeleted return threadDeleted
} }
override fun deleteMessages(messageIds: LongArray, threadId: Long): Boolean {
val attachmentDatabase = get(context).attachmentDatabase()
val groupReceiptDatabase = get(context).groupReceiptDatabase()
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessages(messageIds) })
groupReceiptDatabase.deleteRowsForMessages(messageIds)
val database = databaseHelper.writableDatabase
database!!.delete(TABLE_NAME, ID_IN, arrayOf(messageIds.joinToString(",")))
val threadDeleted = get(context).threadDatabase().update(threadId, false)
notifyConversationListeners(threadId)
notifyStickerListeners()
notifyStickerPackListeners()
return threadDeleted
}
override fun updateThreadId(fromId: Long, toId: Long) { override fun updateThreadId(fromId: Long, toId: Long) {
val contentValues = ContentValues(1) val contentValues = ContentValues(1)
contentValues.put(THREAD_ID, toId) contentValues.put(THREAD_ID, toId)

View File

@ -31,6 +31,7 @@ import com.annimon.stream.Stream;
import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteStatement; import net.zetetic.database.sqlcipher.SQLiteStatement;
import org.apache.commons.lang3.StringUtils;
import org.session.libsession.messaging.calls.CallMessageType; import org.session.libsession.messaging.calls.CallMessageType;
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage; import org.session.libsession.messaging.messages.signal.IncomingGroupMessage;
import org.session.libsession.messaging.messages.signal.IncomingTextMessage; import org.session.libsession.messaging.messages.signal.IncomingTextMessage;
@ -52,6 +53,7 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -603,6 +605,30 @@ public class SmsDatabase extends MessagingDatabase {
return threadDeleted; return threadDeleted;
} }
@Override
public boolean deleteMessages(long[] messageIds, long threadId) {
String[] argsArray = new String[messageIds.length];
String[] argValues = new String[messageIds.length];
Arrays.fill(argsArray, "?");
for (int i = 0; i < messageIds.length; i++) {
argValues[i] = (messageIds[i] + "");
}
String combinedMessageIdArgss = StringUtils.join(messageIds, ',');
String combinedMessageIds = StringUtils.join(messageIds, ',');
Log.i("MessageDatabase", "Deleting: " + combinedMessageIds);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(
TABLE_NAME,
ID + " IN (" + StringUtils.join(argsArray, ',') + ")",
argValues
);
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false);
notifyConversationListeners(threadId);
return threadDeleted;
}
@Override @Override
public void updateThreadId(long fromId, long toId) { public void updateThreadId(long fromId, long toId) {
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);

View File

@ -321,6 +321,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).groupDatabase().updateProfilePicture(groupID, newValue) DatabaseComponent.get(context).groupDatabase().updateProfilePicture(groupID, newValue)
} }
override fun hasDownloadedProfilePicture(groupID: String): Boolean {
return DatabaseComponent.get(context).groupDatabase().hasDownloadedProfilePicture(groupID)
}
override fun getReceivedMessageTimestamps(): Set<Long> { override fun getReceivedMessageTimestamps(): Set<Long> {
return SessionMetaProtocol.getTimestamps() return SessionMetaProtocol.getTimestamps()
} }

View File

@ -1,7 +1,9 @@
package org.thoughtcrime.securesms.webrtc package org.thoughtcrime.securesms.webrtc
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import androidx.core.content.ContextCompat
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -176,8 +178,22 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
_callStateEvents.value = newState _callStateEvents.value = newState
} }
fun isBusy(context: Context, callId: UUID) = callId != this.callId && (currentConnectionState != CallState.Idle fun isBusy(context: Context, callId: UUID): Boolean {
|| context.getSystemService(TelephonyManager::class.java).callState != TelephonyManager.CALL_STATE_IDLE) // Make sure we have the permission before accessing the callState
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
return (
callId != this.callId && (
currentConnectionState != CallState.Idle ||
context.getSystemService(TelephonyManager::class.java).callState != TelephonyManager.CALL_STATE_IDLE
)
)
}
return (
callId != this.callId &&
currentConnectionState != CallState.Idle
)
}
fun isPreOffer() = currentConnectionState == CallState.RemotePreOffer fun isPreOffer() = currentConnectionState == CallState.RemotePreOffer

View File

@ -12,6 +12,7 @@ import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.calls.CallMessageType
import org.session.libsession.messaging.messages.control.CallMessage 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.utilities.Address import org.session.libsession.utilities.Address
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
@ -29,6 +30,10 @@ 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 {
private const val VERY_EXPIRED_TIME = 15 * 60 * 1000L
}
init { init {
lifecycle.coroutineScope.launch(IO) { lifecycle.coroutineScope.launch(IO) {
while (isActive) { while (isActive) {
@ -53,6 +58,13 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
} }
continue continue
} }
val isVeryExpired = (nextMessage.sentTimestamp?:0) + VERY_EXPIRED_TIME < SnodeAPI.nowWithOffset
if (isVeryExpired) {
Log.e("Loki", "Dropping very expired call message")
continue
}
when (nextMessage.type) { when (nextMessage.type) {
OFFER -> incomingCall(nextMessage) OFFER -> incomingCall(nextMessage)
ANSWER -> incomingAnswer(nextMessage) ANSWER -> incomingAnswer(nextMessage)
@ -78,7 +90,7 @@ 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)
ContextCompat.startForegroundService(context, hangupIntent) context.startService(hangupIntent)
} }
private fun incomingAnswer(callMessage: CallMessage) { private fun incomingAnswer(callMessage: CallMessage) {
@ -91,7 +103,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
sdp = sdp, sdp = sdp,
callId = callId callId = callId
) )
ContextCompat.startForegroundService(context, answerIntent) context.startService(answerIntent)
} }
private fun handleIceCandidates(callMessage: CallMessage) { private fun handleIceCandidates(callMessage: CallMessage) {
@ -120,7 +132,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
callId = callId, callId = callId,
callTime = callMessage.sentTimestamp!! callTime = callMessage.sentTimestamp!!
) )
ContextCompat.startForegroundService(context, incomingIntent) context.startService(incomingIntent)
} }
private fun incomingCall(callMessage: CallMessage) { private fun incomingCall(callMessage: CallMessage) {
@ -134,8 +146,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
callId = callId, callId = callId,
callTime = callMessage.sentTimestamp!! callTime = callMessage.sentTimestamp!!
) )
ContextCompat.startForegroundService(context, incomingIntent) context.startService(incomingIntent)
} }
private fun CallMessage.iceCandidates(): List<IceCandidate> { private fun CallMessage.iceCandidates(): List<IceCandidate> {

View File

@ -865,5 +865,5 @@
<string name="new_conversation_dialog_back_button_content_description">Navigate Back</string> <string name="new_conversation_dialog_back_button_content_description">Navigate Back</string>
<string name="new_conversation_dialog_close_button_content_description">Close Dialog</string> <string name="new_conversation_dialog_close_button_content_description">Close Dialog</string>
<string name="ErrorNotifier_migration">Database Upgrade Failed</string> <string name="ErrorNotifier_migration">Database Upgrade Failed</string>
<string name="ErrorNotifier_migration_downgrade">Please contact support to report the error and then install an older version to continue using Session.</string> <string name="ErrorNotifier_migration_downgrade">Please contact support to report the error.</string>
</resources> </resources>

View File

@ -20,7 +20,9 @@ interface MessageDataProvider {
* @return pair of sms or mms table-specific ID and whether it is in SMS table * @return pair of sms or mms table-specific ID and whether it is in SMS table
*/ */
fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>? fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>?
fun getMessageIDs(serverIDs: List<Long>, threadID: Long): Pair<List<Long>, List<Long>>
fun deleteMessage(messageID: Long, isSms: Boolean) fun deleteMessage(messageID: Long, isSms: Boolean)
fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean)
fun updateMessageAsDeleted(timestamp: Long, author: String): Long? fun updateMessageAsDeleted(timestamp: Long, author: String): Long?
fun getServerHashForMessage(messageID: Long): String? fun getServerHashForMessage(messageID: Long): String?
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?

View File

@ -81,6 +81,7 @@ interface StorageProtocol {
// Open Group Metadata // Open Group Metadata
fun updateTitle(groupID: String, newValue: String) fun updateTitle(groupID: String, newValue: String)
fun updateProfilePicture(groupID: String, newValue: ByteArray) fun updateProfilePicture(groupID: String, newValue: ByteArray)
fun hasDownloadedProfilePicture(groupID: String): Boolean
fun setUserCount(room: String, server: String, newValue: Int) fun setUserCount(room: String, server: String, newValue: Int)
// Last Message Server ID // Last Message Server ID

View File

@ -77,7 +77,11 @@ object FileServerApi {
OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).map { OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).map {
it.body ?: throw Error.ParsingFailed it.body ?: throw Error.ParsingFailed
}.fail { e -> }.fail { e ->
Log.e("Loki", "File server request failed.", e) when (e) {
// No need for the stack trace for HTTP errors
is HTTP.HTTPRequestFailedException -> Log.e("Loki", "File server request failed due to error: ${e.message}")
else -> Log.e("Loki", "File server request failed", e)
}
} }
} else { } else {
Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests.")) Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))

View File

@ -92,14 +92,25 @@ class BatchMessageReceiveJob(
threadMap[threadID]!! += parsedParams threadMap[threadID]!! += parsedParams
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Couldn't receive message.", e) when (e) {
if (e is MessageReceiver.Error && !e.isRetryable) { is MessageReceiver.Error.DuplicateMessage, MessageReceiver.Error.SelfSend -> {
Log.e(TAG, "Message failed permanently",e) Log.i(TAG, "Couldn't receive message, failed with error: ${e.message}")
} else { }
Log.e(TAG, "Message failed",e) is MessageReceiver.Error -> {
if (!e.isRetryable) {
Log.e(TAG, "Couldn't receive message, failed permanently", e)
}
else {
Log.e(TAG, "Couldn't receive message, failed", e)
failures += messageParameters failures += messageParameters
} }
} }
else -> {
Log.e(TAG, "Couldn't receive message, failed", e)
failures += messageParameters
}
}
}
} }
// iterate over threads and persist them (persistence is the longest constant in the batch process operation) // iterate over threads and persist them (persistence is the longest constant in the batch process operation)

View File

@ -26,7 +26,7 @@ class JobQueue : JobDelegate {
private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>() private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>()
private val rxDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val rxDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val rxMediaDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher() private val rxMediaDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
private val openGroupDispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher() private val openGroupDispatcher = Executors.newFixedThreadPool(8).asCoroutineDispatcher()
private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val scope = CoroutineScope(Dispatchers.Default) + SupervisorJob() private val scope = CoroutineScope(Dispatchers.Default) + SupervisorJob()
private val queue = Channel<Job>(UNLIMITED) private val queue = Channel<Job>(UNLIMITED)

View File

@ -11,6 +11,7 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.Data import org.session.libsession.messaging.utilities.Data
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
class MessageSendJob(val message: Message, val destination: Destination) : Job { class MessageSendJob(val message: Message, val destination: Destination) : Job {
@ -67,14 +68,25 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
val promise = MessageSender.send(this.message, this.destination).success { val promise = MessageSender.send(this.message, this.destination).success {
this.handleSuccess() this.handleSuccess()
}.fail { exception -> }.fail { exception ->
Log.e(TAG, "Couldn't send message due to error: $exception.") var logStacktrace = true
if (exception is MessageSender.Error) {
when (exception) {
// No need for the stack trace for HTTP errors
is HTTP.HTTPRequestFailedException -> {
logStacktrace = false
if (exception.statusCode == 429) { this.handlePermanentFailure(exception) }
else { this.handleFailure(exception) }
}
is MessageSender.Error -> {
if (!exception.isRetryable) { this.handlePermanentFailure(exception) } if (!exception.isRetryable) { this.handlePermanentFailure(exception) }
else { this.handleFailure(exception) }
} }
if (exception is OnionRequestAPI.HTTPRequestFailedAtDestinationException && exception.statusCode == 429) { else -> this.handleFailure(exception)
this.handlePermanentFailure(exception)
} }
this.handleFailure(exception)
if (logStacktrace) { Log.e(TAG, "Couldn't send message due to error", exception) }
else { Log.e(TAG, "Couldn't send message due to error: ${exception.message}") }
} }
try { try {
promise.get() promise.get()

View File

@ -23,15 +23,28 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val numberToDelete = messageServerIds.size val numberToDelete = messageServerIds.size
Log.d(TAG, "Deleting $numberToDelete messages") Log.d(TAG, "Deleting $numberToDelete messages")
var numberDeleted = 0
messageServerIds.forEach { serverId -> // FIXME: This entire process should probably run in a transaction (with the attachment deletion happening only if it succeeded)
val (messageId, isSms) = dataProvider.getMessageID(serverId, threadId) ?: return@forEach try {
dataProvider.deleteMessage(messageId, isSms) val messageIds = dataProvider.getMessageIDs(messageServerIds.toList(), threadId)
numberDeleted++
// Delete the SMS messages
if (messageIds.first.isNotEmpty()) {
dataProvider.deleteMessages(messageIds.first, threadId, true)
} }
Log.d(TAG, "Deleted $numberDeleted messages successfully")
// Delete the MMS messages
if (messageIds.second.isNotEmpty()) {
dataProvider.deleteMessages(messageIds.second, threadId, false)
}
Log.d(TAG, "Deleted ${messageIds.first.size + messageIds.second.size} messages successfully")
delegate?.handleJobSucceeded(this) delegate?.handleJobSucceeded(this)
} }
catch (e: Exception) {
delegate?.handleJobFailed(this, e)
}
}
override fun serialize(): Data = Data.Builder() override fun serialize(): Data = Data.Builder()
.putLongArray(MESSAGE_IDS, messageServerIds) .putLongArray(MESSAGE_IDS, messageServerIds)

View File

@ -383,7 +383,11 @@ object OpenGroupApi {
} }
return if (request.useOnionRouting) { return if (request.useOnionRouting) {
OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey).fail { e -> OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey).fail { e ->
Log.e("SOGS", "Failed onion request", e) when (e) {
// No need for the stack trace for HTTP errors
is HTTP.HTTPRequestFailedException -> Log.e("SOGS", "Failed onion request: ${e.message}")
else -> Log.e("SOGS", "Failed onion request", e)
}
} }
} else { } else {
Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests.")) Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))

View File

@ -117,6 +117,7 @@ class OpenGroupPoller(private val server: String, private val executorService: S
) { ) {
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val groupId = "$server.$roomToken" val groupId = "$server.$roomToken"
val dbGroupId = GroupUtil.getEncodedOpenGroupID(groupId.toByteArray())
val existingOpenGroup = storage.getOpenGroup(roomToken, server) val existingOpenGroup = storage.getOpenGroup(roomToken, server)
val publicKey = existingOpenGroup?.publicKey ?: return val publicKey = existingOpenGroup?.publicKey ?: return
@ -157,8 +158,19 @@ class OpenGroupPoller(private val server: String, private val executorService: S
}) })
} }
// Start downloading the room image (if we don't have one or it's been updated) if (
if (pollInfo.details?.imageId != null && pollInfo.details.imageId != existingOpenGroup.imageId) { (
pollInfo.details != null &&
pollInfo.details.imageId != null && (
pollInfo.details.imageId != existingOpenGroup.imageId ||
!storage.hasDownloadedProfilePicture(dbGroupId)
)
) || (
pollInfo.details == null &&
existingOpenGroup.imageId != null &&
!storage.hasDownloadedProfilePicture(dbGroupId)
)
) {
JobQueue.shared.add(GroupAvatarDownloadJob(roomToken, server)) JobQueue.shared.add(GroupAvatarDownloadJob(roomToken, server))
} }
} }

View File

@ -78,8 +78,8 @@ object OnionRequestAPI {
// endregion // endregion
class HTTPRequestFailedBlindingRequiredException(statusCode: Int, json: Map<*, *>, destination: String): HTTPRequestFailedAtDestinationException(statusCode, json, destination) class HTTPRequestFailedBlindingRequiredException(statusCode: Int, json: Map<*, *>, destination: String): HTTPRequestFailedAtDestinationException(statusCode, json, destination)
open class HTTPRequestFailedAtDestinationException(val statusCode: Int, val json: Map<*, *>, val destination: String) open class HTTPRequestFailedAtDestinationException(statusCode: Int, json: Map<*, *>, val destination: String)
: Exception("HTTP request failed at destination ($destination) with status code $statusCode.") : HTTP.HTTPRequestFailedException(statusCode, json, "HTTP request failed at destination ($destination) with status code $statusCode.")
class InsufficientSnodesException : Exception("Couldn't find enough snodes to build a path.") class InsufficientSnodesException : Exception("Couldn't find enough snodes to build a path.")
private data class OnionBuildingResult( private data class OnionBuildingResult(

View File

@ -56,6 +56,10 @@ object SnodeAPI {
* user's clock is incorrect. * user's clock is incorrect.
*/ */
internal var clockOffset = 0L internal var clockOffset = 0L
val nowWithOffset
get() = System.currentTimeMillis() + clockOffset
internal var forkInfo by observable(database.getForkInfo()) { _, oldValue, newValue -> internal var forkInfo by observable(database.getForkInfo()) { _, oldValue, newValue ->
if (newValue > oldValue) { if (newValue > oldValue) {
Log.d("Loki", "Setting new fork info new: $newValue, old: $oldValue") Log.d("Loki", "Setting new fork info new: $newValue, old: $oldValue")

View File

@ -2,6 +2,7 @@ package org.session.libsession.utilities
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.session.libsession.messaging.file_server.FileServerApi import org.session.libsession.messaging.file_server.FileServerApi
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import java.io.* import java.io.*
@ -40,7 +41,11 @@ object DownloadUtilities {
outputStream.write(it) outputStream.write(it)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Loki", "Couldn't download attachment.", e) when (e) {
// No need for the stack trace for HTTP errors
is HTTP.HTTPRequestFailedException -> Log.e("Loki", "Couldn't download attachment due to error: ${e.message}")
else -> Log.e("Loki", "Couldn't download attachment", e)
}
throw e throw e
} }
} }

View File

@ -12,6 +12,7 @@ import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
object HTTP { object HTTP {
var isConnectedToNetwork: (() -> Boolean) = { false }
private val seedNodeConnection by lazy { private val seedNodeConnection by lazy {
OkHttpClient().newBuilder() OkHttpClient().newBuilder()
@ -64,8 +65,12 @@ object HTTP {
private const val timeout: Long = 120 private const val timeout: Long = 120
class HTTPRequestFailedException(val statusCode: Int, val json: Map<*, *>?) open class HTTPRequestFailedException(
: kotlin.Exception("HTTP request failed with status code $statusCode.") val statusCode: Int,
val json: Map<*, *>?,
message: String = "HTTP request failed with status code $statusCode"
) : kotlin.Exception(message)
class HTTPNoNetworkException : HTTPRequestFailedException(0, null, "No network connection")
enum class Verb(val rawValue: String) { enum class Verb(val rawValue: String) {
GET("GET"), PUT("PUT"), POST("POST"), DELETE("DELETE") GET("GET"), PUT("PUT"), POST("POST"), DELETE("DELETE")
@ -120,8 +125,11 @@ object HTTP {
response = connection.newCall(request.build()).execute() response = connection.newCall(request.build()).execute()
} catch (exception: Exception) { } catch (exception: Exception) {
Log.d("Loki", "${verb.rawValue} request to $url failed due to error: ${exception.localizedMessage}.") Log.d("Loki", "${verb.rawValue} request to $url failed due to error: ${exception.localizedMessage}.")
if (!isConnectedToNetwork()) { throw HTTPNoNetworkException() }
// Override the actual error so that we can correctly catch failed requests in OnionRequestAPI // Override the actual error so that we can correctly catch failed requests in OnionRequestAPI
throw HTTPRequestFailedException(0, null) throw HTTPRequestFailedException(0, null, "HTTP request failed due to: ${exception.message}")
} }
return when (val statusCode = response.code()) { return when (val statusCode = response.code()) {
200 -> { 200 -> {