Add mention detection to search flows.
This commit is contained in:
parent
5cd4b82ed0
commit
d563de4207
7 changed files with 323 additions and 56 deletions
|
@ -11,20 +11,29 @@ public class MessageResult {
|
||||||
|
|
||||||
public final Recipient conversationRecipient;
|
public final Recipient conversationRecipient;
|
||||||
public final Recipient messageRecipient;
|
public final Recipient messageRecipient;
|
||||||
|
public final String body;
|
||||||
public final String bodySnippet;
|
public final String bodySnippet;
|
||||||
public final long threadId;
|
public final long threadId;
|
||||||
|
public final long messageId;
|
||||||
public final long receivedTimestampMs;
|
public final long receivedTimestampMs;
|
||||||
|
public final boolean isMms;
|
||||||
|
|
||||||
public MessageResult(@NonNull Recipient conversationRecipient,
|
public MessageResult(@NonNull Recipient conversationRecipient,
|
||||||
@NonNull Recipient messageRecipient,
|
@NonNull Recipient messageRecipient,
|
||||||
|
@NonNull String body,
|
||||||
@NonNull String bodySnippet,
|
@NonNull String bodySnippet,
|
||||||
long threadId,
|
long threadId,
|
||||||
long receivedTimestampMs)
|
long messageId,
|
||||||
|
long receivedTimestampMs,
|
||||||
|
boolean isMms)
|
||||||
{
|
{
|
||||||
this.conversationRecipient = conversationRecipient;
|
this.conversationRecipient = conversationRecipient;
|
||||||
this.messageRecipient = messageRecipient;
|
this.messageRecipient = messageRecipient;
|
||||||
|
this.body = body;
|
||||||
this.bodySnippet = bodySnippet;
|
this.bodySnippet = bodySnippet;
|
||||||
this.threadId = threadId;
|
this.threadId = threadId;
|
||||||
|
this.messageId = messageId;
|
||||||
this.receivedTimestampMs = receivedTimestampMs;
|
this.receivedTimestampMs = receivedTimestampMs;
|
||||||
|
this.isMms = isMms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,9 @@ import android.database.Cursor;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
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.CursorUtil;
|
||||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
@ -86,27 +88,58 @@ public class MentionDatabase extends Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull Map<Long, List<Mention>> getMentionsForMessages(@NonNull Collection<Long> messageIds) {
|
public @NonNull Map<Long, List<Mention>> getMentionsForMessages(@NonNull Collection<Long> messageIds) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
Map<Long, List<Mention>> mentions = new HashMap<>();
|
String ids = TextUtils.join(",", messageIds);
|
||||||
|
|
||||||
String ids = TextUtils.join(",", messageIds);
|
|
||||||
|
|
||||||
try (Cursor cursor = db.query(TABLE_NAME, null, MESSAGE_ID + " IN (" + ids + ")", null, null, null, null)) {
|
try (Cursor cursor = db.query(TABLE_NAME, null, MESSAGE_ID + " IN (" + ids + ")", null, null, null, null)) {
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
return readMentions(cursor);
|
||||||
long messageId = CursorUtil.requireLong(cursor, MESSAGE_ID);
|
}
|
||||||
List<Mention> messageMentions = mentions.get(messageId);
|
}
|
||||||
|
|
||||||
if (messageMentions == null) {
|
public @NonNull Map<Long, List<Mention>> getMentionsContainingRecipients(@NonNull Collection<RecipientId> recipientIds, long limit) {
|
||||||
messageMentions = new LinkedList<>();
|
return getMentionsContainingRecipients(recipientIds, -1, limit);
|
||||||
mentions.put(messageId, messageMentions);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
messageMentions.add(new Mention(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)),
|
public @NonNull Map<Long, List<Mention>> getMentionsContainingRecipients(@NonNull Collection<RecipientId> recipientIds, long threadId, long limit) {
|
||||||
CursorUtil.requireInt(cursor, RANGE_START),
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
CursorUtil.requireInt(cursor, RANGE_LENGTH)));
|
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;
|
return mentions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
public Reader getExpireStartedMessages() {
|
||||||
String where = EXPIRE_STARTED + " > 0";
|
String where = EXPIRE_STARTED + " > 0";
|
||||||
return readerFor(rawQuery(where, null));
|
return readerFor(rawQuery(where, null));
|
||||||
|
|
|
@ -1933,17 +1933,24 @@ public class RecipientDatabase extends Database {
|
||||||
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null);
|
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) {
|
public @NonNull List<Recipient> queryRecipientsForMentions(@NonNull String query) {
|
||||||
if (TextUtils.isEmpty(query) || recipientIds.isEmpty()) {
|
return queryRecipientsForMentions(query, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull List<Recipient> queryRecipientsForMentions(@NonNull String query, @Nullable List<RecipientId> recipientIds) {
|
||||||
|
if (TextUtils.isEmpty(query)) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
query = buildCaseInsensitiveGlobPattern(query);
|
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 " +
|
String selection = BLOCKED + " = 0 AND " +
|
||||||
ID + " IN (" + ids + ") AND " +
|
(ids != null ? ID + " IN (" + ids + ") AND " : "") +
|
||||||
SORT_NAME + " GLOB ?";
|
SORT_NAME + " GLOB ?";
|
||||||
|
|
||||||
List<Recipient> recipients = new ArrayList<>();
|
List<Recipient> recipients = new ArrayList<>();
|
||||||
|
|
|
@ -26,6 +26,10 @@ public class SearchDatabase extends Database {
|
||||||
public static final String SNIPPET = "snippet";
|
public static final String SNIPPET = "snippet";
|
||||||
public static final String CONVERSATION_RECIPIENT = "conversation_recipient";
|
public static final String CONVERSATION_RECIPIENT = "conversation_recipient";
|
||||||
public static final String MESSAGE_RECIPIENT = "message_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 = {
|
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 + ");",
|
"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 " +
|
"SELECT " +
|
||||||
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " AS " + CONVERSATION_RECIPIENT + ", " +
|
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " AS " + CONVERSATION_RECIPIENT + ", " +
|
||||||
MmsSmsColumns.RECIPIENT_ID + " AS " + MESSAGE_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 + ", " +
|
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 + " " +
|
"FROM " + SmsDatabase.TABLE_NAME + " " +
|
||||||
"INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " +
|
"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 + " " +
|
"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 " +
|
"SELECT " +
|
||||||
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " AS " + CONVERSATION_RECIPIENT + ", " +
|
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " AS " + CONVERSATION_RECIPIENT + ", " +
|
||||||
MmsSmsColumns.RECIPIENT_ID + " AS " + MESSAGE_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 + ", " +
|
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 + " " +
|
"FROM " + MmsDatabase.TABLE_NAME + " " +
|
||||||
"INNER JOIN " + MMS_FTS_TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " +
|
"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 + " " +
|
"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 " +
|
"SELECT " +
|
||||||
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " AS " + CONVERSATION_RECIPIENT + ", " +
|
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " AS " + CONVERSATION_RECIPIENT + ", " +
|
||||||
MmsSmsColumns.RECIPIENT_ID + " AS " + MESSAGE_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 + ", " +
|
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 + " " +
|
"FROM " + SmsDatabase.TABLE_NAME + " " +
|
||||||
"INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " +
|
"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 + " " +
|
"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 " +
|
"SELECT " +
|
||||||
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " AS " + CONVERSATION_RECIPIENT + ", " +
|
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " AS " + CONVERSATION_RECIPIENT + ", " +
|
||||||
MmsSmsColumns.RECIPIENT_ID + " AS " + MESSAGE_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 + ", " +
|
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 + " " +
|
"FROM " + MmsDatabase.TABLE_NAME + " " +
|
||||||
"INNER JOIN " + MMS_FTS_TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " +
|
"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 + " " +
|
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
|
||||||
|
|
|
@ -4,41 +4,51 @@ import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.DatabaseUtils;
|
import android.database.DatabaseUtils;
|
||||||
import android.database.MergeCursor;
|
import android.database.MergeCursor;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactRepository;
|
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.CursorList;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
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.MmsSmsColumns;
|
||||||
|
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SearchDatabase;
|
import org.thoughtcrime.securesms.database.SearchDatabase;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
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.database.model.ThreadRecord;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import static org.thoughtcrime.securesms.database.SearchDatabase.SNIPPET_WRAP;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages data retrieval for search.
|
* Manages data retrieval for search.
|
||||||
*/
|
*/
|
||||||
|
@ -70,11 +80,17 @@ public class SearchRepository {
|
||||||
private final ContactAccessor contactAccessor;
|
private final ContactAccessor contactAccessor;
|
||||||
private final Executor serialExecutor;
|
private final Executor serialExecutor;
|
||||||
private final ExecutorService parallelExecutor;
|
private final ExecutorService parallelExecutor;
|
||||||
|
private final RecipientDatabase recipientDatabase;
|
||||||
|
private final MentionDatabase mentionDatabase;
|
||||||
|
private final MmsDatabase mmsDatabase;
|
||||||
|
|
||||||
public SearchRepository() {
|
public SearchRepository() {
|
||||||
this.context = ApplicationDependencies.getApplication().getApplicationContext();
|
this.context = ApplicationDependencies.getApplication().getApplicationContext();
|
||||||
this.searchDatabase = DatabaseFactory.getSearchDatabase(context);
|
this.searchDatabase = DatabaseFactory.getSearchDatabase(context);
|
||||||
this.threadDatabase = DatabaseFactory.getThreadDatabase(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.contactRepository = new ContactRepository(context);
|
||||||
this.contactAccessor = ContactAccessor.getInstance();
|
this.contactAccessor = ContactAccessor.getInstance();
|
||||||
this.serialExecutor = SignalExecutors.SERIAL;
|
this.serialExecutor = SignalExecutors.SERIAL;
|
||||||
|
@ -90,13 +106,14 @@ public class SearchRepository {
|
||||||
serialExecutor.execute(() -> {
|
serialExecutor.execute(() -> {
|
||||||
String cleanQuery = sanitizeQuery(query);
|
String cleanQuery = sanitizeQuery(query);
|
||||||
|
|
||||||
Future<List<Recipient>> contacts = parallelExecutor.submit(() -> queryContacts(cleanQuery));
|
Future<List<Recipient>> contacts = parallelExecutor.submit(() -> queryContacts(cleanQuery));
|
||||||
Future<List<ThreadRecord>> conversations = parallelExecutor.submit(() -> queryConversations(cleanQuery));
|
Future<List<ThreadRecord>> conversations = parallelExecutor.submit(() -> queryConversations(cleanQuery));
|
||||||
Future<List<MessageResult>> messages = parallelExecutor.submit(() -> queryMessages(cleanQuery));
|
Future<List<MessageResult>> messages = parallelExecutor.submit(() -> queryMessages(cleanQuery));
|
||||||
|
Future<List<MessageResult>> mentionMessages = parallelExecutor.submit(() -> queryMentions(sanitizeQueryAsTokens(query)));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
long startTime = System.currentTimeMillis();
|
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");
|
Log.d(TAG, "Total time: " + (System.currentTimeMillis() - startTime) + " ms");
|
||||||
|
|
||||||
|
@ -115,11 +132,13 @@ public class SearchRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
serialExecutor.execute(() -> {
|
serialExecutor.execute(() -> {
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
List<MessageResult> messages = queryMessages(sanitizeQuery(query), threadId);
|
List<MessageResult> messages = queryMessages(sanitizeQuery(query), threadId);
|
||||||
|
List<MessageResult> mentionMessages = queryMentions(sanitizeQueryAsTokens(query), threadId);
|
||||||
|
|
||||||
Log.d(TAG, "[ConversationQuery] " + (System.currentTimeMillis() - startTime) + " ms");
|
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) {
|
private @NonNull List<MessageResult> queryMessages(@NonNull String query) {
|
||||||
|
List<MessageResult> results;
|
||||||
try (Cursor cursor = searchDatabase.queryMessages(query)) {
|
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) {
|
private @NonNull List<MessageResult> queryMessages(@NonNull String query, long threadId) {
|
||||||
try (Cursor cursor = searchDatabase.queryMessages(query, 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) {
|
private @NonNull <T> List<T> readToList(@Nullable Cursor cursor, @NonNull CursorList.ModelBuilder<T> builder) {
|
||||||
return readToList(cursor, builder, -1);
|
return readToList(cursor, builder, -1);
|
||||||
}
|
}
|
||||||
|
@ -203,6 +368,37 @@ public class SearchRepository {
|
||||||
return out.toString();
|
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> {
|
private static class RecipientModelBuilder implements CursorList.ModelBuilder<Recipient> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -228,23 +424,20 @@ public class SearchRepository {
|
||||||
|
|
||||||
private static class MessageModelBuilder implements CursorList.ModelBuilder<MessageResult> {
|
private static class MessageModelBuilder implements CursorList.ModelBuilder<MessageResult> {
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
MessageModelBuilder(@NonNull Context context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MessageResult build(@NonNull Cursor cursor) {
|
public MessageResult build(@NonNull Cursor cursor) {
|
||||||
RecipientId conversationRecipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndex(SearchDatabase.CONVERSATION_RECIPIENT)));
|
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 conversationRecipient = Recipient.live(conversationRecipientId).get();
|
||||||
Recipient messageRecipient = Recipient.live(messageRecipientId).get();
|
Recipient messageRecipient = Recipient.live(messageRecipientId).get();
|
||||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.SNIPPET));
|
String body = CursorUtil.requireString(cursor, SearchDatabase.BODY);
|
||||||
long receivedMs = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED));
|
String bodySnippet = CursorUtil.requireString(cursor, SearchDatabase.SNIPPET);
|
||||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.THREAD_ID));
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -155,10 +155,14 @@ public class Util {
|
||||||
return value == null || value.getText() == null || TextUtils.isEmpty(value.getTextTrimmed());
|
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();
|
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) {
|
public static <K, V> V getOrDefault(@NonNull Map<K, V> map, K key, V defaultValue) {
|
||||||
return map.containsKey(key) ? map.get(key) : defaultValue;
|
return map.containsKey(key) ? map.get(key) : defaultValue;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue