Merge pull request #1445 from alansley/SES1251_AppCrashOnNonAlphanumeric_REFIX

SES1251 - App crash on non alphanumeric RE-FIX
This commit is contained in:
Andrew 2024-04-24 14:14:47 +09:30 committed by GitHub
commit 0e7a981386
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 30 additions and 67 deletions

View File

@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.home.search
import android.content.Context import android.content.Context
import android.text.Editable import android.text.Editable
import android.text.InputFilter
import android.text.InputFilter.LengthFilter
import android.text.TextWatcher import android.text.TextWatcher
import android.util.AttributeSet import android.util.AttributeSet
import android.view.KeyEvent import android.view.KeyEvent
@ -34,6 +36,7 @@ class GlobalSearchInputLayout @JvmOverloads constructor(
binding.searchInput.onFocusChangeListener = this binding.searchInput.onFocusChangeListener = this
binding.searchInput.addTextChangedListener(this) binding.searchInput.addTextChangedListener(this)
binding.searchInput.setOnEditorActionListener(this) binding.searchInput.setOnEditorActionListener(this)
binding.searchInput.setFilters( arrayOf<InputFilter>(LengthFilter(100)) ) // 100 char search limit
binding.searchCancel.setOnClickListener(this) binding.searchCancel.setOnClickListener(this)
binding.searchClear.setOnClickListener(this) binding.searchClear.setOnClickListener(this)
} }

View File

@ -24,8 +24,7 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
private val executor = viewModelScope + SupervisorJob() private val executor = viewModelScope + SupervisorJob()
private val _result: MutableStateFlow<GlobalSearchResult> = private val _result: MutableStateFlow<GlobalSearchResult> = MutableStateFlow(GlobalSearchResult.EMPTY)
MutableStateFlow(GlobalSearchResult.EMPTY)
val result: StateFlow<GlobalSearchResult> = _result val result: StateFlow<GlobalSearchResult> = _result
@ -41,13 +40,14 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
_queryText _queryText
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST) .buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
.mapLatest { query -> .mapLatest { query ->
if (query.trim().length < 2) { // Early exit on empty search query
if (query.trim().isEmpty()) {
SearchResult.EMPTY SearchResult.EMPTY
} else { } else {
// user input delay here in case we get a new query within a few hundred ms // User input delay in case we get a new query within a few hundred ms this
// this coroutine will be cancelled and expensive query will not be run if typing quickly // coroutine will be cancelled and the expensive query will not be run.
// first query of 2 characters will be instant however
delay(300) delay(300)
val settableFuture = SettableFuture<SearchResult>() val settableFuture = SettableFuture<SearchResult>()
searchRepository.query(query.toString(), settableFuture::set) searchRepository.query(query.toString(), settableFuture::set)
try { try {
@ -64,6 +64,4 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
} }
.launchIn(executor) .launchIn(executor)
} }
} }

View File

@ -4,12 +4,8 @@ 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 com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.session.libsession.messaging.contacts.Contact; import org.session.libsession.messaging.contacts.Contact;
import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.GroupRecord; 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.MessageResult;
import org.thoughtcrime.securesms.search.model.SearchResult; import org.thoughtcrime.securesms.search.model.SearchResult;
import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.Stopwatch;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import kotlin.Pair; import kotlin.Pair;
/** // Class to manage data retrieval for search
* Manages data retrieval for search.
*/
public class SearchRepository { public class SearchRepository {
private static final String TAG = SearchRepository.class.getSimpleName(); private static final String TAG = SearchRepository.class.getSimpleName();
private static final Set<Character> BANNED_CHARACTERS = new HashSet<>(); private static final Set<Character> BANNED_CHARACTERS = new HashSet<>();
static { static {
// Several ranges of invalid ASCII characters // Construct a list containing several ranges of invalid ASCII characters
for (int i = 33; i <= 47; i++) { // See: https://www.ascii-code.com/
BANNED_CHARACTERS.add((char) i); 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 = 58; i <= 64; i++) { for (int i = 91; i <= 96; i++) { BANNED_CHARACTERS.add((char) i); } // [, \, ], ^, _, `
BANNED_CHARACTERS.add((char) i); for (int i = 123; i <= 126; 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; private final Context context;
@ -86,35 +70,25 @@ public class SearchRepository {
} }
public void query(@NonNull String query, @NonNull Callback<SearchResult> callback) { public void query(@NonNull String query, @NonNull Callback<SearchResult> 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); callback.onResult(SearchResult.EMPTY);
return; return;
} }
executor.execute(() -> { executor.execute(() -> {
Stopwatch timer = new Stopwatch("FtsQuery"); 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"); timer.split("clean");
Pair<CursorList<Contact>, List<String>> contacts = queryContacts(cleanQuery); Pair<CursorList<Contact>, List<String>> contacts = queryContacts(cleanQuery);
timer.split("contacts"); timer.split("Contacts");
CursorList<GroupRecord> conversations = queryConversations(cleanQuery, contacts.getSecond()); CursorList<GroupRecord> conversations = queryConversations(cleanQuery, contacts.getSecond());
timer.split("conversations"); timer.split("Conversations");
CursorList<MessageResult> messages = queryMessages(cleanQuery); CursorList<MessageResult> messages = queryMessages(cleanQuery);
timer.split("messages"); timer.split("Messages");
timer.stop(TAG); timer.stop(TAG);
@ -123,23 +97,20 @@ public class SearchRepository {
} }
public void query(@NonNull String query, long threadId, @NonNull Callback<CursorList<MessageResult>> callback) { public void query(@NonNull String query, long threadId, @NonNull Callback<CursorList<MessageResult>> 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()); callback.onResult(CursorList.emptyList());
return; return;
} }
executor.execute(() -> { 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<MessageResult> messages = queryMessages(cleanQuery, threadId); CursorList<MessageResult> messages = queryMessages(cleanQuery, threadId);
callback.onResult(messages); callback.onResult(messages);
}); });
} }
private Pair<CursorList<Contact>, List<String>> queryContacts(String query) { private Pair<CursorList<Contact>, List<String>> queryContacts(String query) {
Cursor contacts = contactDatabase.queryContactsByName(query); Cursor contacts = contactDatabase.queryContactsByName(query);
List<Address> contactList = new ArrayList<>(); List<Address> contactList = new ArrayList<>();
List<String> contactStrings = new ArrayList<>(); List<String> contactStrings = new ArrayList<>();
@ -166,11 +137,10 @@ public class SearchRepository {
MergeCursor merged = new MergeCursor(new Cursor[]{addressThreads, individualRecipients}); MergeCursor merged = new MergeCursor(new Cursor[]{addressThreads, individualRecipients});
return new Pair<>(new CursorList<>(merged, new ContactModelBuilder(contactDatabase, threadDatabase)), contactStrings); return new Pair<>(new CursorList<>(merged, new ContactModelBuilder(contactDatabase, threadDatabase)), contactStrings);
} }
private CursorList<GroupRecord> queryConversations(@NonNull String query, List<String> matchingAddresses) { private CursorList<GroupRecord> queryConversations(@NonNull String query, List<String> matchingAddresses) {
List<String> numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query); List<String> numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query);
String localUserNumber = TextSecurePreferences.getLocalNumber(context); String localUserNumber = TextSecurePreferences.getLocalNumber(context);
if (localUserNumber != null) { if (localUserNumber != null) {
matchingAddresses.remove(localUserNumber); matchingAddresses.remove(localUserNumber);
@ -189,9 +159,7 @@ public class SearchRepository {
membersGroupList.close(); membersGroupList.close();
} }
Cursor conversations = threadDatabase.getFilteredConversationList(new ArrayList<>(addresses)); Cursor conversations = threadDatabase.getFilteredConversationList(new ArrayList<>(addresses));
return conversations != null ? new CursorList<>(conversations, new GroupModelBuilder(threadDatabase, groupDatabase)) return conversations != null ? new CursorList<>(conversations, new GroupModelBuilder(threadDatabase, groupDatabase))
: CursorList.emptyList(); : CursorList.emptyList();
} }
@ -256,9 +224,7 @@ public class SearchRepository {
private final Context context; private final Context context;
RecipientModelBuilder(@NonNull Context context) { RecipientModelBuilder(@NonNull Context context) { this.context = context; }
this.context = context;
}
@Override @Override
public Recipient build(@NonNull Cursor cursor) { public Recipient build(@NonNull Cursor cursor) {
@ -301,9 +267,7 @@ public class SearchRepository {
private final Context context; private final Context context;
MessageModelBuilder(@NonNull Context context) { MessageModelBuilder(@NonNull Context context) { this.context = context; }
this.context = context;
}
@Override @Override
public MessageResult build(@NonNull Cursor cursor) { public MessageResult build(@NonNull Cursor cursor) {

View File

@ -37,12 +37,10 @@ public class Stopwatch {
for (int i = 1; i < splits.size(); i++) { for (int i = 1; i < splits.size(); i++) {
out.append(splits.get(i).label).append(": "); out.append(splits.get(i).label).append(": ");
out.append(splits.get(i).time - splits.get(i - 1).time); 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).append("ms.");
out.append("total: ").append(splits.get(splits.size() - 1).time - startTime);
} }
Log.d(tag, out.toString()); Log.d(tag, out.toString());
} }