From ba477dcb5752bf70a60a00720fbdd657213259de Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 30 Jul 2022 19:16:04 +0530 Subject: [PATCH] Use paging library in SearchFragment. --- .../fragments/list/search/SearchFragment.java | 44 ++++---- .../list/search/SearchPagingSource.kt | 22 ++++ .../list/search/SearchRemoteMediator.kt | 23 ++++ .../fragments/list/search/SearchService.kt | 16 +++ .../fragments/list/search/SearchViewModel.kt | 101 ++++++++++++++++++ .../fragments/list/search/SuggestionItem.java | 12 ++- .../list/search/SuggestionListAdapter.java | 4 +- 7 files changed, 194 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchPagingSource.kt create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchRemoteMediator.kt create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchService.kt create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchViewModel.kt diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 44f8328a5ea..b37dc3c446a 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -172,7 +172,7 @@ public class SearchFragment extends BaseListFragmentemptyList) + return Single.just(Collections.emptyList()) .toObservable() .materialize(); } @@ -803,9 +803,7 @@ protected void doInitialLoadLogic() { // no-op } - private void search(final String theSearchString, - final String[] theContentFilter, - final String theSortFilter) { + private void search(final String theSearchString) { if (DEBUG) { Log.d(TAG, "search() called with: query = [" + theSearchString + "]"); } @@ -922,18 +920,16 @@ private void changeContentFilter(final MenuItem item, final List theCont contentFilter = new String[]{theContentFilter.get(0)}; if (!TextUtils.isEmpty(searchString)) { - search(searchString, contentFilter, sortFilter); + search(searchString); } } - private void setQuery(final int theServiceId, - final String theSearchString, - final String[] theContentFilter, - final String theSortFilter) { + private void setQuery(final int theServiceId, final String theSearchString, + final String[] theContentFilter) { serviceId = theServiceId; searchString = theSearchString; contentFilter = theContentFilter; - sortFilter = theSortFilter; + sortFilter = ""; } /*////////////////////////////////////////////////////////////////////////// @@ -1019,7 +1015,7 @@ private void handleSearchSuggestion() { searchBinding.correctSuggestion.setOnClickListener(v -> { searchBinding.correctSuggestion.setVisibility(View.GONE); - search(searchSuggestion, contentFilter, sortFilter); + search(searchSuggestion); searchEditText.setText(searchSuggestion); }); @@ -1068,13 +1064,13 @@ public int getSuggestionMovementFlags(@NonNull final RecyclerView.ViewHolder vie } final SuggestionItem item = suggestionListAdapter.getItem(position); - return item.fromHistory ? makeMovementFlags(0, + return item.isFromHistory() ? makeMovementFlags(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0; } public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) { final int position = viewHolder.getBindingAdapterPosition(); - final String query = suggestionListAdapter.getItem(position).query; + final String query = suggestionListAdapter.getItem(position).getQuery(); final Disposable onDelete = historyRecordManager.deleteSearchHistory(query) .observeOn(AndroidSchedulers.mainThread()) .subscribe( diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchPagingSource.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchPagingSource.kt new file mode 100644 index 00000000000..5dd9c8b1f86 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchPagingSource.kt @@ -0,0 +1,22 @@ +package org.schabi.newpipe.fragments.list.search + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import org.schabi.newpipe.extractor.Page +import org.schabi.newpipe.extractor.search.SearchInfo +import org.schabi.newpipe.util.ExtractorHelper + +class SearchPagingSource( + private val serviceId: Int, + private val query: String, + private val contentFilter: List, + private val sortFilter: String, +) : PagingSource() { + override fun getRefreshKey(state: PagingState): Page? { + TODO("Not yet implemented") + } + + override suspend fun load(params: LoadParams): LoadResult { + TODO("Not yet implemented") + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchRemoteMediator.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchRemoteMediator.kt new file mode 100644 index 00000000000..b50f1ac0689 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchRemoteMediator.kt @@ -0,0 +1,23 @@ +package org.schabi.newpipe.fragments.list.search + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import org.schabi.newpipe.extractor.Page +import org.schabi.newpipe.extractor.search.SearchInfo + +@OptIn(ExperimentalPagingApi::class) +class SearchRemoteMediator( + private val serviceId: Int, + private val query: String, + private val contentFilter: List, + private val sortFilter: String +) : RemoteMediator() { + override suspend fun load( + loadType: LoadType, + state: PagingState + ): MediatorResult { + TODO("Not yet implemented") + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchService.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchService.kt new file mode 100644 index 00000000000..d61e7f4ec06 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchService.kt @@ -0,0 +1,16 @@ +package org.schabi.newpipe.fragments.list.search + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.schabi.newpipe.extractor.search.SearchInfo + +fun getSearchResultStream( + serviceId: Int, query: String, contentFilter: List, sortFilter: String +): Flow> { + return Pager( + config = PagingConfig(pageSize = 50, enablePlaceholders = false), + pagingSourceFactory = { SearchPagingSource(serviceId, query, contentFilter, sortFilter) } + ).flow +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchViewModel.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchViewModel.kt new file mode 100644 index 00000000000..19050b168ff --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchViewModel.kt @@ -0,0 +1,101 @@ +package org.schabi.newpipe.fragments.list.search + +import android.app.Application +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.subjects.PublishSubject +import org.schabi.newpipe.App +import org.schabi.newpipe.local.history.HistoryRecordManager +import org.schabi.newpipe.util.ExtractorHelper +import java.util.concurrent.TimeUnit + +class SearchViewModel( + application: Application, + private val serviceId: Int, + private val showLocalSuggestions: Boolean, + private val showRemoteSuggestions: Boolean +) : ViewModel() { + private val historyRecordManager = HistoryRecordManager(application) + private val suggestionPublisher = PublishSubject.create() + + private val suggestionMutableLiveData = MutableLiveData() + val suggestionLiveData: LiveData get() = suggestionMutableLiveData + + private val suggestionDisposable = suggestionPublisher + .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) + .switchMap { query: String -> + // Only show remote suggestions if they are enabled in settings and + // the query length is at least THRESHOLD_NETWORK_SUGGESTION + val shallShowRemoteSuggestionsNow = (showRemoteSuggestions && query.length >= THRESHOLD_NETWORK_SUGGESTION) + if (showLocalSuggestions && shallShowRemoteSuggestionsNow) { + Observable.zip( + getLocalSuggestionsObservable(query, 3), + getRemoteSuggestionsObservable(query) + ) { local, remote -> (local + remote).distinct() } + } else if (showLocalSuggestions) { + getLocalSuggestionsObservable(query, 25) + } else if (shallShowRemoteSuggestionsNow) { + getRemoteSuggestionsObservable(query) + } else { + Single.just(emptyList()).toObservable() + } + } + .subscribe({ suggestions -> + suggestions.forEach { suggestionMutableLiveData.postValue(SuggestionItemSuccess(it)) } + }) { + suggestionMutableLiveData.postValue(SuggestionItemError(it)) + } + + override fun onCleared() { + suggestionDisposable.dispose() + } + + fun updateSearchQuery(query: String) { + suggestionPublisher.onNext(query) + } + + private fun getLocalSuggestionsObservable(query: String, similarQueryLimit: Int): Observable> { + return historyRecordManager.getRelatedSearches(query, similarQueryLimit, 25) + .toObservable() + .map { entries -> entries.map { SuggestionItem(true, it) } } + } + + private fun getRemoteSuggestionsObservable(query: String): Observable> { + return ExtractorHelper.suggestionsFor(serviceId, query) + .toObservable() + .map { entries -> entries.map { SuggestionItem(false, it) } } + } + + companion object { + /** + * How much time have to pass without emitting a item (i.e. the user stop typing) + * to fetch/show the suggestions, in milliseconds. + */ + private const val SUGGESTIONS_DEBOUNCE = 120L // ms + + /** + * The suggestions will only be fetched from network if the query meet this threshold (>=). + * (local ones will be fetched regardless of the length) + */ + private const val THRESHOLD_NETWORK_SUGGESTION = 1 + + fun getFactory( + serviceId: Int, + showLocalSuggestions: Boolean, + showRemoteSuggestions: Boolean + ) = viewModelFactory { + initializer { + SearchViewModel(App.getApp(), serviceId, showLocalSuggestions, showRemoteSuggestions) + } + } + } +} + +sealed class SuggestionItemResponse +class SuggestionItemSuccess(val item: SuggestionItem) : SuggestionItemResponse() +class SuggestionItemError(val throwable: Throwable) : SuggestionItemResponse() diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java index 83f68dbb571..82e17364e97 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java @@ -3,14 +3,22 @@ import androidx.annotation.NonNull; public class SuggestionItem { - final boolean fromHistory; - public final String query; + private final boolean fromHistory; + private final String query; public SuggestionItem(final boolean fromHistory, final String query) { this.fromHistory = fromHistory; this.query = query; } + public boolean isFromHistory() { + return fromHistory; + } + + public String getQuery() { + return query; + } + @Override public boolean equals(final Object o) { if (o instanceof SuggestionItem) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java index fb983b01e26..4971b2f9c07 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java @@ -109,8 +109,8 @@ private SuggestionItemHolder(final View rootView) { } private void updateFrom(final SuggestionItem item) { - suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId); - itemSuggestionQuery.setText(item.query); + suggestionIcon.setImageResource(item.isFromHistory() ? historyResId : searchResId); + itemSuggestionQuery.setText(item.getQuery()); } } }