Add mention detection to search flows.

master
Cody Henthorne 2020-08-07 15:18:40 -04:00 committed by GitHub
parent 5cd4b82ed0
commit d563de4207
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 323 additions and 56 deletions

View File

@ -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;
}
}

View File

@ -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<Long, List<Mention>> getMentionsForMessages(@NonNull Collection<Long> messageIds) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Map<Long, List<Mention>> 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<Mention> messageMentions = mentions.get(messageId);
return readMentions(cursor);
}
}
if (messageMentions == null) {
messageMentions = new LinkedList<>();
mentions.put(messageId, messageMentions);
}
public @NonNull Map<Long, List<Mention>> getMentionsContainingRecipients(@NonNull Collection<RecipientId> 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<Long, List<Mention>> getMentionsContainingRecipients(@NonNull Collection<RecipientId> 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<Long, List<Mention>> readMentions(@Nullable Cursor cursor) {
Map<Long, List<Mention>> mentions = new HashMap<>();
while (cursor != null && cursor.moveToNext()) {
long messageId = CursorUtil.requireLong(cursor, MESSAGE_ID);
List<Mention> 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;
}
}

View File

@ -470,6 +470,11 @@ public class MmsDatabase extends MessagingDatabase {
}
}
public Reader getMessages(Collection<Long> 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));

View File

@ -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<Recipient> queryRecipientsForMentions(@NonNull String query, @NonNull List<RecipientId> recipientIds) {
if (TextUtils.isEmpty(query) || recipientIds.isEmpty()) {
public @NonNull List<Recipient> queryRecipientsForMentions(@NonNull String query) {
return queryRecipientsForMentions(query, null);
}
public @NonNull List<Recipient> queryRecipientsForMentions(@NonNull String query, @Nullable List<RecipientId> 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<Recipient> recipients = new ArrayList<>();

View File

@ -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 + " " +

View File

@ -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<List<Recipient>> contacts = parallelExecutor.submit(() -> queryContacts(cleanQuery));
Future<List<ThreadRecord>> conversations = parallelExecutor.submit(() -> queryConversations(cleanQuery));
Future<List<MessageResult>> messages = parallelExecutor.submit(() -> queryMessages(cleanQuery));
Future<List<Recipient>> contacts = parallelExecutor.submit(() -> queryContacts(cleanQuery));
Future<List<ThreadRecord>> conversations = parallelExecutor.submit(() -> queryConversations(cleanQuery));
Future<List<MessageResult>> messages = parallelExecutor.submit(() -> queryMessages(cleanQuery));
Future<List<MessageResult>> 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<MessageResult> messages = queryMessages(sanitizeQuery(query), threadId);
long startTime = System.currentTimeMillis();
List<MessageResult> messages = queryMessages(sanitizeQuery(query), threadId);
List<MessageResult> 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<MessageResult> queryMessages(@NonNull String query) {
List<MessageResult> results;
try (Cursor cursor = searchDatabase.queryMessages(query)) {
return readToList(cursor, new MessageModelBuilder(context));
results = readToList(cursor, new MessageModelBuilder());
}
List<Long> messageIds = new LinkedList<>();
for (MessageResult result : results) {
if (result.isMms) {
messageIds.add(result.messageId);
}
}
if (messageIds.isEmpty()) {
return results;
}
Map<Long, List<Mention>> mentions = DatabaseFactory.getMentionDatabase(context).getMentionsForMessages(messageIds);
if (mentions.isEmpty()) {
return results;
}
List<MessageResult> updatedResults = new ArrayList<>(results.size());
for (MessageResult result : results) {
if (result.isMms && mentions.containsKey(result.messageId)) {
List<Mention> 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<Mention> 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<Mention> 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<MessageResult> 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<MessageResult> queryMentions(@NonNull List<String> cleanQueries) {
Set<RecipientId> recipientIds = new HashSet<>();
for (String cleanQuery : cleanQueries) {
for (Recipient recipient : recipientDatabase.queryRecipientsForMentions(cleanQuery)) {
recipientIds.add(recipient.getId());
}
}
Map<Long, List<Mention>> mentionQueryResults = mentionDatabase.getMentionsContainingRecipients(recipientIds, 500);
if (mentionQueryResults.isEmpty()) {
return Collections.emptyList();
}
List<MessageResult> results = new ArrayList<>();
try (MmsDatabase.Reader reader = mmsDatabase.getMessages(mentionQueryResults.keySet())) {
MessageRecord record;
while ((record = reader.getNext()) != null) {
List<Mention> 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<MessageResult> queryMentions(@NonNull List<String> cleanQueries, long threadId) {
Set<RecipientId> recipientIds = new HashSet<>();
for (String cleanQuery : cleanQueries) {
for (Recipient recipient : recipientDatabase.queryRecipientsForMentions(cleanQuery)) {
recipientIds.add(recipient.getId());
}
}
Map<Long, List<Mention>> mentionQueryResults = mentionDatabase.getMentionsContainingRecipients(recipientIds, threadId, 500);
if (mentionQueryResults.isEmpty()) {
return Collections.emptyList();
}
List<MessageResult> 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<String> 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 <T> List<T> readToList(@Nullable Cursor cursor, @NonNull CursorList.ModelBuilder<T> builder) {
return readToList(cursor, builder, -1);
}
@ -203,6 +368,37 @@ public class SearchRepository {
return out.toString();
}
private @NonNull List<String> 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<MessageResult> mergeMessagesAndMentions(@NonNull List<MessageResult> messages, @NonNull List<MessageResult> mentionMessages) {
Set<Long> includedMmsMessages = new HashSet<>();
List<MessageResult> 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<Recipient> {
@Override
@ -228,23 +424,20 @@ public class SearchRepository {
private static class MessageModelBuilder implements CursorList.ModelBuilder<MessageResult> {
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);
}
}

View File

@ -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 <K, V> V getOrDefault(@NonNull Map<K, V> map, K key, V defaultValue) {
return map.containsKey(key) ? map.get(key) : defaultValue;
}