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
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.dynamiclanguage.DynamicLanguageContextWrapper;
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
import org.session.libsignal.utilities.HTTP;
import org.session.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.Log;
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.jobmanager.JobManager;
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.JobManagerFactories;
import org.thoughtcrime.securesms.logging.AndroidLogger;
@@ -237,6 +239,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
resubmitProfilePictureIfNeeded();
loadEmojiSearchIndexIfNeeded();
EmojiSource.refresh();
NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create();
HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
}
@Override

View File

@@ -176,6 +176,11 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
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) {
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase()
@@ -184,6 +189,15 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
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? {
val database = DatabaseComponent.get(context).mmsSmsDatabase()
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?) {
super.onChange(selfChange, uri)
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) {
queryRelativeDataColumn(uri)
} else {

View File

@@ -315,11 +315,24 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
restoreDraftIfNeeded()
setUpUiStateObserver()
binding!!.scrollToBottomButton.setOnClickListener {
val layoutManager = binding?.conversationRecyclerView?.layoutManager ?: return@setOnClickListener
val layoutManager = (binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager) ?: return@setOnClickListener
if (layoutManager.isSmoothScrolling) {
binding?.conversationRecyclerView?.scrollToPosition(0)
} else {
binding?.conversationRecyclerView?.smoothScrollToPosition(0)
// 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)
}
}
}
unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)

View File

@@ -35,6 +35,7 @@ import com.bumptech.glide.Glide;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
@@ -318,6 +319,28 @@ public class AttachmentDatabase extends Database {
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) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();

View File

@@ -318,6 +318,19 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
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) {
Collections.sort(members);

View File

@@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.apache.commons.lang3.StringUtils;
import org.session.libsession.utilities.Address;
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)});
}
void deleteRowsForMessages(long[] mmsIds) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, MMS_ID + " IN (?)", new String[] {StringUtils.join(mmsIds, ',')});
}
void deleteAllRows() {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, null, null);

View File

@@ -77,6 +77,25 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
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
*/
@@ -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) {
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(3)
@@ -183,6 +233,15 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
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) {
val database = databaseHelper.writableDatabase
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 boolean deleteMessage(long messageId);
public abstract boolean deleteMessages(long[] messageId, long threadId);
public abstract void updateThreadId(long fromId, long toId);

View File

@@ -999,6 +999,23 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
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) {
val contentValues = ContentValues(1)
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.SQLiteStatement;
import org.apache.commons.lang3.StringUtils;
import org.session.libsession.messaging.calls.CallMessageType;
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage;
import org.session.libsession.messaging.messages.signal.IncomingTextMessage;
@@ -52,6 +53,7 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -603,6 +605,30 @@ public class SmsDatabase extends MessagingDatabase {
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
public void updateThreadId(long fromId, long toId) {
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)
}
override fun hasDownloadedProfilePicture(groupID: String): Boolean {
return DatabaseComponent.get(context).groupDatabase().hasDownloadedProfilePicture(groupID)
}
override fun getReceivedMessageTimestamps(): Set<Long> {
return SessionMetaProtocol.getTimestamps()
}

View File

@@ -1,7 +1,9 @@
package org.thoughtcrime.securesms.webrtc
import android.content.Context
import android.content.pm.PackageManager
import android.telephony.TelephonyManager
import androidx.core.content.ContextCompat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.serialization.json.Json
@@ -176,8 +178,22 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
_callStateEvents.value = newState
}
fun isBusy(context: Context, callId: UUID) = callId != this.callId && (currentConnectionState != CallState.Idle
|| context.getSystemService(TelephonyManager::class.java).callState != TelephonyManager.CALL_STATE_IDLE)
fun isBusy(context: Context, callId: UUID): Boolean {
// 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

View File

@@ -12,6 +12,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.utilities.WebRtcUtils
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.TextSecurePreferences
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) {
companion object {
private const val VERY_EXPIRED_TIME = 15 * 60 * 1000L
}
init {
lifecycle.coroutineScope.launch(IO) {
while (isActive) {
@@ -53,6 +58,13 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
}
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) {
OFFER -> incomingCall(nextMessage)
ANSWER -> incomingAnswer(nextMessage)
@@ -78,7 +90,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)
ContextCompat.startForegroundService(context, hangupIntent)
context.startService(hangupIntent)
}
private fun incomingAnswer(callMessage: CallMessage) {
@@ -91,7 +103,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
sdp = sdp,
callId = callId
)
ContextCompat.startForegroundService(context, answerIntent)
context.startService(answerIntent)
}
private fun handleIceCandidates(callMessage: CallMessage) {
@@ -120,7 +132,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
callId = callId,
callTime = callMessage.sentTimestamp!!
)
ContextCompat.startForegroundService(context, incomingIntent)
context.startService(incomingIntent)
}
private fun incomingCall(callMessage: CallMessage) {
@@ -134,8 +146,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
callId = callId,
callTime = callMessage.sentTimestamp!!
)
ContextCompat.startForegroundService(context, incomingIntent)
context.startService(incomingIntent)
}
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_close_button_content_description">Close Dialog</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>