package org.thoughtcrime.securesms.search; import android.content.Context; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import com.b44t.messenger.DcChatlist; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcLot; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.search.model.SearchResult; import org.thoughtcrime.securesms.util.Util; class SearchViewModel extends ViewModel { private static final String TAG = "SearchViewModel"; private final ObservingLiveData searchResult; private String lastQuery; private final Context appContext; private final DcContext dcContext; private boolean forwarding = false; private final Object bgSearchLock = new Object(); private boolean inBgSearch; private boolean needsAnotherBgSearch; SearchViewModel(@NonNull Context context) { this.appContext = context.getApplicationContext(); this.dcContext = DcHelper.getContext(appContext); this.searchResult = new ObservingLiveData(); } LiveData getSearchResult() { return searchResult; } public void setForwardingMode(boolean forwarding) { this.forwarding = forwarding; } void updateQuery(String query) { lastQuery = query; updateQuery(); } public void updateQuery() { synchronized (bgSearchLock) { if (inBgSearch) { Log.i(TAG, "... search call debounced"); needsAnotherBgSearch = true; return; } inBgSearch = true; } Util.runOnBackground( () -> { while (true) { Log.i(TAG, "... executing debounced search call"); queryAndCallback(lastQuery, searchResult::postValue); synchronized (bgSearchLock) { if (!needsAnotherBgSearch) { inBgSearch = false; return; } needsAnotherBgSearch = false; } Util.sleep(100); } }); } private void queryAndCallback(@NonNull String query, @NonNull SearchViewModel.Callback callback) { int overallCnt = 0; if (TextUtils.isEmpty(query)) { callback.onResult(SearchResult.EMPTY); return; } QrInviteData qrInviteData = null; if (!forwarding && query.contains(":")) { DcLot qrParsed = dcContext.checkQr(query); qrInviteData = QrInviteData.from(appContext, dcContext, qrParsed, query); } // #1 search for chats long startMs = System.currentTimeMillis(); DcChatlist conversations = dcContext.getChatlist(forwarding ? DcContext.DC_GCL_FOR_FORWARDING : 0, query, 0); overallCnt += conversations.getCnt(); Log.i(TAG, "⏰ getChatlist(" + query + "): " + (System.currentTimeMillis() - startMs) + "ms"); // #2 search for contacts if (!query.equals(lastQuery) && overallCnt > 0) { Log.i(TAG, "... skipping getContacts() and searchMsgs(), more recent search pending"); callback.onResult( new SearchResult(query, new int[0], conversations, new int[0], qrInviteData)); return; } startMs = System.currentTimeMillis(); int[] contacts = dcContext.getContacts(DcContext.DC_GCL_ADD_SELF, query); overallCnt += contacts.length; Log.i(TAG, "⏰ getContacts(" + query + "): " + (System.currentTimeMillis() - startMs) + "ms"); // #3 search for messages if (forwarding) { Log.i(TAG, "... searchMsgs() disabled by caller"); callback.onResult(new SearchResult(query, contacts, conversations, new int[0], qrInviteData)); return; } if (query.length() <= 1) { Log.i(TAG, "... skipping searchMsgs(), string too short"); callback.onResult(new SearchResult(query, contacts, conversations, new int[0], qrInviteData)); return; } if (!query.equals(lastQuery) && overallCnt > 0) { Log.i(TAG, "... skipping searchMsgs(), more recent search pending"); callback.onResult(new SearchResult(query, contacts, conversations, new int[0], qrInviteData)); return; } startMs = System.currentTimeMillis(); int[] messages = dcContext.searchMsgs(0, query); Log.i(TAG, "⏰ searchMsgs(" + query + "): " + (System.currentTimeMillis() - startMs) + "ms"); callback.onResult(new SearchResult(query, contacts, conversations, messages, qrInviteData)); } @NonNull String getLastQuery() { return lastQuery == null ? "" : lastQuery; } @Override protected void onCleared() {} private static class ObservingLiveData extends MutableLiveData {} public static class Factory extends ViewModelProvider.NewInstanceFactory { private final Context context; public Factory(@NonNull Context context) { this.context = context; } @NonNull @Override public T create(@NonNull Class modelClass) { return modelClass.cast(new SearchViewModel(context)); } } public interface Callback { void onResult(@NonNull SearchResult result); } }