diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt index 1537769cdc..c22ccde1f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt @@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.home.search import android.content.Context import android.text.Editable +import android.text.InputFilter +import android.text.InputFilter.LengthFilter import android.text.TextWatcher import android.util.AttributeSet import android.view.KeyEvent @@ -34,6 +36,7 @@ class GlobalSearchInputLayout @JvmOverloads constructor( binding.searchInput.onFocusChangeListener = this binding.searchInput.addTextChangedListener(this) binding.searchInput.setOnEditorActionListener(this) + binding.searchInput.setFilters( arrayOf(LengthFilter(100)) ) // 100 char search limit binding.searchCancel.setOnClickListener(this) binding.searchClear.setOnClickListener(this) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt index 8908554b03..1ff0a395fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt @@ -24,8 +24,7 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se private val executor = viewModelScope + SupervisorJob() - private val _result: MutableStateFlow = - MutableStateFlow(GlobalSearchResult.EMPTY) + private val _result: MutableStateFlow = MutableStateFlow(GlobalSearchResult.EMPTY) val result: StateFlow = _result @@ -41,13 +40,14 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se _queryText .buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST) .mapLatest { query -> - if (query.trim().length < 2) { + // Early exit on empty search query + if (query.trim().isEmpty()) { SearchResult.EMPTY } else { - // user input delay here in case we get a new query within a few hundred ms - // this coroutine will be cancelled and expensive query will not be run if typing quickly - // first query of 2 characters will be instant however + // User input delay in case we get a new query within a few hundred ms this + // coroutine will be cancelled and the expensive query will not be run. delay(300) + val settableFuture = SettableFuture() searchRepository.query(query.toString(), settableFuture::set) try { @@ -64,6 +64,4 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se } .launchIn(executor) } - - } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java index a5669aa351..f2adbf2349 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java @@ -4,12 +4,8 @@ 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 com.annimon.stream.Stream; - import org.session.libsession.messaging.contacts.Contact; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.GroupRecord; @@ -27,37 +23,25 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.search.model.MessageResult; import org.thoughtcrime.securesms.search.model.SearchResult; import org.thoughtcrime.securesms.util.Stopwatch; - import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Executor; - import kotlin.Pair; -/** - * Manages data retrieval for search. - */ +// Class to manage data retrieval for search public class SearchRepository { - private static final String TAG = SearchRepository.class.getSimpleName(); private static final Set BANNED_CHARACTERS = new HashSet<>(); static { - // Several ranges of invalid ASCII characters - for (int i = 33; i <= 47; i++) { - BANNED_CHARACTERS.add((char) i); - } - for (int i = 58; i <= 64; i++) { - BANNED_CHARACTERS.add((char) i); - } - for (int i = 91; i <= 96; i++) { - BANNED_CHARACTERS.add((char) i); - } - for (int i = 123; i <= 126; i++) { - BANNED_CHARACTERS.add((char) i); - } + // Construct a list containing several ranges of invalid ASCII characters + // See: https://www.ascii-code.com/ + for (int i = 33; i <= 47; i++) { BANNED_CHARACTERS.add((char) i); } // !, ", #, $, %, &, ', (, ), *, +, ,, -, ., / + for (int i = 58; i <= 64; i++) { BANNED_CHARACTERS.add((char) i); } // :, ;, <, =, >, ?, @ + for (int i = 91; i <= 96; i++) { BANNED_CHARACTERS.add((char) i); } // [, \, ], ^, _, ` + for (int i = 123; i <= 126; i++) { BANNED_CHARACTERS.add((char) i); } // {, |, }, ~ } private final Context context; @@ -86,35 +70,25 @@ public class SearchRepository { } public void query(@NonNull String query, @NonNull Callback callback) { - if (TextUtils.isEmpty(query)) { + // If the sanitized search is empty then abort without search + String cleanQuery = sanitizeQuery(query).trim(); + if (cleanQuery.isEmpty()) { callback.onResult(SearchResult.EMPTY); return; } executor.execute(() -> { Stopwatch timer = new Stopwatch("FtsQuery"); - - String cleanQuery = sanitizeQuery(query); - - // If the search is for a single character and it was stripped by `sanitizeQuery` then abort - // the search for an empty string to avoid SQLite error. - if (cleanQuery.length() == 0) - { - Log.d(TAG, "Aborting empty search query."); - timer.stop(TAG); - return; - } - timer.split("clean"); Pair, List> contacts = queryContacts(cleanQuery); - timer.split("contacts"); + timer.split("Contacts"); CursorList conversations = queryConversations(cleanQuery, contacts.getSecond()); - timer.split("conversations"); + timer.split("Conversations"); CursorList messages = queryMessages(cleanQuery); - timer.split("messages"); + timer.split("Messages"); timer.stop(TAG); @@ -123,23 +97,20 @@ public class SearchRepository { } public void query(@NonNull String query, long threadId, @NonNull Callback> callback) { - if (TextUtils.isEmpty(query)) { + // If the sanitized search query is empty then abort the search + String cleanQuery = sanitizeQuery(query).trim(); + if (cleanQuery.isEmpty()) { callback.onResult(CursorList.emptyList()); return; } executor.execute(() -> { - // If the sanitized search query is empty then abort the search to prevent SQLite errors. - String cleanQuery = sanitizeQuery(query).trim(); - if (cleanQuery.isEmpty()) { return; } - CursorList messages = queryMessages(cleanQuery, threadId); callback.onResult(messages); }); } private Pair, List> queryContacts(String query) { - Cursor contacts = contactDatabase.queryContactsByName(query); List
contactList = new ArrayList<>(); List contactStrings = new ArrayList<>(); @@ -166,11 +137,10 @@ public class SearchRepository { MergeCursor merged = new MergeCursor(new Cursor[]{addressThreads, individualRecipients}); return new Pair<>(new CursorList<>(merged, new ContactModelBuilder(contactDatabase, threadDatabase)), contactStrings); - } private CursorList queryConversations(@NonNull String query, List matchingAddresses) { - List numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query); + List numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query); String localUserNumber = TextSecurePreferences.getLocalNumber(context); if (localUserNumber != null) { matchingAddresses.remove(localUserNumber); @@ -189,9 +159,7 @@ public class SearchRepository { membersGroupList.close(); } - Cursor conversations = threadDatabase.getFilteredConversationList(new ArrayList<>(addresses)); - return conversations != null ? new CursorList<>(conversations, new GroupModelBuilder(threadDatabase, groupDatabase)) : CursorList.emptyList(); } @@ -256,9 +224,7 @@ public class SearchRepository { private final Context context; - RecipientModelBuilder(@NonNull Context context) { - this.context = context; - } + RecipientModelBuilder(@NonNull Context context) { this.context = context; } @Override public Recipient build(@NonNull Cursor cursor) { @@ -301,9 +267,7 @@ public class SearchRepository { private final Context context; - MessageModelBuilder(@NonNull Context context) { - this.context = context; - } + MessageModelBuilder(@NonNull Context context) { this.context = context; } @Override public MessageResult build(@NonNull Cursor cursor) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Stopwatch.java b/app/src/main/java/org/thoughtcrime/securesms/util/Stopwatch.java index cac53899fb..d92fc7546d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Stopwatch.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Stopwatch.java @@ -37,12 +37,10 @@ public class Stopwatch { for (int i = 1; i < splits.size(); i++) { out.append(splits.get(i).label).append(": "); out.append(splits.get(i).time - splits.get(i - 1).time); - out.append(" "); + out.append("ms "); } - - out.append("total: ").append(splits.get(splits.size() - 1).time - startTime); + out.append("total: ").append(splits.get(splits.size() - 1).time - startTime).append("ms."); } - Log.d(tag, out.toString()); }