Improve search performance.
This commit is contained in:
parent
53ffca964d
commit
c274ed6a96
18 changed files with 413 additions and 219 deletions
|
@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase;
|
|||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
@ -179,41 +180,6 @@ public class ContactAccessor {
|
|||
return contactData;
|
||||
}
|
||||
|
||||
public List<String> getNumbersForThreadSearchFilter(Context context, String constraint) {
|
||||
LinkedList<String> numberList = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = DatabaseFactory.getRecipientDatabase(context).queryAllContacts(constraint)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String phone = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.PHONE));
|
||||
String email = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.EMAIL));
|
||||
|
||||
numberList.add(Util.getFirstNonEmpty(phone, email));
|
||||
}
|
||||
}
|
||||
|
||||
GroupDatabase.Reader reader = null;
|
||||
GroupRecord record;
|
||||
|
||||
try {
|
||||
reader = DatabaseFactory.getGroupDatabase(context).getGroupsFilteredByTitle(constraint, true, false);
|
||||
|
||||
while ((record = reader.getNext()) != null) {
|
||||
numberList.add(record.getId().toString());
|
||||
}
|
||||
} finally {
|
||||
if (reader != null)
|
||||
reader.close();
|
||||
}
|
||||
|
||||
if (context.getString(R.string.note_to_self).toLowerCase().contains(constraint.toLowerCase()) &&
|
||||
!numberList.contains(TextSecurePreferences.getLocalNumber(context)))
|
||||
{
|
||||
numberList.add(TextSecurePreferences.getLocalNumber(context));
|
||||
}
|
||||
|
||||
return numberList;
|
||||
}
|
||||
|
||||
public CharSequence phoneTypeToString(Context mContext, int type, CharSequence label) {
|
||||
return Phone.getTypeLabel(mContext.getResources(), type, label);
|
||||
}
|
||||
|
|
|
@ -141,7 +141,7 @@ import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationM
|
|||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.conversation.ui.groupcall.GroupCallViewModel;
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.search.MessageResult;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
|
@ -2132,7 +2132,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
|
||||
if (!result.getResults().isEmpty()) {
|
||||
MessageResult messageResult = result.getResults().get(result.getPosition());
|
||||
fragment.jumpToMessage(messageResult.messageRecipient.getId(), messageResult.receivedTimestampMs, searchViewModel::onMissingResult);
|
||||
fragment.jumpToMessage(messageResult.getMessageRecipient().getId(), messageResult.getReceivedTimestampMs(), searchViewModel::onMissingResult);
|
||||
}
|
||||
|
||||
searchNav.setData(result.getPosition(), result.getResults().size());
|
||||
|
|
|
@ -8,7 +8,7 @@ import androidx.lifecycle.LiveData;
|
|||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.search.MessageResult;
|
||||
import org.thoughtcrime.securesms.database.CursorList;
|
||||
import org.thoughtcrime.securesms.search.SearchRepository;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
|
|
|
@ -91,8 +91,8 @@ import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
|
|||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationFragment;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
|
||||
import org.thoughtcrime.securesms.search.MessageResult;
|
||||
import org.thoughtcrime.securesms.search.SearchResult;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
|
||||
|
@ -400,12 +400,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
@Override
|
||||
public void onMessageClicked(@NonNull MessageResult message) {
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
int startingPosition = DatabaseFactory.getMmsSmsDatabase(getContext()).getMessagePositionInConversation(message.threadId, message.receivedTimestampMs);
|
||||
int startingPosition = DatabaseFactory.getMmsSmsDatabase(getContext()).getMessagePositionInConversation(message.getThreadId(), message.getReceivedTimestampMs());
|
||||
return Math.max(0, startingPosition);
|
||||
}, startingPosition -> {
|
||||
hideKeyboard();
|
||||
getNavigator().goToConversation(message.conversationRecipient.getId(),
|
||||
message.threadId,
|
||||
getNavigator().goToConversation(message.getConversationRecipient().getId(),
|
||||
message.getThreadId(),
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
startingPosition);
|
||||
});
|
||||
|
@ -481,7 +481,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
public void onSearchTextChange(String text) {
|
||||
String trimmed = text.trim();
|
||||
|
||||
viewModel.updateQuery(trimmed);
|
||||
viewModel.onSearchQueryUpdated(trimmed);
|
||||
|
||||
if (trimmed.length() > 0) {
|
||||
if (activeAdapter != searchAdapter) {
|
||||
|
|
|
@ -48,7 +48,7 @@ import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
|||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.search.MessageResult;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
|
@ -237,7 +237,7 @@ public final class ConversationListItem extends ConstraintLayout
|
|||
@NonNull Locale locale,
|
||||
@Nullable String highlightSubstring)
|
||||
{
|
||||
observeRecipient(messageResult.conversationRecipient.live());
|
||||
observeRecipient(messageResult.getConversationRecipient().live());
|
||||
observeDisplayBody(null);
|
||||
setSubjectViewText(null);
|
||||
|
||||
|
@ -245,8 +245,8 @@ public final class ConversationListItem extends ConstraintLayout
|
|||
this.glideRequests = glideRequests;
|
||||
|
||||
fromView.setText(recipient.get(), true);
|
||||
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), messageResult.bodySnippet, highlightSubstring));
|
||||
dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, messageResult.receivedTimestampMs));
|
||||
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), messageResult.getBodySnippet(), highlightSubstring));
|
||||
dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, messageResult.getReceivedTimestampMs()));
|
||||
archivedView.setVisibility(GONE);
|
||||
unreadIndicator.setVisibility(GONE);
|
||||
deliveryStatusIndicator.setNone();
|
||||
|
|
|
@ -10,8 +10,8 @@ import androidx.annotation.Nullable;
|
|||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
|
||||
import org.thoughtcrime.securesms.search.MessageResult;
|
||||
import org.thoughtcrime.securesms.search.SearchResult;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
|
|
@ -9,13 +9,12 @@ import androidx.lifecycle.MutableLiveData;
|
|||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.paging.PagedData;
|
||||
import org.signal.paging.PagingConfig;
|
||||
import org.signal.paging.PagingController;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
|
||||
import org.thoughtcrime.securesms.search.SearchResult;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
|
@ -47,15 +46,17 @@ class ConversationListViewModel extends ViewModel {
|
|||
private final LiveData<Boolean> hasNoConversations;
|
||||
private final SearchRepository searchRepository;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
private final Debouncer searchDebouncer;
|
||||
private final Debouncer messageSearchDebouncer;
|
||||
private final Debouncer contactSearchDebouncer;
|
||||
private final ThrottledDebouncer updateDebouncer;
|
||||
private final DatabaseObserver.Observer observer;
|
||||
private final Invalidator invalidator;
|
||||
private final UnreadPaymentsLiveData unreadPaymentsLiveData;
|
||||
private final UnreadPaymentsRepository unreadPaymentsRepository;
|
||||
|
||||
private String lastQuery;
|
||||
private int pinnedCount;
|
||||
private String activeQuery;
|
||||
private SearchResult activeSearchResult;
|
||||
private int pinnedCount;
|
||||
|
||||
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository, boolean isArchived) {
|
||||
this.megaphone = new MutableLiveData<>();
|
||||
|
@ -63,19 +64,21 @@ class ConversationListViewModel extends ViewModel {
|
|||
this.searchRepository = searchRepository;
|
||||
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
|
||||
this.unreadPaymentsRepository = new UnreadPaymentsRepository();
|
||||
this.searchDebouncer = new Debouncer(300);
|
||||
this.messageSearchDebouncer = new Debouncer(500);
|
||||
this.contactSearchDebouncer = new Debouncer(100);
|
||||
this.updateDebouncer = new ThrottledDebouncer(500);
|
||||
this.activeSearchResult = SearchResult.EMPTY;
|
||||
this.invalidator = new Invalidator();
|
||||
this.pagedData = PagedData.create(ConversationListDataSource.create(application, isArchived),
|
||||
new PagingConfig.Builder()
|
||||
.setPageSize(15)
|
||||
.setBufferPages(2)
|
||||
.build());
|
||||
.setPageSize(15)
|
||||
.setBufferPages(2)
|
||||
.build());
|
||||
this.unreadPaymentsLiveData = new UnreadPaymentsLiveData();
|
||||
this.observer = () -> {
|
||||
updateDebouncer.publish(() -> {
|
||||
if (!TextUtils.isEmpty(getLastQuery())) {
|
||||
searchRepository.query(getLastQuery(), searchResult::postValue);
|
||||
if (!TextUtils.isEmpty(activeQuery)) {
|
||||
onSearchQueryUpdated(activeQuery);
|
||||
}
|
||||
pagedData.getController().onDataInvalidated();
|
||||
});
|
||||
|
@ -154,25 +157,57 @@ class ConversationListViewModel extends ViewModel {
|
|||
unreadPaymentsRepository.markAllPaymentsSeen();
|
||||
}
|
||||
|
||||
void updateQuery(String query) {
|
||||
lastQuery = query;
|
||||
searchDebouncer.publish(() -> searchRepository.query(query, result -> {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
if (query.equals(lastQuery)) {
|
||||
searchResult.setValue(result);
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
void onSearchQueryUpdated(String query) {
|
||||
activeQuery = query;
|
||||
|
||||
private @NonNull String getLastQuery() {
|
||||
return lastQuery == null ? "" : lastQuery;
|
||||
contactSearchDebouncer.publish(() -> {
|
||||
searchRepository.queryThreads(query, result -> {
|
||||
if (!result.getQuery().equals(activeQuery)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeSearchResult.getQuery().equals(activeQuery)) {
|
||||
activeSearchResult = SearchResult.EMPTY;
|
||||
}
|
||||
|
||||
activeSearchResult = activeSearchResult.merge(result);
|
||||
searchResult.postValue(activeSearchResult);
|
||||
});
|
||||
|
||||
searchRepository.queryContacts(query, result -> {
|
||||
if (!result.getQuery().equals(activeQuery)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeSearchResult.getQuery().equals(activeQuery)) {
|
||||
activeSearchResult = SearchResult.EMPTY;
|
||||
}
|
||||
|
||||
activeSearchResult = activeSearchResult.merge(result);
|
||||
searchResult.postValue(activeSearchResult);
|
||||
});
|
||||
});
|
||||
|
||||
messageSearchDebouncer.publish(() -> {
|
||||
searchRepository.queryMessages(query, result -> {
|
||||
if (!result.getQuery().equals(activeQuery)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeSearchResult.getQuery().equals(activeQuery)) {
|
||||
activeSearchResult = SearchResult.EMPTY;
|
||||
}
|
||||
|
||||
activeSearchResult = activeSearchResult.merge(result);
|
||||
searchResult.postValue(activeSearchResult);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
invalidator.invalidate();
|
||||
searchDebouncer.clear();
|
||||
messageSearchDebouncer.clear();
|
||||
updateDebouncer.clear();
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer);
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversationlist.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
/**
|
||||
* Represents a search result for a message.
|
||||
*/
|
||||
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 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;
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversationlist.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents an all-encompassing search result that can contain various result for different
|
||||
* subcategories.
|
||||
*/
|
||||
public class SearchResult {
|
||||
|
||||
public static final SearchResult EMPTY = new SearchResult("", Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
|
||||
private final String query;
|
||||
private final List<Recipient> contacts;
|
||||
private final List<ThreadRecord> conversations;
|
||||
private final List<MessageResult> messages;
|
||||
|
||||
public SearchResult(@NonNull String query,
|
||||
@NonNull List<Recipient> contacts,
|
||||
@NonNull List<ThreadRecord> conversations,
|
||||
@NonNull List<MessageResult> messages)
|
||||
{
|
||||
this.query = query;
|
||||
this.contacts = contacts;
|
||||
this.conversations = conversations;
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public List<Recipient> getContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public List<ThreadRecord> getConversations() {
|
||||
return conversations;
|
||||
}
|
||||
|
||||
public List<MessageResult> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return contacts.size() + conversations.size() + messages.size();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return size() == 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package org.thoughtcrime.securesms.search
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
data class ContactSearchResult(val results: List<Recipient>, val query: String)
|
|
@ -0,0 +1,17 @@
|
|||
package org.thoughtcrime.securesms.search
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Represents a search result for a message.
|
||||
*/
|
||||
data class MessageResult(
|
||||
val conversationRecipient: Recipient,
|
||||
val messageRecipient: Recipient,
|
||||
val body: String,
|
||||
val bodySnippet: String,
|
||||
val threadId: Long,
|
||||
val messageId: Long,
|
||||
val receivedTimestampMs: Long,
|
||||
val isMms: Boolean
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
package org.thoughtcrime.securesms.search
|
||||
|
||||
data class MessageSearchResult(val results: List<MessageResult>, val query: String)
|
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.search;
|
|||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.MergeCursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
|
@ -13,12 +12,11 @@ import com.annimon.stream.Stream;
|
|||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
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.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.MentionDatabase;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
|
@ -35,18 +33,19 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.FtsUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.signal.core.util.concurrent.LatestPrioritizedSerialExecutor;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
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 java.util.function.Consumer;
|
||||
|
||||
import static org.thoughtcrime.securesms.database.SearchDatabase.SNIPPET_WRAP;
|
||||
|
||||
|
@ -61,13 +60,13 @@ public class SearchRepository {
|
|||
private final SearchDatabase searchDatabase;
|
||||
private final ContactRepository contactRepository;
|
||||
private final ThreadDatabase threadDatabase;
|
||||
private final ContactAccessor contactAccessor;
|
||||
private final Executor serialExecutor;
|
||||
private final ExecutorService parallelExecutor;
|
||||
private final RecipientDatabase recipientDatabase;
|
||||
private final MentionDatabase mentionDatabase;
|
||||
private final MessageDatabase mmsDatabase;
|
||||
|
||||
private final LatestPrioritizedSerialExecutor searchExecutor;
|
||||
private final Executor serialExecutor;
|
||||
|
||||
public SearchRepository() {
|
||||
this.context = ApplicationDependencies.getApplication().getApplicationContext();
|
||||
this.searchDatabase = DatabaseFactory.getSearchDatabase(context);
|
||||
|
@ -76,36 +75,44 @@ public class SearchRepository {
|
|||
this.mentionDatabase = DatabaseFactory.getMentionDatabase(context);
|
||||
this.mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
this.contactRepository = new ContactRepository(context);
|
||||
this.contactAccessor = ContactAccessor.getInstance();
|
||||
this.serialExecutor = SignalExecutors.SERIAL;
|
||||
this.parallelExecutor = SignalExecutors.BOUNDED;
|
||||
this.searchExecutor = new LatestPrioritizedSerialExecutor(SignalExecutors.BOUNDED);
|
||||
this.serialExecutor = new SerialExecutor(SignalExecutors.BOUNDED);
|
||||
}
|
||||
|
||||
public void query(@NonNull String query, @NonNull Callback<SearchResult> callback) {
|
||||
if (TextUtils.isEmpty(query)) {
|
||||
callback.onResult(SearchResult.EMPTY);
|
||||
return;
|
||||
}
|
||||
public void queryThreads(@NonNull String query, @NonNull Consumer<ThreadSearchResult> callback) {
|
||||
searchExecutor.execute(2, () -> {
|
||||
long start = System.currentTimeMillis();
|
||||
List<ThreadRecord> result = queryConversations(query);
|
||||
|
||||
serialExecutor.execute(() -> {
|
||||
Log.d(TAG, "[threads] Search took " + (System.currentTimeMillis() - start) + " ms");
|
||||
|
||||
callback.accept(new ThreadSearchResult(result, query));
|
||||
});
|
||||
}
|
||||
|
||||
public void queryContacts(@NonNull String query, @NonNull Consumer<ContactSearchResult> callback) {
|
||||
searchExecutor.execute(1, () -> {
|
||||
long start = System.currentTimeMillis();
|
||||
List<Recipient> result = queryContacts(query);
|
||||
|
||||
Log.d(TAG, "[contacts] Search took " + (System.currentTimeMillis() - start) + " ms");
|
||||
|
||||
callback.accept(new ContactSearchResult(result, query));
|
||||
});
|
||||
}
|
||||
|
||||
public void queryMessages(@NonNull String query, @NonNull Consumer<MessageSearchResult> callback) {
|
||||
searchExecutor.execute(0, () -> {
|
||||
long start = System.currentTimeMillis();
|
||||
String cleanQuery = FtsUtil.sanitize(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<MessageResult>> mentionMessages = parallelExecutor.submit(() -> queryMentions(sanitizeQueryAsTokens(query)));
|
||||
List<MessageResult> messages = queryMessages(cleanQuery);
|
||||
List<MessageResult> mentionMessages = queryMentions(sanitizeQueryAsTokens(query));
|
||||
List<MessageResult> combined = mergeMessagesAndMentions(messages, mentionMessages);
|
||||
|
||||
try {
|
||||
long startTime = System.currentTimeMillis();
|
||||
SearchResult result = new SearchResult(cleanQuery, contacts.get(), conversations.get(), mergeMessagesAndMentions(messages.get(), mentionMessages.get()));
|
||||
Log.d(TAG, "[messages] Search took " + (System.currentTimeMillis() - start) + " ms");
|
||||
|
||||
Log.d(TAG, "Total time: " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
|
||||
callback.onResult(result);
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
Log.w(TAG, e);
|
||||
callback.onResult(SearchResult.EMPTY);
|
||||
}
|
||||
callback.accept(new MessageSearchResult(combined, query));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -127,6 +134,10 @@ public class SearchRepository {
|
|||
}
|
||||
|
||||
private List<Recipient> queryContacts(String query) {
|
||||
if (Util.isEmpty(query)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Cursor contacts = null;
|
||||
|
||||
try {
|
||||
|
@ -144,15 +155,39 @@ public class SearchRepository {
|
|||
}
|
||||
|
||||
private @NonNull List<ThreadRecord> queryConversations(@NonNull String query) {
|
||||
List<String> numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query);
|
||||
List<RecipientId> recipientIds = Stream.of(numbers).map(number -> Recipient.external(context, number)).map(Recipient::getId).toList();
|
||||
if (Util.isEmpty(query)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
try (Cursor cursor = threadDatabase.getFilteredConversationList(recipientIds)) {
|
||||
Set<RecipientId> recipientIds = new LinkedHashSet<>();
|
||||
|
||||
try (Cursor cursor = DatabaseFactory.getRecipientDatabase(context).queryAllContacts(query)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
recipientIds.add(RecipientId.from(CursorUtil.requireString(cursor, RecipientDatabase.ID)));
|
||||
}
|
||||
}
|
||||
|
||||
GroupDatabase.GroupRecord record;
|
||||
try (GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(context).getGroupsFilteredByTitle(query, true, false)) {
|
||||
while ((record = reader.getNext()) != null) {
|
||||
recipientIds.add(record.getRecipientId());
|
||||
}
|
||||
}
|
||||
|
||||
if (context.getString(R.string.note_to_self).toLowerCase().contains(query.toLowerCase())) {
|
||||
recipientIds.add(Recipient.self().getId());
|
||||
}
|
||||
|
||||
try (Cursor cursor = threadDatabase.getFilteredConversationList(new ArrayList<>(recipientIds))) {
|
||||
return readToList(cursor, new ThreadModelBuilder(threadDatabase));
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull List<MessageResult> queryMessages(@NonNull String query) {
|
||||
if (Util.isEmpty(query)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<MessageResult> results;
|
||||
try (Cursor cursor = searchDatabase.queryMessages(query)) {
|
||||
results = readToList(cursor, new MessageModelBuilder());
|
||||
|
@ -160,8 +195,8 @@ public class SearchRepository {
|
|||
|
||||
List<Long> messageIds = new LinkedList<>();
|
||||
for (MessageResult result : results) {
|
||||
if (result.isMms) {
|
||||
messageIds.add(result.messageId);
|
||||
if (result.isMms()) {
|
||||
messageIds.add(result.getMessageId());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -176,15 +211,15 @@ public class SearchRepository {
|
|||
|
||||
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);
|
||||
if (result.isMms() && mentions.containsKey(result.getMessageId())) {
|
||||
List<Mention> messageMentions = mentions.get(result.getMessageId());
|
||||
|
||||
//noinspection ConstantConditions
|
||||
String updatedBody = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, result.body, messageMentions).getBody().toString();
|
||||
String updatedSnippet = updateSnippetWithDisplayNames(result.body, result.bodySnippet, messageMentions);
|
||||
String updatedBody = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, result.getBody(), messageMentions).getBody().toString();
|
||||
String updatedSnippet = updateSnippetWithDisplayNames(result.getBody(), result.getBodySnippet(), messageMentions);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
updatedResults.add(new MessageResult(result.conversationRecipient, result.messageRecipient, updatedBody, updatedSnippet, result.threadId, result.messageId, result.receivedTimestampMs, result.isMms));
|
||||
updatedResults.add(new MessageResult(result.getConversationRecipient(), result.getMessageRecipient(), updatedBody, updatedSnippet, result.getThreadId(), result.getMessageId(), result.getReceivedTimestampMs(), result.isMms()));
|
||||
} else {
|
||||
updatedResults.add(result);
|
||||
}
|
||||
|
@ -345,18 +380,18 @@ public class SearchRepository {
|
|||
List<MessageResult> combined = new ArrayList<>(messages.size() + mentionMessages.size());
|
||||
for (MessageResult result : messages) {
|
||||
combined.add(result);
|
||||
if (result.isMms) {
|
||||
includedMmsMessages.add(result.messageId);
|
||||
if (result.isMms()) {
|
||||
includedMmsMessages.add(result.getMessageId());
|
||||
}
|
||||
}
|
||||
|
||||
for (MessageResult result : mentionMessages) {
|
||||
if (!includedMmsMessages.contains(result.messageId)) {
|
||||
if (!includedMmsMessages.contains(result.getMessageId())) {
|
||||
combined.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(combined, Collections.reverseOrder((left, right) -> Long.compare(left.receivedTimestampMs, right.receivedTimestampMs)));
|
||||
Collections.sort(combined, Collections.reverseOrder((left, right) -> Long.compare(left.getReceivedTimestampMs(), right.getReceivedTimestampMs())));
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package org.thoughtcrime.securesms.search
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Represents an all-encompassing search result that can contain various result for different
|
||||
* subcategories.
|
||||
*/
|
||||
data class SearchResult(
|
||||
val query: String,
|
||||
val contacts: List<Recipient>,
|
||||
val conversations: List<ThreadRecord>,
|
||||
val messages: List<MessageResult>
|
||||
) {
|
||||
fun size(): Int {
|
||||
return contacts.size + conversations.size + messages.size
|
||||
}
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = size() == 0
|
||||
|
||||
fun merge(result: ContactSearchResult): SearchResult {
|
||||
return this.copy(contacts = result.results, query = result.query)
|
||||
}
|
||||
|
||||
fun merge(result: ThreadSearchResult): SearchResult {
|
||||
return this.copy(conversations = result.results, query = result.query)
|
||||
}
|
||||
|
||||
fun merge(result: MessageSearchResult): SearchResult {
|
||||
return this.copy(messages = result.results, query = result.query)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val EMPTY = SearchResult("", emptyList(), emptyList(), emptyList())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package org.thoughtcrime.securesms.search
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
|
||||
data class ThreadSearchResult(val results: List<ThreadRecord>, val query: String)
|
|
@ -10,9 +10,11 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion MINIMUM_SDK
|
||||
targetSdkVersion TARGET_SDK
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JAVA_VERSION
|
||||
targetCompatibility JAVA_VERSION
|
||||
}
|
||||
|
@ -40,9 +42,12 @@ protobuf {
|
|||
dependencies {
|
||||
lintChecks project(':lintchecks')
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
api 'androidx.annotation:annotation:1.1.0'
|
||||
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
|
||||
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.mockito:mockito-core:2.23.4'
|
||||
}
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
package org.signal.core.util.concurrent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A serial executor that will order pending tasks by a specified priority, and will only keep a single task of a given priority, preferring the latest.
|
||||
*
|
||||
* So imagine a world where the following tasks were all enqueued (meaning they're all waiting to be executed):
|
||||
*
|
||||
* execute(0, runnableA);
|
||||
* execute(3, runnableC1);
|
||||
* execute(3, runnableC2);
|
||||
* execute(2, runnableB);
|
||||
*
|
||||
* You'd expect the execution order to be:
|
||||
* - runnableC2
|
||||
* - runnableB
|
||||
* - runnableA
|
||||
*
|
||||
* (We order by priority, and C1 was replaced by C2)
|
||||
*/
|
||||
public final class LatestPrioritizedSerialExecutor {
|
||||
private final Queue<PriorityRunnable> tasks;
|
||||
private final Executor executor;
|
||||
private Runnable active;
|
||||
|
||||
public LatestPrioritizedSerialExecutor(@NonNull Executor executor) {
|
||||
this.executor = executor;
|
||||
this.tasks = new PriorityQueue<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute with a priority. Higher priorities are executed first.
|
||||
*/
|
||||
public synchronized void execute(int priority, @NonNull Runnable r) {
|
||||
Iterator<PriorityRunnable> iterator = tasks.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().getPriority() == priority) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
tasks.offer(new PriorityRunnable(priority) {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
r.run();
|
||||
} finally {
|
||||
scheduleNext();
|
||||
}
|
||||
}
|
||||
});
|
||||
if (active == null) {
|
||||
scheduleNext();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void scheduleNext() {
|
||||
if ((active = tasks.poll()) != null) {
|
||||
executor.execute(active);
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class PriorityRunnable implements Runnable, Comparable<PriorityRunnable> {
|
||||
private final int priority;
|
||||
|
||||
public PriorityRunnable(int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int compareTo(PriorityRunnable other) {
|
||||
return other.getPriority() - this.getPriority();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package org.signal.core.util.concurrent;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public final class LatestPrioritizedSerialExecutorTest {
|
||||
|
||||
@Test
|
||||
public void execute_sortsInPriorityOrder() {
|
||||
TestExecutor executor = new TestExecutor();
|
||||
Runnable placeholder = new TestRunnable();
|
||||
|
||||
Runnable first = spy(new TestRunnable());
|
||||
Runnable second = spy(new TestRunnable());
|
||||
Runnable third = spy(new TestRunnable());
|
||||
|
||||
LatestPrioritizedSerialExecutor subject = new LatestPrioritizedSerialExecutor(executor);
|
||||
subject.execute(0, placeholder); // The first thing we execute can't be sorted, so we put in this placeholder
|
||||
subject.execute(1, third);
|
||||
subject.execute(2, second);
|
||||
subject.execute(3, first);
|
||||
|
||||
executor.next(); // Clear the placeholder task
|
||||
|
||||
executor.next();
|
||||
verify(first).run();
|
||||
|
||||
executor.next();
|
||||
verify(second).run();
|
||||
|
||||
executor.next();
|
||||
verify(third).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void execute_replacesDupes() {
|
||||
TestExecutor executor = new TestExecutor();
|
||||
Runnable placeholder = new TestRunnable();
|
||||
|
||||
Runnable firstReplaced = spy(new TestRunnable());
|
||||
Runnable first = spy(new TestRunnable());
|
||||
Runnable second = spy(new TestRunnable());
|
||||
Runnable thirdReplaced = spy(new TestRunnable());
|
||||
Runnable third = spy(new TestRunnable());
|
||||
|
||||
LatestPrioritizedSerialExecutor subject = new LatestPrioritizedSerialExecutor(executor);
|
||||
subject.execute(0, placeholder); // The first thing we execute can't be sorted, so we put in this placeholder
|
||||
subject.execute(1, thirdReplaced);
|
||||
subject.execute(1, third);
|
||||
subject.execute(2, second);
|
||||
subject.execute(3, firstReplaced);
|
||||
subject.execute(3, first);
|
||||
|
||||
executor.next(); // Clear the placeholder task
|
||||
|
||||
executor.next();
|
||||
verify(first).run();
|
||||
|
||||
executor.next();
|
||||
verify(second).run();
|
||||
|
||||
executor.next();
|
||||
verify(third).run();
|
||||
|
||||
verify(firstReplaced, never()).run();
|
||||
verify(thirdReplaced, never()).run();
|
||||
}
|
||||
|
||||
private static final class TestExecutor implements Executor {
|
||||
|
||||
private final Queue<Runnable> tasks = new LinkedList<>();
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
tasks.add(command);
|
||||
}
|
||||
|
||||
public void next() {
|
||||
tasks.remove().run();
|
||||
}
|
||||
}
|
||||
|
||||
public static class TestRunnable implements Runnable {
|
||||
@Override
|
||||
public void run() { }
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue