From c77ed747fe3f02931d431c3d2f9894a58d9dc3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=C3=A1n=20Mac=20Gillicuddy?= Date: Tue, 16 Jun 2020 14:58:48 +0100 Subject: [PATCH] #3772 Convert SearchImagesFragment to use Pagination (#3779) --- .../main/java/fr/free/nrw/commons/Media.java | 2 +- .../nrw/commons/di/FragmentBuilderModule.java | 4 +- .../nrw/commons/explore/BaseSearchFragment.kt | 29 +- .../commons/explore/BaseSearchPresenter.kt | 22 +- .../nrw/commons/explore/SearchActivity.java | 47 ++- .../explore/SearchDataSourceFactory.kt | 6 +- .../commons/explore/SearchFragmentContract.kt | 3 +- .../free/nrw/commons/explore/SearchModule.kt | 11 +- .../categories/SearchCategoryFragment.kt | 2 + .../depictions/SearchDepictionsFragment.kt | 4 +- .../explore/images/SearchImageFragment.java | 288 ------------------ .../explore/images/SearchImagesAdapter.kt | 27 -- .../images/SearchImagesAdapterDelegates.kt | 24 -- .../commons/explore/media/MediaConverter.kt | 9 + .../explore/media/PageableMediaDataSource.kt | 34 +++ .../explore/media/SearchMediaAdapter.kt | 49 +++ .../explore/media/SearchMediaFragment.kt | 57 ++++ .../media/SearchMediaFragmentContract.kt | 10 + .../media/SearchMediaFragmentPresenter.kt | 14 + .../explore/media/SimpleDataObserver.kt | 40 +++ .../free/nrw/commons/media/MediaClient.java | 17 +- .../commons/media/MediaDetailInterface.java | 3 +- .../nrw/commons/media/MediaInterface.java | 5 +- .../explore/BaseSearchPresenterTest.kt | 23 +- .../commons/explore/PageableDataSourceTest.kt | 4 +- .../media/PageableMediaDataSourceTest.kt | 71 +++++ 26 files changed, 386 insertions(+), 419 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesAdapterDelegates.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaDataSource.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaAdapter.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaFragment.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaFragmentContract.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaFragmentPresenter.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/media/SimpleDataObserver.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/explore/media/PageableMediaDataSourceTest.kt diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index b3e18fe11..96df2a4ac 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -203,7 +203,7 @@ public class Media implements Parcelable { * @param page response from the API * @return Media object */ - @Nullable + @NonNull public static Media from(final MwQueryPage page) { final ImageInfo imageInfo = page.imageInfo(); if (imageInfo == null) { diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java index 3e25ac815..b60b68bf2 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -12,7 +12,7 @@ import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment; import fr.free.nrw.commons.depictions.subClass.SubDepictionListFragment; import fr.free.nrw.commons.explore.categories.SearchCategoryFragment; import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragment; -import fr.free.nrw.commons.explore.images.SearchImageFragment; +import fr.free.nrw.commons.explore.media.SearchMediaFragment; import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment; import fr.free.nrw.commons.media.MediaDetailFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment; @@ -58,7 +58,7 @@ public abstract class FragmentBuilderModule { abstract SubCategoryListFragment bindSubCategoryListFragment(); @ContributesAndroidInjector - abstract SearchImageFragment bindBrowseImagesListFragment(); + abstract SearchMediaFragment bindBrowseImagesListFragment(); @ContributesAndroidInjector abstract SearchCategoryFragment bindSearchCategoryListFragment(); diff --git a/app/src/main/java/fr/free/nrw/commons/explore/BaseSearchFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/BaseSearchFragment.kt index 62e6049d8..ae4c2cd9c 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/BaseSearchFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/BaseSearchFragment.kt @@ -5,8 +5,7 @@ import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE +import android.view.View.* import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData @@ -24,9 +23,10 @@ import kotlinx.android.synthetic.main.fragment_search_paginated.* abstract class BaseSearchFragment : CommonsDaggerSupportFragment(), SearchFragmentContract.View { - abstract val pagedListAdapter: PagedListAdapter + abstract val pagedListAdapter: PagedListAdapter abstract val injectedPresenter: SearchFragmentContract.Presenter abstract val emptyTemplateTextId: Int + abstract val errorTextId: Int private val loadingAdapter by lazy { FooterAdapter { injectedPresenter.retryFailedRequest() } } private val mergeAdapter by lazy { MergeAdapter(pagedListAdapter, loadingAdapter) } private var searchResults: LiveData>? = null @@ -53,9 +53,7 @@ abstract class BaseSearchFragment : CommonsDaggerSupportFragment(), this.searchResults?.removeObservers(viewLifecycleOwner) this.searchResults = searchResults searchResults.observe(viewLifecycleOwner, Observer { - pagedListAdapter.submitList(it) - contentNotFound.visibility = if (it.loadedCount == 0) VISIBLE else GONE - }) + pagedListAdapter.submitList(it) }) } override fun onAttach(context: Context) { @@ -69,25 +67,30 @@ abstract class BaseSearchFragment : CommonsDaggerSupportFragment(), injectedPresenter.onDetachView() } - override fun setEmptyViewText(query: String) { - contentNotFound.text = getString(emptyTemplateTextId, query) - } - override fun hideInitialLoadProgress() { - paginatedSearchInitialLoadProgress.visibility = View.GONE + paginatedSearchInitialLoadProgress.visibility = GONE } override fun showInitialLoadInProgress() { - paginatedSearchInitialLoadProgress.visibility = View.VISIBLE + paginatedSearchInitialLoadProgress.visibility = VISIBLE } override fun showSnackbar() { - ViewUtil.showShortSnackbar(paginatedSearchResultsList, R.string.error_loading_depictions) + ViewUtil.showShortSnackbar(paginatedSearchResultsList, errorTextId) } fun onQueryUpdated(query: String) { injectedPresenter.onQueryUpdated(query) } + + override fun showEmptyText(query: String) { + contentNotFound.text = getString(emptyTemplateTextId, query) + contentNotFound.visibility = VISIBLE + } + + override fun hideEmptyText() { + contentNotFound.visibility = GONE + } } private val Fragment.isPortrait get() = orientation == Configuration.ORIENTATION_PORTRAIT diff --git a/app/src/main/java/fr/free/nrw/commons/explore/BaseSearchPresenter.kt b/app/src/main/java/fr/free/nrw/commons/explore/BaseSearchPresenter.kt index 3f2bdfbb7..f21f391c9 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/BaseSearchPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/BaseSearchPresenter.kt @@ -14,8 +14,6 @@ abstract class BaseSearchPresenter( private val DUMMY: SearchFragmentContract.View = proxy() private var view: SearchFragmentContract.View = DUMMY - private var currentQuery: String? = null - private val compositeDisposable = CompositeDisposable() override val listFooterData = MutableLiveData>().apply { value = emptyList() } @@ -27,31 +25,30 @@ abstract class BaseSearchPresenter( pageableDataSource.loadingStates .observeOn(mainThreadScheduler) .subscribe(::onLoadingState, Timber::e), - pageableDataSource.noItemsLoadedEvent.subscribe { - setEmptyViewText() - } + pageableDataSource.noItemsLoadedQueries.subscribe(view::showEmptyText) ) } private fun onLoadingState(it: LoadingState) = when (it) { - LoadingState.Loading -> listFooterData.postValue(listOf(FooterItem.LoadingItem)) + LoadingState.Loading -> { + view.hideEmptyText() + listFooterData.postValue(listOf(FooterItem.LoadingItem)) + } LoadingState.Complete -> { listFooterData.postValue(emptyList()) view.hideInitialLoadProgress() } - LoadingState.InitialLoad -> view.showInitialLoadInProgress() + LoadingState.InitialLoad -> { + view.hideEmptyText() + view.showInitialLoadInProgress() + } LoadingState.Error -> { - setEmptyViewText() view.showSnackbar() view.hideInitialLoadProgress() listFooterData.postValue(listOf(FooterItem.RefreshItem)) } } - private fun setEmptyViewText() { - currentQuery?.let(view::setEmptyViewText) - } - override fun retryFailedRequest() { pageableDataSource.retryFailedRequest() } @@ -62,7 +59,6 @@ abstract class BaseSearchPresenter( } override fun onQueryUpdated(query: String) { - currentQuery = query pageableDataSource.onQueryUpdated(query) } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java index 39ce648fd..29d8cbe0c 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java @@ -5,6 +5,7 @@ import android.text.TextUtils; import android.view.View; import android.widget.FrameLayout; import android.widget.SearchView; +import androidx.annotation.NonNull; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -20,7 +21,9 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.category.CategoryImagesCallback; import fr.free.nrw.commons.explore.categories.SearchCategoryFragment; import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragment; -import fr.free.nrw.commons.explore.images.SearchImageFragment; +import fr.free.nrw.commons.explore.media.SearchMediaFragment; +import fr.free.nrw.commons.explore.recentsearches.RecentSearch; +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao; import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.theme.NavigationBaseActivity; @@ -28,8 +31,10 @@ import fr.free.nrw.commons.utils.FragmentUtils; import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.android.schedulers.AndroidSchedulers; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; +import javax.inject.Inject; import timber.log.Timber; /** @@ -46,14 +51,16 @@ public class SearchActivity extends NavigationBaseActivity @BindView(R.id.tab_layout) TabLayout tabLayout; @BindView(R.id.viewPager) ViewPager viewPager; - private SearchImageFragment searchImageFragment; + @Inject + RecentSearchesDao recentSearchesDao; + + private SearchMediaFragment searchMediaFragment; private SearchCategoryFragment searchCategoryFragment; private SearchDepictionsFragment searchDepictionsFragment; private RecentSearchesFragment recentSearchesFragment; private FragmentManager supportFragmentManager; private MediaDetailPagerFragment mediaDetails; ViewPagerAdapter viewPagerAdapter; - private String query; @Override protected void onCreate(Bundle savedInstanceState) { @@ -92,10 +99,10 @@ public class SearchActivity extends NavigationBaseActivity public void setTabs() { List fragmentList = new ArrayList<>(); List titleList = new ArrayList<>(); - searchImageFragment = new SearchImageFragment(); + searchMediaFragment = new SearchMediaFragment(); searchDepictionsFragment = new SearchDepictionsFragment(); searchCategoryFragment= new SearchCategoryFragment(); - fragmentList.add(searchImageFragment); + fragmentList.add(searchMediaFragment); titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase()); fragmentList.add(searchCategoryFragment); titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase()); @@ -109,9 +116,9 @@ public class SearchActivity extends NavigationBaseActivity .debounce(500, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(query -> { - this.query = query.toString(); - //update image list + //update image list if (!TextUtils.isEmpty(query)) { + saveRecentSearch(query.toString()); viewPager.setVisibility(View.VISIBLE); tabLayout.setVisibility(View.VISIBLE); searchHistoryContainer.setVisibility(View.GONE); @@ -120,8 +127,8 @@ public class SearchActivity extends NavigationBaseActivity searchDepictionsFragment.onQueryUpdated(query.toString()); } - if (FragmentUtils.isFragmentUIActive(searchImageFragment)) { - searchImageFragment.updateImageList(query.toString()); + if (FragmentUtils.isFragmentUIActive(searchMediaFragment)) { + searchMediaFragment.onQueryUpdated(query.toString()); } if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) { @@ -140,13 +147,25 @@ public class SearchActivity extends NavigationBaseActivity )); } + private void saveRecentSearch(@NonNull final String query) { + final RecentSearch recentSearch = recentSearchesDao.find(query); + // Newly searched query... + if (recentSearch == null) { + recentSearchesDao.save(new RecentSearch(null, query, new Date())); + } + else { + recentSearch.setLastSearched(new Date()); + recentSearchesDao.save(recentSearch); + } + } + /** * returns Media Object at position * @param i position of Media in the imagesRecyclerView adapter. */ @Override public Media getMediaAtPosition(int i) { - return searchImageFragment.getImageAtPosition(i); + return searchMediaFragment.getImageAtPosition(i); } /** @@ -154,7 +173,7 @@ public class SearchActivity extends NavigationBaseActivity */ @Override public int getTotalMediaCount() { - return searchImageFragment.getTotalImagesCount(); + return searchMediaFragment.getTotalImagesCount(); } /** @@ -207,7 +226,7 @@ public class SearchActivity extends NavigationBaseActivity //FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time. //FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894 // This is called on screen rotation when user is inside media details. Ideally it should show Media Details but since we are not saving the state now. We are throwing the user to search screen otherwise the app was crashing. - // + // onBackPressed(); } super.onResume(); @@ -250,8 +269,8 @@ public class SearchActivity extends NavigationBaseActivity */ @Override public void requestMoreImages() { - if (searchImageFragment!=null){ - searchImageFragment.addImagesToList(query); + if (searchMediaFragment!=null){ + searchMediaFragment.requestMoreImages(); } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchDataSourceFactory.kt b/app/src/main/java/fr/free/nrw/commons/explore/SearchDataSourceFactory.kt index 7a53572e7..c51d022c9 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchDataSourceFactory.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchDataSourceFactory.kt @@ -24,8 +24,8 @@ abstract class PageableDataSource(private val liveDataConverter: LiveDataConv val loadingStates: Flowable = _loadingStates private val _searchResults = PublishProcessor.create>>() val searchResults: Flowable>> = _searchResults - private val _noItemsLoadedEvent = PublishProcessor.create() - val noItemsLoadedEvent: Flowable = _noItemsLoadedEvent + private val _noItemsLoadedEvent = PublishProcessor.create() + val noItemsLoadedQueries: Flowable = _noItemsLoadedEvent private var currentFactory: SearchDataSourceFactory? = null abstract val loadFunction: LoadFunction @@ -34,7 +34,7 @@ abstract class PageableDataSource(private val liveDataConverter: LiveDataConv this.query = query _searchResults.offer( liveDataConverter.convert(dataSourceFactoryFactory().also { currentFactory = it }) { - _noItemsLoadedEvent.offer(Unit) + _noItemsLoadedEvent.offer(query) } ) } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchFragmentContract.kt b/app/src/main/java/fr/free/nrw/commons/explore/SearchFragmentContract.kt index 19507a303..b81608c27 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchFragmentContract.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchFragmentContract.kt @@ -8,9 +8,10 @@ interface SearchFragmentContract { interface View { fun showSnackbar() fun observeSearchResults(searchResults: LiveData>) - fun setEmptyViewText(query: String) fun showInitialLoadInProgress() fun hideInitialLoadProgress() + fun showEmptyText(query: String) + fun hideEmptyText() } interface Presenter : BasePresenter> { diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchModule.kt b/app/src/main/java/fr/free/nrw/commons/explore/SearchModule.kt index 22ac34939..cac220c18 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchModule.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchModule.kt @@ -5,6 +5,8 @@ import dagger.Module import fr.free.nrw.commons.explore.categories.SearchCategoriesFragmentContract import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentContract import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentPresenter +import fr.free.nrw.commons.explore.media.SearchMediaFragmentContract +import fr.free.nrw.commons.explore.media.SearchMediaFragmentPresenter /** * The Dagger Module for explore:depictions related presenters and (some other objects maybe in future) @@ -18,7 +20,12 @@ abstract class SearchModule { @Binds abstract fun bindsSearchCategoriesFragmentPresenter( - presenter: SearchCategoriesFragmentPresenter? - ): SearchCategoriesFragmentContract.Presenter? + presenter: SearchCategoriesFragmentPresenter + ): SearchCategoriesFragmentContract.Presenter + + @Binds + abstract fun bindsSearchMediaFragmentPresenter( + presenter: SearchMediaFragmentPresenter + ): SearchMediaFragmentContract.Presenter } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.kt index aa5cae887..da5058fe0 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.kt @@ -15,6 +15,8 @@ class SearchCategoryFragment : BaseSearchFragment() { override val emptyTemplateTextId: Int = R.string.categories_not_found + override val errorTextId: Int = R.string.error_loading_categories + override val injectedPresenter: SearchFragmentContract.Presenter get() = presenter diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragment.kt index 0acf619fc..579281da2 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragment.kt @@ -17,7 +17,9 @@ class SearchDepictionsFragment : BaseSearchFragment(), override val emptyTemplateTextId: Int = R.string.depictions_not_found - override val injectedPresenter: SearchFragmentContract.Presenter + override val errorTextId: Int = R.string.error_loading_depictions + + override val injectedPresenter: SearchDepictionsFragmentContract.Presenter get() = presenter override val pagedListAdapter by lazy { diff --git a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java deleted file mode 100644 index d1af331b3..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java +++ /dev/null @@ -1,288 +0,0 @@ -package fr.free.nrw.commons.explore.images; - -import static android.view.View.GONE; -import static android.view.View.VISIBLE; -import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX; - -import android.annotation.SuppressLint; -import android.content.res.Configuration; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ProgressBar; -import android.widget.TextView; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import butterknife.BindView; -import butterknife.ButterKnife; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.explore.SearchActivity; -import fr.free.nrw.commons.explore.recentsearches.RecentSearch; -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.media.MediaClient; -import fr.free.nrw.commons.utils.NetworkUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Named; -import kotlin.Unit; -import timber.log.Timber; - -/** - * Displays the image search screen. - */ - -public class SearchImageFragment extends CommonsDaggerSupportFragment { - - @BindView(R.id.imagesListBox) - RecyclerView imagesRecyclerView; - @BindView(R.id.imageSearchInProgress) - ProgressBar progressBar; - @BindView(R.id.imagesNotFound) - TextView imagesNotFoundView; - String query; - @BindView(R.id.bottomProgressBar) - ProgressBar bottomProgressBar; - - @Inject RecentSearchesDao recentSearchesDao; - @Inject - MediaClient mediaClient; - @Inject - @Named("default_preferences") - JsonKvStore defaultKvStore; - - /** - * A variable to store number of list items for whom API has been called to fetch captions - */ - private int mediaSize = 0; - - private SearchImagesAdapter imagesAdapter; - private List queryList = new ArrayList<>(); - - /** - * This method saves Search Query in the Recent Searches Database. - * @param query - */ - private void saveQuery(String query) { - RecentSearch recentSearch = recentSearchesDao.find(query); - - // Newly searched query... - if (recentSearch == null) { - recentSearch = new RecentSearch(null, query, new Date()); - } - else { - recentSearch.setLastSearched(new Date()); - } - - recentSearchesDao.save(recentSearch); - - } - - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false); - ButterKnife.bind(this, rootView); - if (getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ - imagesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - } - else{ - imagesRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); - } - imagesAdapter =new SearchImagesAdapter(media -> { - ((SearchActivity)getContext()).onSearchImageClicked(imagesAdapter.getItems().indexOf(media)); - saveQuery(query); - return Unit.INSTANCE; - }); - imagesRecyclerView.setAdapter(imagesAdapter); - imagesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrollStateChanged(RecyclerView recyclerView, int newState) { - super.onScrollStateChanged(recyclerView, newState); - // check if end of recycler view is reached, if yes then add more results to existing results - if (!recyclerView.canScrollVertically(1)) { - addImagesToList(query); - } - } - }); - return rootView; - } - - /** - * Checks for internet connection and then initializes the recycler view with 25 images of the searched query - * Clearing imageAdapter every time new keyword is searched so that user can see only new results - */ - @SuppressLint("CheckResult") - public void updateImageList(String query) { - this.query = query; - if (imagesNotFoundView != null) { - imagesNotFoundView.setVisibility(GONE); - } - if (!NetworkUtils.isInternetConnectionEstablished(getContext())) { - handleNoInternet(); - return; - } - progressBar.setVisibility(View.VISIBLE); - bottomProgressBar.setVisibility(GONE); - queryList.clear(); - imagesAdapter.clear(); - compositeDisposable.add(mediaClient.getMediaListFromSearch(query) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe(disposable -> saveQuery(query)) - .subscribe(this::handleSuccess, this::handleError)); - } - - - /** - * Adds more results to existing search results - */ - @SuppressLint("CheckResult") - public void addImagesToList(String query) { - this.query = query; - bottomProgressBar.setVisibility(View.VISIBLE); - progressBar.setVisibility(GONE); - compositeDisposable.add(mediaClient.getMediaListFromSearch(query) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::handlePaginationSuccess, this::handleError)); - } - - /** - * Handles the success scenario - * it initializes the recycler view by adding items to the adapter - * @param mediaList List of media to be added - */ - private void handlePaginationSuccess(List mediaList) { - progressBar.setVisibility(View.GONE); - bottomProgressBar.setVisibility(GONE); - if (mediaList.size() != 0 && !queryList.get(queryList.size() - 1).getFilename().equals(mediaList.get(mediaList.size() - 1).getFilename())) { - queryList.addAll(mediaList); - imagesAdapter.addAll(mediaList); - ((SearchActivity) getContext()).viewPagerNotifyDataSetChanged(); - } - } - - - - /** - * Handles the success scenario - * it initializes the recycler view by adding items to the adapter - * @param mediaList List of media to be shown - */ - private void handleSuccess(List mediaList) { - queryList = mediaList; - if (mediaList == null || mediaList.isEmpty()) { - initErrorView(); - } - else { - bottomProgressBar.setVisibility(View.GONE); - progressBar.setVisibility(GONE); - imagesAdapter.addAll(mediaList); - imagesAdapter.notifyDataSetChanged(); - ((SearchActivity)getContext()).viewPagerNotifyDataSetChanged(); - for (Media m : mediaList) { - final String pageId = m.getPageId(); - if (pageId != null) { - replaceTitlesWithCaptions(PAGE_ID_PREFIX + pageId, mediaSize++); - } - } - } - } - - /** - * In explore we first show title and simultaneously call the API to retrieve captions - * When captions are retrieved they replace title - */ - - public void replaceTitlesWithCaptions(String wikibaseIdentifier, int position) { - compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(subscriber -> { - handleLabelforImage(subscriber, position); - })); - - } - - private void handleLabelforImage(String s, int position) { - if (!s.trim().equals(getString(R.string.detail_caption_empty))) { - imagesAdapter.updateThumbnail(position, s); - } - } - - /** - * Logs and handles API error scenario - * @param throwable - */ - private void handleError(Throwable throwable) { - Timber.e(throwable, "Error occurred while loading queried images"); - try { - ViewUtil.showShortSnackbar(imagesRecyclerView, R.string.error_loading_images); - }catch (Exception e){ - e.printStackTrace(); - } - } - - /** - * Handles the UI updates for a error scenario - */ - private void initErrorView() { - progressBar.setVisibility(GONE); - imagesNotFoundView.setVisibility(VISIBLE); - imagesNotFoundView.setText(getString(R.string.images_not_found,query)); - } - - /** - * Handles the UI updates for no internet scenario - */ - private void handleNoInternet() { - if (null - != getView()) {//We have exposed public methods to update our ui, we will have to add null checks until we make this lifecycle aware - if (null != progressBar) { - progressBar.setVisibility(GONE); - } - ViewUtil.showShortSnackbar(imagesRecyclerView, R.string.no_internet); - } else { - Timber.d("Attempt to update fragment ui after its view was destroyed"); - } - } - - /** - * returns total number of images present in the recyclerview adapter. - */ - public int getTotalImagesCount(){ - if (imagesAdapter == null) { - return 0; - } - else { - return imagesAdapter.getItemCount(); - } - } - - /** - * returns Media Object at position - * @param i position of Media in the recyclerview adapter. - */ - public Media getImageAtPosition(int i) { - if (imagesAdapter.getItemAt(i).getFilename() == null) { - // not yet ready to return data - return null; - } - return imagesAdapter.getItemAt(i); - } - - @Override public void onDestroyView() { - super.onDestroyView(); - compositeDisposable.clear(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesAdapter.kt b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesAdapter.kt deleted file mode 100644 index 65e1510ec..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesAdapter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package fr.free.nrw.commons.explore.images - -import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter -import fr.free.nrw.commons.Media - -class SearchImagesAdapter(onImageClicked: (Media) -> Unit) : ListDelegationAdapter>( - searchImagesAdapter(onImageClicked) -) { - fun getItemAt(position: Int) = items[position] - - init { - items = emptyList() - } - - fun clear() { - items = emptyList() - } - - fun addAll(mediaList: List) { - items = items + mediaList - } - - fun updateThumbnail(position: Int, thumbnailTitle: String) { - items[position].thumbnailTitle = thumbnailTitle - notifyItemChanged(position) - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesAdapterDelegates.kt b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesAdapterDelegates.kt deleted file mode 100644 index 12feb4e50..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesAdapterDelegates.kt +++ /dev/null @@ -1,24 +0,0 @@ -package fr.free.nrw.commons.explore.images - -import android.view.View -import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer -import fr.free.nrw.commons.Media -import fr.free.nrw.commons.R -import kotlinx.android.synthetic.main.layout_category_images.* - - -fun searchImagesAdapter(onImageClicked: (Media) -> Unit) = - adapterDelegateLayoutContainer(R.layout.layout_category_images) { - categoryImageView.setOnClickListener { onImageClicked(item) } - bind { - categoryImageTitle.text = item.thumbnailTitle - categoryImageView.setImageURI(item.thumbUrl) - if (item.creator?.isNotEmpty() == true) { - categoryImageAuthor.visibility = View.VISIBLE - categoryImageAuthor.text = getString(R.string.image_uploaded_by, item.creator) - } else { - categoryImageAuthor.visibility = View.GONE - } - } - - } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt new file mode 100644 index 000000000..41483dd8e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt @@ -0,0 +1,9 @@ +package fr.free.nrw.commons.explore.media + +import fr.free.nrw.commons.Media +import org.wikipedia.dataclient.mwapi.MwQueryPage +import javax.inject.Inject + +class MediaConverter @Inject constructor() { + fun convert(mwQueryPage: MwQueryPage): Media = Media.from(mwQueryPage) +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaDataSource.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaDataSource.kt new file mode 100644 index 000000000..641de6d6a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaDataSource.kt @@ -0,0 +1,34 @@ +package fr.free.nrw.commons.explore.media + +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX +import fr.free.nrw.commons.explore.LiveDataConverter +import fr.free.nrw.commons.explore.PageableDataSource +import fr.free.nrw.commons.explore.depictions.LoadFunction +import fr.free.nrw.commons.media.MediaClient +import fr.free.nrw.commons.media.MediaClient.NO_CAPTION +import javax.inject.Inject + +class PageableMediaDataSource @Inject constructor( + liveDataConverter: LiveDataConverter, + private val mediaConverter: MediaConverter, + private val mediaClient: MediaClient +) : PageableDataSource(liveDataConverter) { + override val loadFunction: LoadFunction = { loadSize: Int, startPosition: Int -> + mediaClient.getMediaListFromSearch(query, loadSize, startPosition) + .map { it.query()?.pages()?.map(mediaConverter::convert) ?: emptyList() } + .map { it.zip(getCaptions(it)) } + .map { it.map { (media, caption) -> media.also { it.caption = caption } } } + .blockingGet() + } + + private fun getCaptions(it: List) = + mediaClient.getEntities(it.joinToString("|") { PAGE_ID_PREFIX + it.pageId }) + .map { + it.entities().values.map { entity -> + entity.labels().values.firstOrNull()?.value() ?: NO_CAPTION + } + } + .blockingGet() + +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaAdapter.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaAdapter.kt new file mode 100644 index 000000000..71a25d6c9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaAdapter.kt @@ -0,0 +1,49 @@ +package fr.free.nrw.commons.explore.media + +import android.view.View +import android.view.ViewGroup +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.DiffUtil +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.explore.BaseViewHolder +import fr.free.nrw.commons.explore.inflate +import kotlinx.android.synthetic.main.layout_category_images.* + +class SearchImagesAdapter(private val onImageClicked: (Int) -> Unit) : + PagedListAdapter(object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Media, newItem: Media) = + oldItem.pageId == newItem.pageId + + override fun areContentsTheSame(oldItem: Media, newItem: Media) = + oldItem.pageId == newItem.pageId + }) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + SearchImagesViewHolder( + parent.inflate(R.layout.layout_category_images), + onImageClicked + ) + + override fun onBindViewHolder(holder: SearchImagesViewHolder, position: Int) { + holder.bind(getItem(position)!! to position) + } +} + +class SearchImagesViewHolder(containerView: View, val onImageClicked: (Int) -> Unit) : + BaseViewHolder>(containerView) { + override fun bind(item: Pair) { + val media = item.first + categoryImageView.setOnClickListener { onImageClicked(item.second) } + categoryImageTitle.text = media.thumbnailTitle + categoryImageView.setImageURI(media.thumbUrl) + if (media.creator?.isNotEmpty() == true) { + categoryImageAuthor.visibility = View.VISIBLE + categoryImageAuthor.text = + containerView.context.getString(R.string.image_uploaded_by, media.creator) + } else { + categoryImageAuthor.visibility = View.GONE + } + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaFragment.kt new file mode 100644 index 000000000..677e306e1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaFragment.kt @@ -0,0 +1,57 @@ +package fr.free.nrw.commons.explore.media + +import android.os.Bundle +import android.view.View +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.explore.BaseSearchFragment +import fr.free.nrw.commons.explore.SearchActivity +import javax.inject.Inject + +/** + * Displays the image search screen. + */ +class SearchMediaFragment : BaseSearchFragment(), SearchMediaFragmentContract.View { + @Inject + lateinit var presenter: SearchMediaFragmentContract.Presenter + + override val emptyTemplateTextId: Int = R.string.depictions_not_found + + override val errorTextId: Int = R.string.error_loading_images + + override val injectedPresenter: SearchMediaFragmentContract.Presenter + get() = presenter + + override val pagedListAdapter by lazy { + SearchImagesAdapter { + (context as SearchActivity?)!!.onSearchImageClicked(it) + } + } + + private val simpleDataObserver = SimpleDataObserver { notifyViewPager() } + + fun requestMoreImages() { + // This functionality is replaced by a dataSetObserver and by using loadAround + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + pagedListAdapter.registerAdapterDataObserver(simpleDataObserver) + } + + override fun onDestroyView() { + super.onDestroyView() + pagedListAdapter.unregisterAdapterDataObserver(simpleDataObserver) + } + + private fun notifyViewPager() { + (activity as SearchActivity).viewPagerNotifyDataSetChanged() + } + + fun getImageAtPosition(position: Int): Media? = + pagedListAdapter.currentList?.get(position)?.takeIf { it.filename != null } + .also { pagedListAdapter.currentList?.loadAround(position) } + + fun getTotalImagesCount(): Int = pagedListAdapter.itemCount +} + diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaFragmentContract.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaFragmentContract.kt new file mode 100644 index 000000000..0995c2e42 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaFragmentContract.kt @@ -0,0 +1,10 @@ +package fr.free.nrw.commons.explore.media + +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.explore.SearchFragmentContract + + +interface SearchMediaFragmentContract { + interface View : SearchFragmentContract.View + interface Presenter : SearchFragmentContract.Presenter +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaFragmentPresenter.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaFragmentPresenter.kt new file mode 100644 index 000000000..dcad9ad8f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/SearchMediaFragmentPresenter.kt @@ -0,0 +1,14 @@ +package fr.free.nrw.commons.explore.media + +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.di.CommonsApplicationModule +import fr.free.nrw.commons.explore.BaseSearchPresenter +import io.reactivex.Scheduler +import javax.inject.Inject +import javax.inject.Named + +class SearchMediaFragmentPresenter @Inject constructor( + @Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler, + dataSourceFactory: PageableMediaDataSource +) : BaseSearchPresenter(mainThreadScheduler, dataSourceFactory), + SearchMediaFragmentContract.Presenter diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/SimpleDataObserver.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/SimpleDataObserver.kt new file mode 100644 index 000000000..4bd9e0f2b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/SimpleDataObserver.kt @@ -0,0 +1,40 @@ +package fr.free.nrw.commons.explore.media + +import androidx.recyclerview.widget.RecyclerView + +class SimpleDataObserver(private val onAnyChange: () -> Unit) : RecyclerView.AdapterDataObserver() { + override fun onChanged() { + super.onChanged() + onAnyChange.invoke() + } + + override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { + super.onItemRangeRemoved(positionStart, itemCount) + onAnyChange.invoke() + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + super.onItemRangeMoved(fromPosition, toPosition, itemCount) + onAnyChange.invoke() + } + + override fun onStateRestorationPolicyChanged() { + super.onStateRestorationPolicyChanged() + onAnyChange.invoke() + } + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + super.onItemRangeInserted(positionStart, itemCount) + onAnyChange.invoke() + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { + super.onItemRangeChanged(positionStart, itemCount) + onAnyChange.invoke() + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { + super.onItemRangeChanged(positionStart, itemCount, payload) + onAnyChange.invoke() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java index d23f3842e..846864979 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java @@ -124,14 +124,12 @@ public class MediaClient { * It uses the generator query API to get the images searched using a query, 10 at a time. * * @param keyword the search keyword + * @param limit + * @param offset * @return */ - public Single> getMediaListFromSearch(String keyword) { - return responseToMediaList( - continuationStore.containsKey("search_" + keyword) && (continuationStore.get("search_" + keyword) != null) ? - mediaInterface.getMediaListFromSearch(keyword, 10, continuationStore.get("search_" + keyword)) : //if true - mediaInterface.getMediaListFromSearch(keyword, 10, Collections.emptyMap()), //if false - "search_" + keyword); + public Single getMediaListFromSearch(String keyword, int limit, int offset) { + return mediaInterface.getMediaListFromSearch(keyword, limit, offset); } @@ -270,9 +268,10 @@ public class MediaClient { } } throw new RuntimeException("failed getEntities"); - }) - .singleOrError(); + }); } + public Single getEntities(String entityId) { + return mediaDetailInterface.getEntity(entityId); } - +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailInterface.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailInterface.java index f1e407f76..ef9559c29 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailInterface.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailInterface.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.media; import io.reactivex.Observable; +import io.reactivex.Single; import org.wikipedia.wikidata.Entities; import retrofit2.http.GET; import retrofit2.http.Query; @@ -24,7 +25,7 @@ public interface MediaDetailInterface { * */ @GET("/w/api.php?format=json&action=wbgetentities&props=labels&languagefallback=1") - Observable getEntity(@Query("ids") String entityId); + Single getEntity(@Query("ids") String entityId); /** * Fetches caption using wikibaseIdentifier diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java index e102a8fcb..aaec15136 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java @@ -2,6 +2,7 @@ package fr.free.nrw.commons.media; import fr.free.nrw.commons.depictions.models.DepictionResponse; import io.reactivex.Observable; +import io.reactivex.Single; import java.util.Map; import org.wikipedia.dataclient.mwapi.MwQueryResponse; import retrofit2.http.GET; @@ -65,13 +66,13 @@ public interface MediaInterface { * * @param keyword the searched keyword * @param itemLimit how many images are returned - * @param continuation the continuation string from the previous query + * @param offset the offset in the result set * @return */ @GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters "&generator=search&gsrwhat=text&gsrnamespace=6" + //Search parameters MEDIA_PARAMS) - Observable getMediaListFromSearch(@Query("gsrsearch") String keyword, @Query("gsrlimit") int itemLimit, @QueryMap Map continuation); + Single getMediaListFromSearch(@Query("gsrsearch") String keyword, @Query("gsrlimit") int itemLimit, @Query("gsroffset") int offset); /** * Fetches Media object from the imageInfo API diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/BaseSearchPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/BaseSearchPresenterTest.kt index 740984882..4be25522b 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/BaseSearchPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/BaseSearchPresenterTest.kt @@ -34,7 +34,7 @@ class BaseSearchPresenterTest { private var searchResults: PublishProcessor>> = PublishProcessor.create() - private var noItemLoadedEvent: PublishProcessor = PublishProcessor.create() + private var noItemLoadedQueries: PublishProcessor = PublishProcessor.create() @Before @Throws(Exception::class) @@ -42,8 +42,8 @@ class BaseSearchPresenterTest { MockitoAnnotations.initMocks(this) whenever(pageableDataSource.searchResults).thenReturn(searchResults) whenever(pageableDataSource.loadingStates).thenReturn(loadingStates) - whenever(pageableDataSource.noItemsLoadedEvent) - .thenReturn(noItemLoadedEvent) + whenever(pageableDataSource.noItemsLoadedQueries) + .thenReturn(noItemLoadedQueries) testScheduler = TestScheduler() baseSearchPresenter = object : BaseSearchPresenter(testScheduler, pageableDataSource) {} @@ -60,6 +60,7 @@ class BaseSearchPresenterTest { @Test fun `Loading offers a loading list item`() { onLoadingState(LoadingState.Loading) + verify(view).hideEmptyText() baseSearchPresenter.listFooterData.test().assertValue(listOf(FooterItem.LoadingItem)) } @@ -74,6 +75,7 @@ class BaseSearchPresenterTest { @Test fun `InitialLoad shows initial loader`() { onLoadingState(LoadingState.InitialLoad) + verify(view).hideEmptyText() verify(view).showInitialLoadInProgress() } @@ -81,16 +83,6 @@ class BaseSearchPresenterTest { fun `Error offers a refresh list item, hides initial loader and shows error with a set text`() { baseSearchPresenter.onQueryUpdated("test") onLoadingState(LoadingState.Error) - verify(view).setEmptyViewText("test") - verify(view).showSnackbar() - verify(view).hideInitialLoadProgress() - baseSearchPresenter.listFooterData.test().assertValue(listOf(FooterItem.RefreshItem)) - } - - @Test - fun `Error offers a refresh list item, hides initial loader and shows error with a unset text`() { - onLoadingState(LoadingState.Error) - verify(view, never()).setEmptyViewText(any()) verify(view).showSnackbar() verify(view).hideInitialLoadProgress() baseSearchPresenter.listFooterData.test().assertValue(listOf(FooterItem.RefreshItem)) @@ -98,9 +90,8 @@ class BaseSearchPresenterTest { @Test fun `no Items event sets empty view text`() { - baseSearchPresenter.onQueryUpdated("test") - noItemLoadedEvent.offer(Unit) - verify(view).setEmptyViewText("test") + noItemLoadedQueries.offer("test") + verify(view).showEmptyText("test") } @Test diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/PageableDataSourceTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/PageableDataSourceTest.kt index 2ced05a27..49222f2c3 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/PageableDataSourceTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/PageableDataSourceTest.kt @@ -39,9 +39,9 @@ class PageableDataSourceTest { fun `onQueryUpdated invokes livedatconverter with no items emitter`() { val (zeroItemsFuncCaptor, _) = expectNewLiveData() pageableDataSource.onQueryUpdated("test") - pageableDataSource.noItemsLoadedEvent.test() + pageableDataSource.noItemsLoadedQueries.test() .also { zeroItemsFuncCaptor.firstValue.invoke() } - .assertValue(Unit) + .assertValue("test") } /* diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/media/PageableMediaDataSourceTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/media/PageableMediaDataSourceTest.kt new file mode 100644 index 000000000..41395fd8a --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/media/PageableMediaDataSourceTest.kt @@ -0,0 +1,71 @@ +package fr.free.nrw.commons.explore.media + +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX +import fr.free.nrw.commons.media.MediaClient +import io.reactivex.Single +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.wikipedia.dataclient.mwapi.MwQueryPage +import org.wikipedia.dataclient.mwapi.MwQueryResponse +import org.wikipedia.dataclient.mwapi.MwQueryResult +import org.wikipedia.wikidata.Entities + +class PageableMediaDataSourceTest { + @Mock + lateinit var mediaConverter: MediaConverter + @Mock + lateinit var mediaClient: MediaClient + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun `loadFunction invokes mediaClient and has Label`() { + val (media, entity: Entities.Entity) = expectMediaAndEntity() + val label: Entities.Label = mock() + whenever(entity.labels()).thenReturn(mapOf(" " to label)) + whenever(label.value()).thenReturn("label") + val pageableMediaDataSource = PageableMediaDataSource(mock(), mediaConverter, mediaClient) + pageableMediaDataSource.onQueryUpdated("test") + assertThat(pageableMediaDataSource.loadFunction(0,1), `is`(listOf(media))) + verify(media).caption = "label" + } + + @Test + fun `loadFunction invokes mediaClient and does not have Label`() { + val (media, entity: Entities.Entity) = expectMediaAndEntity() + whenever(entity.labels()).thenReturn(mapOf()) + val pageableMediaDataSource = PageableMediaDataSource(mock(), mediaConverter, mediaClient) + pageableMediaDataSource.onQueryUpdated("test") + assertThat(pageableMediaDataSource.loadFunction(0,1), `is`(listOf(media))) + verify(media).caption = MediaClient.NO_CAPTION + } + + private fun expectMediaAndEntity(): Pair { + val queryResponse: MwQueryResponse = mock() + whenever(mediaClient.getMediaListFromSearch("test", 0, 1)) + .thenReturn(Single.just(queryResponse)) + val queryResult: MwQueryResult = mock() + whenever(queryResponse.query()).thenReturn(queryResult) + val queryPage: MwQueryPage = mock() + whenever(queryResult.pages()).thenReturn(listOf(queryPage)) + val media = mock() + whenever(mediaConverter.convert(queryPage)).thenReturn(media) + whenever(media.pageId).thenReturn("1") + val entities: Entities = mock() + whenever(mediaClient.getEntities("${PAGE_ID_PREFIX}1")).thenReturn(Single.just(entities)) + val entity: Entities.Entity = mock() + whenever(entities.entities()).thenReturn(mapOf("" to entity)) + return Pair(media, entity) + } +}