diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/model/MessageResult.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/model/MessageResult.java index 903e23413..17fd7b55b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/model/MessageResult.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/model/MessageResult.java @@ -11,20 +11,29 @@ public class MessageResult { public final Recipient conversationRecipient; public final Recipient messageRecipient; + public final String body; public final String bodySnippet; public final long threadId; + public final long messageId; public final long receivedTimestampMs; + public final boolean isMms; public MessageResult(@NonNull Recipient conversationRecipient, @NonNull Recipient messageRecipient, + @NonNull String body, @NonNull String bodySnippet, long threadId, - long receivedTimestampMs) + long messageId, + long receivedTimestampMs, + boolean isMms) { this.conversationRecipient = conversationRecipient; this.messageRecipient = messageRecipient; + this.body = body; this.bodySnippet = bodySnippet; this.threadId = threadId; + this.messageId = messageId; this.receivedTimestampMs = receivedTimestampMs; + this.isMms = isMms; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MentionDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MentionDatabase.java index 3486dbb6e..211bc0694 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MentionDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MentionDatabase.java @@ -6,6 +6,9 @@ import android.database.Cursor; import android.text.TextUtils; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.annimon.stream.Stream; import net.sqlcipher.database.SQLiteDatabase; @@ -15,7 +18,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.CursorUtil; import org.thoughtcrime.securesms.util.SqlUtil; -import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; @@ -86,27 +88,58 @@ public class MentionDatabase extends Database { } public @NonNull Map> getMentionsForMessages(@NonNull Collection messageIds) { - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - Map> mentions = new HashMap<>(); - - String ids = TextUtils.join(",", messageIds); + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String ids = TextUtils.join(",", messageIds); try (Cursor cursor = db.query(TABLE_NAME, null, MESSAGE_ID + " IN (" + ids + ")", null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - long messageId = CursorUtil.requireLong(cursor, MESSAGE_ID); - List messageMentions = mentions.get(messageId); + return readMentions(cursor); + } + } - if (messageMentions == null) { - messageMentions = new LinkedList<>(); - mentions.put(messageId, messageMentions); - } + public @NonNull Map> getMentionsContainingRecipients(@NonNull Collection recipientIds, long limit) { + return getMentionsContainingRecipients(recipientIds, -1, limit); + } - messageMentions.add(new Mention(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)), - CursorUtil.requireInt(cursor, RANGE_START), - CursorUtil.requireInt(cursor, RANGE_LENGTH))); - } + public @NonNull Map> getMentionsContainingRecipients(@NonNull Collection recipientIds, long threadId, long limit) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String ids = TextUtils.join(",", Stream.of(recipientIds).map(RecipientId::serialize).toList()); + + String where = " WHERE " + RECIPIENT_ID + " IN (" + ids + ")"; + if (threadId != -1) { + where += " AND " + THREAD_ID + " = " + threadId; } + String subSelect = "SELECT DISTINCT " + MESSAGE_ID + + " FROM " + TABLE_NAME + + where + + " ORDER BY " + ID + " DESC" + + " LIMIT " + limit; + + String query = "SELECT *" + + " FROM " + TABLE_NAME + + " WHERE " + MESSAGE_ID + + " IN (" + subSelect + ")"; + + try (Cursor cursor = db.rawQuery(query, null)) { + return readMentions(cursor); + } + } + + private @NonNull Map> readMentions(@Nullable Cursor cursor) { + Map> mentions = new HashMap<>(); + while (cursor != null && cursor.moveToNext()) { + long messageId = CursorUtil.requireLong(cursor, MESSAGE_ID); + List messageMentions = mentions.get(messageId); + + if (messageMentions == null) { + messageMentions = new LinkedList<>(); + mentions.put(messageId, messageMentions); + } + + messageMentions.add(new Mention(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)), + CursorUtil.requireInt(cursor, RANGE_START), + CursorUtil.requireInt(cursor, RANGE_LENGTH))); + } return mentions; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index b7282b0b7..ea7e031e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -470,6 +470,11 @@ public class MmsDatabase extends MessagingDatabase { } } + public Reader getMessages(Collection messageIds) { + String ids = TextUtils.join(",", messageIds); + return readerFor(rawQuery(MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " IN (" + ids + ")", null)); + } + public Reader getExpireStartedMessages() { String where = EXPIRE_STARTED + " > 0"; return readerFor(rawQuery(where, null)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index ad4e103ba..316b7e3f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -1933,17 +1933,24 @@ public class RecipientDatabase extends Database { return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null); } - public @NonNull List queryRecipientsForMentions(@NonNull String query, @NonNull List recipientIds) { - if (TextUtils.isEmpty(query) || recipientIds.isEmpty()) { + public @NonNull List queryRecipientsForMentions(@NonNull String query) { + return queryRecipientsForMentions(query, null); + } + + public @NonNull List queryRecipientsForMentions(@NonNull String query, @Nullable List recipientIds) { + if (TextUtils.isEmpty(query)) { return Collections.emptyList(); } query = buildCaseInsensitiveGlobPattern(query); - String ids = TextUtils.join(",", Stream.of(recipientIds).map(RecipientId::serialize).toList()); + String ids = null; + if (Util.hasItems(recipientIds)) { + ids = TextUtils.join(",", Stream.of(recipientIds).map(RecipientId::serialize).toList()); + } String selection = BLOCKED + " = 0 AND " + - ID + " IN (" + ids + ") AND " + + (ids != null ? ID + " IN (" + ids + ") AND " : "") + SORT_NAME + " GLOB ?"; List recipients = new ArrayList<>(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java index ee59015b7..285a74e9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java @@ -26,6 +26,10 @@ public class SearchDatabase extends Database { public static final String SNIPPET = "snippet"; public static final String CONVERSATION_RECIPIENT = "conversation_recipient"; public static final String MESSAGE_RECIPIENT = "message_recipient"; + public static final String IS_MMS = "is_mms"; + public static final String MESSAGE_ID = "message_id"; + + public static final String SNIPPET_WRAP = "..."; public static final String[] CREATE_TABLE = { "CREATE VIRTUAL TABLE " + SMS_FTS_TABLE_NAME + " USING fts5(" + BODY + ", " + THREAD_ID + " UNINDEXED, content=" + SmsDatabase.TABLE_NAME + ", content_rowid=" + SmsDatabase.ID + ");", @@ -60,9 +64,12 @@ public class SearchDatabase extends Database { "SELECT " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " AS " + CONVERSATION_RECIPIENT + ", " + MmsSmsColumns.RECIPIENT_ID + " AS " + MESSAGE_RECIPIENT + ", " + - "snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " + + "snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '" + SNIPPET_WRAP + "', 7) AS " + SNIPPET + ", " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " + - SMS_FTS_TABLE_NAME + "." + THREAD_ID + " " + + SMS_FTS_TABLE_NAME + "." + THREAD_ID + ", " + + SMS_FTS_TABLE_NAME + "." + BODY + ", " + + SMS_FTS_TABLE_NAME + "." + ID + " AS " + MESSAGE_ID + ", " + + "0 AS " + IS_MMS + " " + "FROM " + SmsDatabase.TABLE_NAME + " " + "INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " + "INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " + @@ -71,9 +78,12 @@ public class SearchDatabase extends Database { "SELECT " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " AS " + CONVERSATION_RECIPIENT + ", " + MmsSmsColumns.RECIPIENT_ID + " AS " + MESSAGE_RECIPIENT + ", " + - "snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " + + "snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '" + SNIPPET_WRAP + "', 7) AS " + SNIPPET + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " + - MMS_FTS_TABLE_NAME + "." + THREAD_ID + " " + + MMS_FTS_TABLE_NAME + "." + THREAD_ID + ", " + + MMS_FTS_TABLE_NAME + "." + BODY + ", " + + MMS_FTS_TABLE_NAME + "." + ID + " AS " + MESSAGE_ID + ", " + + "1 AS " + IS_MMS + " " + "FROM " + MmsDatabase.TABLE_NAME + " " + "INNER JOIN " + MMS_FTS_TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " + "INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " + @@ -85,9 +95,12 @@ public class SearchDatabase extends Database { "SELECT " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " AS " + CONVERSATION_RECIPIENT + ", " + MmsSmsColumns.RECIPIENT_ID + " AS " + MESSAGE_RECIPIENT + ", " + - "snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " + + "snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '" + SNIPPET_WRAP + "', 7) AS " + SNIPPET + ", " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " + - SMS_FTS_TABLE_NAME + "." + THREAD_ID + " " + + SMS_FTS_TABLE_NAME + "." + THREAD_ID + ", " + + SMS_FTS_TABLE_NAME + "." + BODY + ", " + + SMS_FTS_TABLE_NAME + "." + ID + " AS " + MESSAGE_ID + ", " + + "0 AS " + IS_MMS + " " + "FROM " + SmsDatabase.TABLE_NAME + " " + "INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " + "INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " + @@ -96,9 +109,12 @@ public class SearchDatabase extends Database { "SELECT " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " AS " + CONVERSATION_RECIPIENT + ", " + MmsSmsColumns.RECIPIENT_ID + " AS " + MESSAGE_RECIPIENT + ", " + - "snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " + + "snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '" + SNIPPET_WRAP + "', 7) AS " + SNIPPET + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " + - MMS_FTS_TABLE_NAME + "." + THREAD_ID + " " + + MMS_FTS_TABLE_NAME + "." + THREAD_ID + ", " + + MMS_FTS_TABLE_NAME + "." + BODY + ", " + + MMS_FTS_TABLE_NAME + "." + ID + " AS " + MESSAGE_ID + ", " + + "1 AS " + IS_MMS + " " + "FROM " + MmsDatabase.TABLE_NAME + " " + "INNER JOIN " + MMS_FTS_TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " + "INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " + diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java index 66a51b360..2a8509d9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java @@ -4,41 +4,51 @@ import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.MergeCursor; +import android.text.TextUtils; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.text.TextUtils; - import com.annimon.stream.Stream; - import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactRepository; +import org.thoughtcrime.securesms.conversationlist.model.MessageResult; +import org.thoughtcrime.securesms.conversationlist.model.SearchResult; import org.thoughtcrime.securesms.database.CursorList; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MentionDatabase; +import org.thoughtcrime.securesms.database.MentionUtil; +import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsSmsColumns; +import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SearchDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.database.model.Mention; +import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.conversationlist.model.MessageResult; -import org.thoughtcrime.securesms.conversationlist.model.SearchResult; -import org.thoughtcrime.securesms.util.Stopwatch; +import org.thoughtcrime.securesms.util.CursorUtil; +import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; +import static org.thoughtcrime.securesms.database.SearchDatabase.SNIPPET_WRAP; + /** * Manages data retrieval for search. */ @@ -70,11 +80,17 @@ public class SearchRepository { private final ContactAccessor contactAccessor; private final Executor serialExecutor; private final ExecutorService parallelExecutor; + private final RecipientDatabase recipientDatabase; + private final MentionDatabase mentionDatabase; + private final MmsDatabase mmsDatabase; public SearchRepository() { this.context = ApplicationDependencies.getApplication().getApplicationContext(); this.searchDatabase = DatabaseFactory.getSearchDatabase(context); this.threadDatabase = DatabaseFactory.getThreadDatabase(context); + this.recipientDatabase = DatabaseFactory.getRecipientDatabase(context); + this.mentionDatabase = DatabaseFactory.getMentionDatabase(context); + this.mmsDatabase = DatabaseFactory.getMmsDatabase(context); this.contactRepository = new ContactRepository(context); this.contactAccessor = ContactAccessor.getInstance(); this.serialExecutor = SignalExecutors.SERIAL; @@ -90,13 +106,14 @@ public class SearchRepository { serialExecutor.execute(() -> { String cleanQuery = sanitizeQuery(query); - Future> contacts = parallelExecutor.submit(() -> queryContacts(cleanQuery)); - Future> conversations = parallelExecutor.submit(() -> queryConversations(cleanQuery)); - Future> messages = parallelExecutor.submit(() -> queryMessages(cleanQuery)); + Future> contacts = parallelExecutor.submit(() -> queryContacts(cleanQuery)); + Future> conversations = parallelExecutor.submit(() -> queryConversations(cleanQuery)); + Future> messages = parallelExecutor.submit(() -> queryMessages(cleanQuery)); + Future> mentionMessages = parallelExecutor.submit(() -> queryMentions(sanitizeQueryAsTokens(query))); try { long startTime = System.currentTimeMillis(); - SearchResult result = new SearchResult(cleanQuery, contacts.get(), conversations.get(), messages.get()); + SearchResult result = new SearchResult(cleanQuery, contacts.get(), conversations.get(), mergeMessagesAndMentions(messages.get(), mentionMessages.get())); Log.d(TAG, "Total time: " + (System.currentTimeMillis() - startTime) + " ms"); @@ -115,11 +132,13 @@ public class SearchRepository { } serialExecutor.execute(() -> { - long startTime = System.currentTimeMillis(); - List messages = queryMessages(sanitizeQuery(query), threadId); + long startTime = System.currentTimeMillis(); + List messages = queryMessages(sanitizeQuery(query), threadId); + List mentionMessages = queryMentions(sanitizeQueryAsTokens(query), threadId); + Log.d(TAG, "[ConversationQuery] " + (System.currentTimeMillis() - startTime) + " ms"); - callback.onResult(messages); + callback.onResult(mergeMessagesAndMentions(messages, mentionMessages)); }); } @@ -150,17 +169,163 @@ public class SearchRepository { } private @NonNull List queryMessages(@NonNull String query) { + List results; try (Cursor cursor = searchDatabase.queryMessages(query)) { - return readToList(cursor, new MessageModelBuilder(context)); + results = readToList(cursor, new MessageModelBuilder()); } + + List messageIds = new LinkedList<>(); + for (MessageResult result : results) { + if (result.isMms) { + messageIds.add(result.messageId); + } + } + + if (messageIds.isEmpty()) { + return results; + } + + Map> mentions = DatabaseFactory.getMentionDatabase(context).getMentionsForMessages(messageIds); + if (mentions.isEmpty()) { + return results; + } + + List updatedResults = new ArrayList<>(results.size()); + for (MessageResult result : results) { + if (result.isMms && mentions.containsKey(result.messageId)) { + List messageMentions = mentions.get(result.messageId); + + //noinspection ConstantConditions + String updatedBody = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, result.body, messageMentions).getBody().toString(); + String updatedSnippet = updateSnippetWithDisplayNames(result.body, result.bodySnippet, messageMentions); + + //noinspection ConstantConditions + updatedResults.add(new MessageResult(result.conversationRecipient, result.messageRecipient, updatedBody, updatedSnippet, result.threadId, result.messageId, result.receivedTimestampMs, result.isMms)); + } else { + updatedResults.add(result); + } + } + + return updatedResults; + } + + private @NonNull String updateSnippetWithDisplayNames(@NonNull String body, @NonNull String bodySnippet, @NonNull List mentions) { + String cleanSnippet = bodySnippet; + int startOffset = 0; + + if (cleanSnippet.startsWith(SNIPPET_WRAP)) { + cleanSnippet = cleanSnippet.substring(SNIPPET_WRAP.length()); + startOffset = SNIPPET_WRAP.length(); + } + + if (cleanSnippet.endsWith(SNIPPET_WRAP)) { + cleanSnippet = cleanSnippet.substring(0, cleanSnippet.length() - SNIPPET_WRAP.length()); + } + + int startIndex = body.indexOf(cleanSnippet); + + if (startIndex != -1) { + List adjustMentions = new ArrayList<>(mentions.size()); + for (Mention mention : mentions) { + int adjustedStart = mention.getStart() - startIndex + startOffset; + if (adjustedStart >= 0 && adjustedStart + mention.getLength() <= cleanSnippet.length()) { + adjustMentions.add(new Mention(mention.getRecipientId(), adjustedStart, mention.getLength())); + } + } + + //noinspection ConstantConditions + return MentionUtil.updateBodyAndMentionsWithDisplayNames(context, bodySnippet, adjustMentions).getBody().toString(); + } + + return bodySnippet; } private @NonNull List queryMessages(@NonNull String query, long threadId) { try (Cursor cursor = searchDatabase.queryMessages(query, threadId)) { - return readToList(cursor, new MessageModelBuilder(context)); + return readToList(cursor, new MessageModelBuilder()); } } + private @NonNull List queryMentions(@NonNull List cleanQueries) { + Set recipientIds = new HashSet<>(); + for (String cleanQuery : cleanQueries) { + for (Recipient recipient : recipientDatabase.queryRecipientsForMentions(cleanQuery)) { + recipientIds.add(recipient.getId()); + } + } + + Map> mentionQueryResults = mentionDatabase.getMentionsContainingRecipients(recipientIds, 500); + + if (mentionQueryResults.isEmpty()) { + return Collections.emptyList(); + } + + List results = new ArrayList<>(); + + try (MmsDatabase.Reader reader = mmsDatabase.getMessages(mentionQueryResults.keySet())) { + MessageRecord record; + while ((record = reader.getNext()) != null) { + List mentions = mentionQueryResults.get(record.getId()); + if (Util.hasItems(mentions)) { + MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, record.getBody(), mentions); + String updatedBody = updated.getBody() != null ? updated.getBody().toString() : record.getBody(); + String updatedSnippet = makeSnippet(cleanQueries, updatedBody); + + //noinspection ConstantConditions + results.add(new MessageResult(threadDatabase.getRecipientForThreadId(record.getThreadId()), record.getRecipient(), updatedBody, updatedSnippet, record.getThreadId(), record.getId(), record.getDateReceived(), true)); + } + } + } + + return results; + } + + private @NonNull List queryMentions(@NonNull List cleanQueries, long threadId) { + Set recipientIds = new HashSet<>(); + for (String cleanQuery : cleanQueries) { + for (Recipient recipient : recipientDatabase.queryRecipientsForMentions(cleanQuery)) { + recipientIds.add(recipient.getId()); + } + } + + Map> mentionQueryResults = mentionDatabase.getMentionsContainingRecipients(recipientIds, threadId, 500); + + if (mentionQueryResults.isEmpty()) { + return Collections.emptyList(); + } + + List results = new ArrayList<>(); + + try (MmsDatabase.Reader reader = mmsDatabase.getMessages(mentionQueryResults.keySet())) { + MessageRecord record; + while ((record = reader.getNext()) != null) { + //noinspection ConstantConditions + results.add(new MessageResult(threadDatabase.getRecipientForThreadId(record.getThreadId()), record.getRecipient(), record.getBody(), record.getBody(), record.getThreadId(), record.getId(), record.getDateReceived(), true)); + } + } + + return results; + } + + private @NonNull String makeSnippet(@NonNull List queries, @NonNull String body) { + if (body.length() < 50) { + return body; + } + + String lowerBody = body.toLowerCase(); + for (String query : queries) { + int foundIndex = lowerBody.indexOf(query.toLowerCase()); + if (foundIndex != -1) { + int snippetStart = Math.max(0, Math.max(body.lastIndexOf(' ', foundIndex - 5) + 1, foundIndex - 15)); + int lastSpace = body.indexOf(' ', foundIndex + 30); + int snippetEnd = Math.min(body.length(), lastSpace > 0 ? Math.min(lastSpace, foundIndex + 40) : foundIndex + 40); + + return (snippetStart > 0 ? SNIPPET_WRAP : "") + body.substring(snippetStart, snippetEnd) + (snippetEnd < body.length() ? SNIPPET_WRAP : ""); + } + } + return body; + } + private @NonNull List readToList(@Nullable Cursor cursor, @NonNull CursorList.ModelBuilder builder) { return readToList(cursor, builder, -1); } @@ -203,6 +368,37 @@ public class SearchRepository { return out.toString(); } + private @NonNull List sanitizeQueryAsTokens(@NonNull String query) { + String[] parts = query.split("\\s+"); + if (parts.length > 3) { + return Collections.emptyList(); + } + + return Stream.of(parts).map(this::sanitizeQuery).toList(); + } + + private static @NonNull List mergeMessagesAndMentions(@NonNull List messages, @NonNull List mentionMessages) { + Set includedMmsMessages = new HashSet<>(); + + List combined = new ArrayList<>(messages.size() + mentionMessages.size()); + for (MessageResult result : messages) { + combined.add(result); + if (result.isMms) { + includedMmsMessages.add(result.messageId); + } + } + + for (MessageResult result : mentionMessages) { + if (!includedMmsMessages.contains(result.messageId)) { + combined.add(result); + } + } + + Collections.sort(combined, Collections.reverseOrder((left, right) -> Long.compare(left.receivedTimestampMs, right.receivedTimestampMs))); + + return combined; + } + private static class RecipientModelBuilder implements CursorList.ModelBuilder { @Override @@ -228,23 +424,20 @@ public class SearchRepository { private static class MessageModelBuilder implements CursorList.ModelBuilder { - private final Context context; - - MessageModelBuilder(@NonNull Context context) { - this.context = context; - } - @Override public MessageResult build(@NonNull Cursor cursor) { RecipientId conversationRecipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndex(SearchDatabase.CONVERSATION_RECIPIENT))); - RecipientId messageRecipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(SearchDatabase.MESSAGE_RECIPIENT))); + RecipientId messageRecipientId = RecipientId.from(CursorUtil.requireLong(cursor, SearchDatabase.MESSAGE_RECIPIENT)); Recipient conversationRecipient = Recipient.live(conversationRecipientId).get(); Recipient messageRecipient = Recipient.live(messageRecipientId).get(); - String body = cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.SNIPPET)); - long receivedMs = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED)); - long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.THREAD_ID)); + String body = CursorUtil.requireString(cursor, SearchDatabase.BODY); + String bodySnippet = CursorUtil.requireString(cursor, SearchDatabase.SNIPPET); + long receivedMs = CursorUtil.requireLong(cursor, MmsSmsColumns.NORMALIZED_DATE_RECEIVED); + long threadId = CursorUtil.requireLong(cursor, MmsSmsColumns.THREAD_ID); + int messageId = CursorUtil.requireInt(cursor, SearchDatabase.MESSAGE_ID); + boolean isMms = CursorUtil.requireInt(cursor, SearchDatabase.IS_MMS) == 1; - return new MessageResult(conversationRecipient, messageRecipient, body, threadId, receivedMs); + return new MessageResult(conversationRecipient, messageRecipient, body, bodySnippet, threadId, messageId, receivedMs, isMms); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java index de1bd5266..9e8915729 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -155,10 +155,14 @@ public class Util { return value == null || value.getText() == null || TextUtils.isEmpty(value.getTextTrimmed()); } - public static boolean isEmpty(Collection collection) { + public static boolean isEmpty(Collection collection) { return collection == null || collection.isEmpty(); } + public static boolean hasItems(@Nullable Collection collection) { + return collection != null && !collection.isEmpty(); + } + public static V getOrDefault(@NonNull Map map, K key, V defaultValue) { return map.containsKey(key) ? map.get(key) : defaultValue; }