mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-06 05:14:50 +01:00
Improve search performance.
This commit is contained in:
+35
-10
@@ -91,11 +91,15 @@ class ContactSearchViewModel(
|
||||
private val internalFastScrollerEnabled = MutableStateFlow(false)
|
||||
private val internalDisplayingContextMenu = MutableStateFlow(false)
|
||||
private val internalScrollRequests = MutableSharedFlow<ScrollRequest>(extraBufferCapacity = 1)
|
||||
private val internalSearchInProgress = MutableStateFlow(false)
|
||||
|
||||
val fastScrollerEnabled: StateFlow<Boolean> = internalFastScrollerEnabled
|
||||
val isDisplayingContextMenu: StateFlow<Boolean> = internalDisplayingContextMenu
|
||||
val scrollRequests: SharedFlow<ScrollRequest> = internalScrollRequests
|
||||
|
||||
/** True while a [setConfiguration] call is fetching results off the main thread. Suitable for driving a loading indicator. */
|
||||
val searchInProgress: StateFlow<Boolean> = internalSearchInProgress
|
||||
|
||||
val query: StateFlow<String?> = rawQuery
|
||||
|
||||
init {
|
||||
@@ -151,17 +155,22 @@ class ContactSearchViewModel(
|
||||
}
|
||||
|
||||
suspend fun setConfiguration(contactSearchConfiguration: ContactSearchConfiguration) {
|
||||
val (pagedDataSource, size) = withContext(Dispatchers.IO) {
|
||||
val source = ContactSearchPagedDataSource(
|
||||
contactSearchConfiguration,
|
||||
arbitraryRepository = arbitraryRepository,
|
||||
searchRepository = searchRepository,
|
||||
contactSearchPagedDataSourceRepository = contactSearchPagedDataSourceRepository
|
||||
)
|
||||
source to source.size()
|
||||
internalSearchInProgress.value = true
|
||||
try {
|
||||
val (pagedDataSource, size) = withContext(Dispatchers.IO) {
|
||||
val source = ContactSearchPagedDataSource(
|
||||
contactSearchConfiguration,
|
||||
arbitraryRepository = arbitraryRepository,
|
||||
searchRepository = searchRepository,
|
||||
contactSearchPagedDataSourceRepository = contactSearchPagedDataSourceRepository
|
||||
)
|
||||
source to source.size()
|
||||
}
|
||||
internalTotalCount.value = size
|
||||
pagedData.value = PagedData.createForStateFlow(pagedDataSource, pagingConfig, data.value)
|
||||
} finally {
|
||||
internalSearchInProgress.value = false
|
||||
}
|
||||
internalTotalCount.value = size
|
||||
pagedData.value = PagedData.createForStateFlow(pagedDataSource, pagingConfig, data.value)
|
||||
}
|
||||
|
||||
fun setQuery(query: String?) {
|
||||
@@ -312,3 +321,19 @@ fun ContactSearchViewModel.bindAdapterToLifecycle(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes [ContactSearchViewModel.searchInProgress] scoped to the given [LifecycleOwner], invoking
|
||||
* [onSearchInProgressChanged] on the main thread whenever the loading state changes. Designed for Java
|
||||
* callers that want to drive a loading indicator.
|
||||
*/
|
||||
fun ContactSearchViewModel.bindSearchInProgressToLifecycle(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
onSearchInProgressChanged: (Boolean) -> Unit
|
||||
) {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
searchInProgress.collect { onSearchInProgressChanged(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+39
@@ -208,12 +208,16 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
|
||||
private static final String TAG = Log.tag(ConversationListFragment.class);
|
||||
|
||||
private static final long SEARCH_LOADING_SHOW_DELAY_MS = 150L;
|
||||
|
||||
private static final int MAX_CHATS_ABOVE_FOLD = 7;
|
||||
private static final int MAX_CONTACTS_ABOVE_FOLD = 5;
|
||||
private static final int MAX_GROUP_MEMBERSHIPS_ABOVE_FOLD = 5;
|
||||
private View coordinator;
|
||||
private RecyclerView chatFolderList;
|
||||
private RecyclerView list;
|
||||
private View searchLoading;
|
||||
private boolean searchInProgress;
|
||||
private Stub<ComposeView> bannerView;
|
||||
private ConversationListFilterPullView pullView;
|
||||
private AppBarLayout pullViewAppBarLayout;
|
||||
@@ -221,6 +225,11 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
private RecyclerView.Adapter activeAdapter;
|
||||
private ConversationListAdapter defaultAdapter;
|
||||
private PagingMappingAdapter<ContactSearchKey> searchAdapter;
|
||||
private final Runnable showSearchLoadingRunnable = () -> {
|
||||
if (searchLoading != null && searchInProgress && activeAdapter == searchAdapter) {
|
||||
searchLoading.setVisibility(View.VISIBLE);
|
||||
}
|
||||
};
|
||||
private SnapToTopDataObserver snapToTopDataObserver;
|
||||
private Drawable archiveDrawable;
|
||||
private AppForegroundObserver.Listener appForegroundObserver;
|
||||
@@ -317,6 +326,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
|
||||
chatFolderList = view.findViewById(R.id.chat_folder_list);
|
||||
list = view.findViewById(R.id.list);
|
||||
searchLoading = view.findViewById(R.id.search_loading);
|
||||
bottomActionBar = view.findViewById(R.id.conversation_list_bottom_action_bar);
|
||||
bannerView = new Stub<>(view.findViewById(R.id.banner_compose_view));
|
||||
voiceNotePlayerViewStub = new Stub<>(view.findViewById(R.id.voice_note_player));
|
||||
@@ -352,6 +362,11 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
);
|
||||
|
||||
ContactSearchViewModelKt.bindAdapterToLifecycle(contactSearchViewModel, getViewLifecycleOwner(), searchAdapter, this::mapSearchStateToConfiguration);
|
||||
ContactSearchViewModelKt.bindSearchInProgressToLifecycle(contactSearchViewModel, getViewLifecycleOwner(), inProgress -> {
|
||||
searchInProgress = inProgress;
|
||||
updateSearchLoadingVisibility();
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
initializeSearchFilterListener();
|
||||
|
||||
@@ -517,6 +532,11 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
defaultAdapter = null;
|
||||
searchAdapter = null;
|
||||
|
||||
if (searchLoading != null) {
|
||||
searchLoading.removeCallbacks(showSearchLoadingRunnable);
|
||||
searchLoading = null;
|
||||
}
|
||||
|
||||
dismissProgressDialog();
|
||||
|
||||
super.onDestroyView();
|
||||
@@ -956,6 +976,25 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
} else {
|
||||
defaultAdapter.unregisterAdapterDataObserver(snapToTopDataObserver);
|
||||
}
|
||||
|
||||
updateSearchLoadingVisibility();
|
||||
}
|
||||
|
||||
private void updateSearchLoadingVisibility() {
|
||||
if (searchLoading == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean shouldShow = searchInProgress && activeAdapter == searchAdapter;
|
||||
searchLoading.removeCallbacks(showSearchLoadingRunnable);
|
||||
|
||||
if (shouldShow) {
|
||||
if (searchLoading.getVisibility() != View.VISIBLE) {
|
||||
searchLoading.postDelayed(showSearchLoadingRunnable, SEARCH_LOADING_SHOW_DELAY_MS);
|
||||
}
|
||||
} else {
|
||||
searchLoading.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeTypingObserver() {
|
||||
|
||||
@@ -69,55 +69,70 @@ class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
"""
|
||||
)
|
||||
|
||||
// The FTS MATCH can hit a huge number of rows for broad/short queries. We defer all of the heavy per-row work (column
|
||||
// materialization and the thread join) until after the LIMIT has cut the result set down to 500 by selecting only the
|
||||
// matching row ids in the inner query, then joining back for the displayed columns. Note that snippet() is intentionally
|
||||
// not computed here: it can only be evaluated cheaply inline during the FTS scan (over every match), so it's generated in
|
||||
// app code from the body of just the returned rows instead. See SearchRepository.
|
||||
@Language("sql")
|
||||
private const val MESSAGES_QUERY = """
|
||||
SELECT
|
||||
${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} AS $CONVERSATION_RECIPIENT,
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID} AS $MESSAGE_RECIPIENT,
|
||||
snippet($FTS_TABLE_NAME, -1, '', '', '$SNIPPET_WRAP', 7) AS $SNIPPET,
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED},
|
||||
$FTS_TABLE_NAME.$THREAD_ID,
|
||||
$FTS_TABLE_NAME.$BODY,
|
||||
$FTS_TABLE_NAME.$ID AS $MESSAGE_ID,
|
||||
1 AS $IS_MMS
|
||||
FROM
|
||||
$FTS_TABLE_NAME
|
||||
CROSS JOIN ${MessageTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = $FTS_TABLE_NAME.$ID
|
||||
INNER JOIN ${ThreadTable.TABLE_NAME} ON $FTS_TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID}
|
||||
WHERE
|
||||
$FTS_TABLE_NAME MATCH ? AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MessageTypes.GROUP_V2_BIT} = 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} = 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.SCHEDULED_DATE} < 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.LATEST_REVISION_ID} IS NULL
|
||||
ORDER BY ${MessageTable.DATE_RECEIVED} DESC
|
||||
LIMIT 500
|
||||
SELECT
|
||||
${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} AS $CONVERSATION_RECIPIENT,
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID} AS $MESSAGE_RECIPIENT,
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.BODY},
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED},
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID},
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.ID} AS $MESSAGE_ID,
|
||||
1 AS $IS_MMS
|
||||
FROM (
|
||||
SELECT $FTS_TABLE_NAME.$ID AS mid
|
||||
FROM
|
||||
$FTS_TABLE_NAME
|
||||
CROSS JOIN ${MessageTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = $FTS_TABLE_NAME.$ID
|
||||
INNER JOIN ${ThreadTable.TABLE_NAME} ON $FTS_TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID}
|
||||
WHERE
|
||||
$FTS_TABLE_NAME MATCH ? AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MessageTypes.GROUP_V2_BIT} = 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} = 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.SCHEDULED_DATE} < 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.LATEST_REVISION_ID} IS NULL
|
||||
ORDER BY ${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED} DESC
|
||||
LIMIT 500
|
||||
) AS limited
|
||||
JOIN ${MessageTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = limited.mid
|
||||
JOIN ${ThreadTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID}
|
||||
ORDER BY ${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED} DESC
|
||||
"""
|
||||
|
||||
@Language("sql")
|
||||
private const val MESSAGES_FOR_THREAD_QUERY = """
|
||||
SELECT
|
||||
${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} AS $CONVERSATION_RECIPIENT,
|
||||
SELECT
|
||||
${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} AS $CONVERSATION_RECIPIENT,
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID} AS $MESSAGE_RECIPIENT,
|
||||
snippet($FTS_TABLE_NAME, -1, '', '', '$SNIPPET_WRAP', 7) AS $SNIPPET,
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED},
|
||||
$FTS_TABLE_NAME.$THREAD_ID,
|
||||
$FTS_TABLE_NAME.$BODY,
|
||||
$FTS_TABLE_NAME.$ID AS $MESSAGE_ID,
|
||||
1 AS $IS_MMS
|
||||
FROM
|
||||
$FTS_TABLE_NAME
|
||||
CROSS JOIN ${MessageTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = $FTS_TABLE_NAME.$ID
|
||||
INNER JOIN ${ThreadTable.TABLE_NAME} ON $FTS_TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID}
|
||||
WHERE
|
||||
$FTS_TABLE_NAME MATCH ? AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} = ? AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MessageTypes.GROUP_V2_BIT} = 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} = 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.SCHEDULED_DATE} < 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.LATEST_REVISION_ID} IS NULL
|
||||
ORDER BY ${MessageTable.DATE_RECEIVED} DESC
|
||||
LIMIT 500
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.BODY},
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED},
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID},
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.ID} AS $MESSAGE_ID,
|
||||
1 AS $IS_MMS
|
||||
FROM (
|
||||
SELECT $FTS_TABLE_NAME.$ID AS mid
|
||||
FROM
|
||||
$FTS_TABLE_NAME
|
||||
CROSS JOIN ${MessageTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = $FTS_TABLE_NAME.$ID
|
||||
INNER JOIN ${ThreadTable.TABLE_NAME} ON $FTS_TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID}
|
||||
WHERE
|
||||
$FTS_TABLE_NAME MATCH ? AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} = ? AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MessageTypes.GROUP_V2_BIT} = 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} = 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.SCHEDULED_DATE} < 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.LATEST_REVISION_ID} IS NULL
|
||||
ORDER BY ${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED} DESC
|
||||
LIMIT 500
|
||||
) AS limited
|
||||
JOIN ${MessageTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = limited.mid
|
||||
JOIN ${ThreadTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID}
|
||||
ORDER BY ${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED} DESC
|
||||
"""
|
||||
}
|
||||
|
||||
@@ -160,8 +175,8 @@ class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
|
||||
@Language("sql")
|
||||
val filteredQuery = MESSAGES_QUERY.replace(
|
||||
"ORDER BY ${MessageTable.DATE_RECEIVED} DESC",
|
||||
"$extraConditions ORDER BY ${MessageTable.DATE_RECEIVED} DESC"
|
||||
"${MessageTable.TABLE_NAME}.${MessageTable.LATEST_REVISION_ID} IS NULL",
|
||||
"${MessageTable.TABLE_NAME}.${MessageTable.LATEST_REVISION_ID} IS NULL$extraConditions"
|
||||
)
|
||||
|
||||
return readableDatabase.rawQuery(filteredQuery, args.toTypedArray())
|
||||
|
||||
@@ -9,10 +9,10 @@ import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.CursorUtil;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
@@ -46,6 +46,7 @@ import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
@@ -60,6 +61,8 @@ public class SearchRepository {
|
||||
|
||||
private static final String TAG = Log.tag(SearchRepository.class);
|
||||
|
||||
private static final int MAX_SNIPPET_SIZE = 100;
|
||||
|
||||
private final Context context;
|
||||
private final String noteToSelfTitle;
|
||||
private final SearchTable searchDatabase;
|
||||
@@ -183,6 +186,12 @@ public class SearchRepository {
|
||||
results = readToList(cursor, new MessageModelBuilder());
|
||||
}
|
||||
|
||||
if (results.isEmpty()) {
|
||||
return results;
|
||||
}
|
||||
|
||||
List<String> snippetQueries = tokenizeQuery(query);
|
||||
|
||||
List<Long> messageIds = new LinkedList<>();
|
||||
for (MessageResult result : results) {
|
||||
if (result.isMms()) {
|
||||
@@ -190,121 +199,65 @@ public class SearchRepository {
|
||||
}
|
||||
}
|
||||
|
||||
if (messageIds.isEmpty()) {
|
||||
return results;
|
||||
}
|
||||
|
||||
Map<Long, BodyRangeList> bodyRanges = SignalDatabase.messages().getBodyRangesForMessages(messageIds);
|
||||
Map<Long, List<Mention>> mentions = SignalDatabase.mentions().getMentionsForMessages(messageIds);
|
||||
|
||||
if (bodyRanges.isEmpty() && mentions.isEmpty()) {
|
||||
return results;
|
||||
}
|
||||
Map<Long, BodyRangeList> bodyRanges = messageIds.isEmpty() ? Collections.emptyMap() : SignalDatabase.messages().getBodyRangesForMessages(messageIds);
|
||||
Map<Long, List<Mention>> mentions = messageIds.isEmpty() ? Collections.emptyMap() : SignalDatabase.mentions().getMentionsForMessages(messageIds);
|
||||
|
||||
List<MessageResult> updatedResults = new ArrayList<>(results.size());
|
||||
for (MessageResult result : results) {
|
||||
if (bodyRanges.containsKey(result.getMessageId()) || mentions.containsKey(result.getMessageId())) {
|
||||
CharSequence body = result.getBody();
|
||||
CharSequence bodySnippet = result.getBodySnippet();
|
||||
CharSequence updatedBody = body;
|
||||
List<BodyAdjustment> bodyAdjustments = Collections.emptyList();
|
||||
CharSequence updatedSnippet = bodySnippet;
|
||||
List<BodyAdjustment> snippetAdjustments = Collections.emptyList();
|
||||
List<Mention> messageMentions = mentions.get(result.getMessageId());
|
||||
BodyRangeList ranges = bodyRanges.get(result.getMessageId());
|
||||
CharSequence updatedBody = result.getBody();
|
||||
List<BodyAdjustment> bodyAdjustments = Collections.emptyList();
|
||||
List<Mention> messageMentions = mentions.get(result.getMessageId());
|
||||
BodyRangeList ranges = bodyRanges.get(result.getMessageId());
|
||||
|
||||
if (messageMentions != null) {
|
||||
MentionUtil.UpdatedBodyAndMentions bodyMentionUpdate = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, body, messageMentions);
|
||||
updatedBody = Objects.requireNonNull(bodyMentionUpdate.getBody());
|
||||
bodyAdjustments = bodyMentionUpdate.getBodyAdjustments();
|
||||
|
||||
MentionUtil.UpdatedBodyAndMentions snippetMentionUpdate = updateSnippetWithDisplayNames(body, bodySnippet, messageMentions);
|
||||
updatedSnippet = Objects.requireNonNull(snippetMentionUpdate.getBody());
|
||||
snippetAdjustments = snippetMentionUpdate.getBodyAdjustments();
|
||||
}
|
||||
|
||||
if (ranges != null) {
|
||||
updatedBody = SpannableString.valueOf(updatedBody);
|
||||
MessageStyler.style(result.getReceivedTimestampMs(), BodyRangeUtil.adjustBodyRanges(ranges, bodyAdjustments), (Spannable) updatedBody);
|
||||
|
||||
updatedSnippet = SpannableString.valueOf(updatedSnippet);
|
||||
updateSnippetWithStyles(result.getReceivedTimestampMs(), updatedBody, (SpannableString) updatedSnippet, BodyRangeUtil.adjustBodyRanges(ranges, snippetAdjustments));
|
||||
}
|
||||
|
||||
updatedResults.add(new MessageResult(result.getConversationRecipient(), result.getMessageRecipient(), updatedBody, updatedSnippet, result.getThreadId(), result.getMessageId(), result.getReceivedTimestampMs(), result.isMms()));
|
||||
} else {
|
||||
updatedResults.add(result);
|
||||
if (messageMentions != null) {
|
||||
MentionUtil.UpdatedBodyAndMentions bodyMentionUpdate = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, updatedBody, messageMentions);
|
||||
updatedBody = Objects.requireNonNull(bodyMentionUpdate.getBody());
|
||||
bodyAdjustments = bodyMentionUpdate.getBodyAdjustments();
|
||||
}
|
||||
|
||||
if (ranges != null) {
|
||||
updatedBody = SpannableString.valueOf(updatedBody);
|
||||
MessageStyler.style(result.getReceivedTimestampMs(), BodyRangeUtil.adjustBodyRanges(ranges, bodyAdjustments), (Spannable) updatedBody);
|
||||
}
|
||||
|
||||
CharSequence updatedSnippet = makeSnippet(snippetQueries, updatedBody);
|
||||
|
||||
updatedResults.add(new MessageResult(result.getConversationRecipient(), result.getMessageRecipient(), updatedBody, updatedSnippet, result.getThreadId(), result.getMessageId(), result.getReceivedTimestampMs(), result.isMms()));
|
||||
}
|
||||
|
||||
return updatedResults;
|
||||
}
|
||||
|
||||
private @NonNull MentionUtil.UpdatedBodyAndMentions updateSnippetWithDisplayNames(@NonNull CharSequence body, @NonNull CharSequence bodySnippet, @NonNull List<Mention> mentions) {
|
||||
CharSequence cleanSnippet = bodySnippet;
|
||||
int startOffset = 0;
|
||||
|
||||
if (StringUtil.startsWith(cleanSnippet, SNIPPET_WRAP)) {
|
||||
cleanSnippet = cleanSnippet.subSequence(SNIPPET_WRAP.length(), cleanSnippet.length());
|
||||
startOffset = SNIPPET_WRAP.length();
|
||||
}
|
||||
|
||||
if (StringUtil.endsWith(cleanSnippet, SNIPPET_WRAP)) {
|
||||
cleanSnippet = cleanSnippet.subSequence(0, cleanSnippet.length() - SNIPPET_WRAP.length());
|
||||
}
|
||||
|
||||
int startIndex = TextUtils.indexOf(body, 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()));
|
||||
}
|
||||
}
|
||||
|
||||
return MentionUtil.updateBodyAndMentionsWithDisplayNames(context, bodySnippet, adjustMentions);
|
||||
} else {
|
||||
return MentionUtil.updateBodyAndMentionsWithDisplayNames(context, bodySnippet, Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSnippetWithStyles(long id, @NonNull CharSequence body, @NonNull SpannableString bodySnippet, @NonNull BodyRangeList bodyRanges) {
|
||||
CharSequence cleanSnippet = bodySnippet;
|
||||
int startOffset = 0;
|
||||
|
||||
if (StringUtil.startsWith(cleanSnippet, SNIPPET_WRAP)) {
|
||||
cleanSnippet = cleanSnippet.subSequence(SNIPPET_WRAP.length(), cleanSnippet.length());
|
||||
startOffset = SNIPPET_WRAP.length();
|
||||
}
|
||||
|
||||
if (StringUtil.endsWith(cleanSnippet, SNIPPET_WRAP)) {
|
||||
cleanSnippet = cleanSnippet.subSequence(0, cleanSnippet.length() - SNIPPET_WRAP.length());
|
||||
}
|
||||
|
||||
int startIndex = TextUtils.indexOf(body, cleanSnippet);
|
||||
|
||||
if (startIndex != -1) {
|
||||
List<BodyRangeList.BodyRange> newRanges = new ArrayList<>(bodyRanges.ranges.size());
|
||||
for (BodyRangeList.BodyRange range : bodyRanges.ranges) {
|
||||
int adjustedStart = range.start - startIndex + startOffset;
|
||||
if (adjustedStart >= 0 && adjustedStart + range.length <= bodySnippet.length()) {
|
||||
newRanges.add(range.newBuilder().start(adjustedStart).build());
|
||||
}
|
||||
}
|
||||
|
||||
BodyRangeList.Builder builder = new BodyRangeList.Builder();
|
||||
builder.ranges(newRanges);
|
||||
|
||||
MessageStyler.style(id, builder.build(), bodySnippet);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull List<MessageResult> queryMessages(@NonNull String query, long threadId) {
|
||||
List<MessageResult> results;
|
||||
try (Cursor cursor = searchDatabase.queryMessages(query, threadId)) {
|
||||
return readToList(cursor, new MessageModelBuilder());
|
||||
results = readToList(cursor, new MessageModelBuilder());
|
||||
}
|
||||
|
||||
if (results.isEmpty()) {
|
||||
return results;
|
||||
}
|
||||
|
||||
List<String> snippetQueries = tokenizeQuery(query);
|
||||
List<MessageResult> updatedResults = new ArrayList<>(results.size());
|
||||
for (MessageResult result : results) {
|
||||
CharSequence snippet = makeSnippet(snippetQueries, result.getBody());
|
||||
updatedResults.add(new MessageResult(result.getConversationRecipient(), result.getMessageRecipient(), result.getBody(), snippet, result.getThreadId(), result.getMessageId(), result.getReceivedTimestampMs(), result.isMms()));
|
||||
}
|
||||
|
||||
return updatedResults;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static @NonNull List<String> tokenizeQuery(@NonNull String query) {
|
||||
List<String> tokens = new ArrayList<>();
|
||||
for (String part : query.split("\\s+")) {
|
||||
String trimmed = part.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
tokens.add(trimmed);
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private @NonNull List<MessageResult> queryMentions(@NonNull List<String> cleanQueries) {
|
||||
@@ -371,14 +324,15 @@ public class SearchRepository {
|
||||
return results;
|
||||
}
|
||||
|
||||
private @NonNull CharSequence makeSnippet(@NonNull List<String> queries, @NonNull CharSequence styledBody) {
|
||||
@VisibleForTesting
|
||||
static @NonNull CharSequence makeSnippet(@NonNull List<String> queries, @NonNull CharSequence styledBody) {
|
||||
if (styledBody.length() < 50) {
|
||||
return styledBody;
|
||||
}
|
||||
|
||||
String lowerBody = styledBody.toString().toLowerCase();
|
||||
String lowerBody = styledBody.toString().toLowerCase(Locale.ROOT);
|
||||
for (String query : queries) {
|
||||
int foundIndex = lowerBody.indexOf(query.toLowerCase());
|
||||
int foundIndex = lowerBody.indexOf(query.toLowerCase(Locale.ROOT));
|
||||
if (foundIndex != -1) {
|
||||
int snippetStart = Math.max(0, Math.max(TextUtils.lastIndexOf(styledBody,' ', foundIndex - 5) + 1, foundIndex - 15));
|
||||
int lastSpace = TextUtils.indexOf(styledBody, ' ', foundIndex + 30);
|
||||
@@ -390,7 +344,15 @@ public class SearchRepository {
|
||||
}
|
||||
}
|
||||
|
||||
return styledBody;
|
||||
if (styledBody.length() <= MAX_SNIPPET_SIZE) {
|
||||
return styledBody;
|
||||
}
|
||||
|
||||
int lastSpace = TextUtils.lastIndexOf(styledBody, ' ', MAX_SNIPPET_SIZE);
|
||||
int snippetEnd = lastSpace > 0 ? lastSpace : MAX_SNIPPET_SIZE;
|
||||
|
||||
return new SpannableStringBuilder().append(styledBody.subSequence(0, snippetEnd))
|
||||
.append(SNIPPET_WRAP);
|
||||
}
|
||||
|
||||
private @NonNull <T> List<T> readToList(@Nullable Cursor cursor, @NonNull ModelBuilder<T> builder) {
|
||||
@@ -489,7 +451,6 @@ public class SearchRepository {
|
||||
Recipient conversationRecipient = Recipient.resolved(conversationRecipientId);
|
||||
Recipient messageRecipient = Recipient.resolved(messageRecipientId);
|
||||
String body = CursorUtil.requireString(cursor, SearchTable.BODY);
|
||||
String bodySnippet = CursorUtil.requireString(cursor, SearchTable.SNIPPET);
|
||||
long receivedMs = CursorUtil.requireLong(cursor, MessageTable.DATE_RECEIVED);
|
||||
long threadId = CursorUtil.requireLong(cursor, MessageTable.THREAD_ID);
|
||||
int messageId = CursorUtil.requireInt(cursor, SearchTable.MESSAGE_ID);
|
||||
@@ -499,11 +460,7 @@ public class SearchRepository {
|
||||
body = "";
|
||||
}
|
||||
|
||||
if (bodySnippet == null) {
|
||||
bodySnippet = "";
|
||||
}
|
||||
|
||||
return new MessageResult(conversationRecipient, messageRecipient, body, bodySnippet, threadId, messageId, receivedMs, isMms);
|
||||
return new MessageResult(conversationRecipient, messageRecipient, body, body, threadId, messageId, receivedMs, isMms);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user