mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
parent
0ebd59a223
commit
c216fdf0d4
33 changed files with 1035 additions and 616 deletions
|
|
@ -45,6 +45,10 @@ dependencies {
|
||||||
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
||||||
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:$ADAPTER_DELEGATES_VERSION"
|
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:$ADAPTER_DELEGATES_VERSION"
|
||||||
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
|
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
|
||||||
|
implementation "androidx.paging:paging-runtime-ktx:$PAGING_VERSION"
|
||||||
|
testImplementation "androidx.paging:paging-common-ktx:$PAGING_VERSION"
|
||||||
|
implementation "androidx.paging:paging-rxjava2-ktx:$PAGING_VERSION"
|
||||||
|
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation 'ch.acra:acra-dialog:5.3.0'
|
implementation 'ch.acra:acra-dialog:5.3.0'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base presenter, enforcing contracts to atach and detach view
|
* Base presenter, enforcing contracts to atach and detach view
|
||||||
*/
|
*/
|
||||||
|
|
@ -7,7 +9,7 @@ public interface BasePresenter<T> {
|
||||||
/**
|
/**
|
||||||
* Until a view is attached, it is open to listen events from the presenter
|
* Until a view is attached, it is open to listen events from the presenter
|
||||||
*/
|
*/
|
||||||
void onAttachView(T view);
|
void onAttachView(@NonNull T view);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detaching a view makes sure that the view no more receives events from the presenter
|
* Detaching a view makes sure that the view no more receives events from the presenter
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package fr.free.nrw.commons.depictions.subClass
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.explore.depictions.depictionDelegate
|
||||||
|
import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter
|
||||||
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
|
||||||
|
class SubDepictionAdapter(clickListener: (DepictedItem) -> Unit) :
|
||||||
|
BaseDelegateAdapter<DepictedItem>(
|
||||||
|
depictionDelegate(clickListener),
|
||||||
|
areItemsTheSame = { oldItem, newItem -> oldItem.id == newItem.id }
|
||||||
|
)
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
package fr.free.nrw.commons.depictions.subClass;
|
package fr.free.nrw.commons.depictions.subClass;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.BasePresenter;
|
import fr.free.nrw.commons.BasePresenter;
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The contract with which SubDepictionListFragment and its presenter would talk to each other
|
* The contract with which SubDepictionListFragment and its presenter would talk to each other
|
||||||
|
|
@ -28,5 +27,6 @@ public interface SubDepictionListContract {
|
||||||
void initSubDepictionList(String qid, Boolean isParentClass) throws IOException;
|
void initSubDepictionList(String qid, Boolean isParentClass) throws IOException;
|
||||||
|
|
||||||
String getQuery();
|
String getQuery();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import butterknife.ButterKnife;
|
||||||
import dagger.android.support.DaggerFragment;
|
import dagger.android.support.DaggerFragment;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
|
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
|
||||||
import fr.free.nrw.commons.explore.depictions.DepictionAdapter;
|
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
|
|
@ -47,7 +46,7 @@ public class SubDepictionListFragment extends DaggerFragment implements SubDepic
|
||||||
* Keeps a record of whether current instance of the fragment if of SubClass or ParentClass
|
* Keeps a record of whether current instance of the fragment if of SubClass or ParentClass
|
||||||
*/
|
*/
|
||||||
private boolean isParentClass = false;
|
private boolean isParentClass = false;
|
||||||
private DepictionAdapter depictionsAdapter;
|
private SubDepictionAdapter depictionsAdapter;
|
||||||
RecyclerView.LayoutManager layoutManager;
|
RecyclerView.LayoutManager layoutManager;
|
||||||
/**
|
/**
|
||||||
* Stores entityId for the depiction
|
* Stores entityId for the depiction
|
||||||
|
|
@ -100,7 +99,7 @@ public class SubDepictionListFragment extends DaggerFragment implements SubDepic
|
||||||
}
|
}
|
||||||
initViews();
|
initViews();
|
||||||
depictionsRecyclerView.setLayoutManager(layoutManager);
|
depictionsRecyclerView.setLayoutManager(layoutManager);
|
||||||
depictionsAdapter = new DepictionAdapter(depictedItem -> {
|
depictionsAdapter = new SubDepictionAdapter(depictedItem -> {
|
||||||
// Open SubDepiction Details page
|
// Open SubDepiction Details page
|
||||||
getActivity().finish();
|
getActivity().finish();
|
||||||
WikidataItemDetailsActivity.startYourself(getContext(), depictedItem);
|
WikidataItemDetailsActivity.startYourself(getContext(), depictedItem);
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ public class SearchActivity extends NavigationBaseActivity
|
||||||
searchHistoryContainer.setVisibility(View.GONE);
|
searchHistoryContainer.setVisibility(View.GONE);
|
||||||
|
|
||||||
if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) {
|
if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) {
|
||||||
searchDepictionsFragment.updateDepictionList(query.toString());
|
searchDepictionsFragment.onQueryUpdated(query.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FragmentUtils.isFragmentUIActive(searchImageFragment)) {
|
if (FragmentUtils.isFragmentUIActive(searchImageFragment)) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ package fr.free.nrw.commons.explore;
|
||||||
|
|
||||||
import dagger.Binds;
|
import dagger.Binds;
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import fr.free.nrw.commons.explore.depictions.DepictsClient;
|
||||||
|
import fr.free.nrw.commons.explore.depictions.SearchDepictionsDataSourceFactory;
|
||||||
|
import fr.free.nrw.commons.explore.depictions.SearchDepictionsDataSourceFactoryFactory;
|
||||||
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentContract;
|
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentContract;
|
||||||
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentPresenter;
|
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentPresenter;
|
||||||
|
|
||||||
|
|
@ -16,4 +20,10 @@ public abstract class SearchModule {
|
||||||
SearchDepictionsFragmentPresenter
|
SearchDepictionsFragmentPresenter
|
||||||
presenter
|
presenter
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
static public SearchDepictionsDataSourceFactoryFactory providesSearchDepictionsFactoryFactory(
|
||||||
|
DepictsClient depictsClient){
|
||||||
|
return (query, loadingStates) -> new SearchDepictionsDataSourceFactory(depictsClient, query, loadingStates);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,47 @@
|
||||||
package fr.free.nrw.commons.explore.depictions
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.paging.PagedListAdapter
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
|
import kotlinx.android.synthetic.main.item_depictions.*
|
||||||
|
|
||||||
|
|
||||||
class DepictionAdapter(clickListener: (DepictedItem) -> Unit) : BaseDelegateAdapter<DepictedItem>(
|
class DepictionAdapter(val onDepictionClicked: (DepictedItem) -> Unit) :
|
||||||
depictionDelegate(clickListener),
|
PagedListAdapter<DepictedItem, DepictedItemViewHolder>(
|
||||||
areItemsTheSame = { oldItem, newItem -> oldItem.id == newItem.id }
|
object : DiffUtil.ItemCallback<DepictedItem>() {
|
||||||
)
|
override fun areItemsTheSame(oldItem: DepictedItem, newItem: DepictedItem) =
|
||||||
|
oldItem.id == newItem.id
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: DepictedItem, newItem: DepictedItem) =
|
||||||
|
oldItem == newItem
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
) {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DepictedItemViewHolder {
|
||||||
|
return DepictedItemViewHolder(parent.inflate(R.layout.item_depictions))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: DepictedItemViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position)!!, onDepictionClicked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DepictedItemViewHolder(override val containerView: View) :
|
||||||
|
RecyclerView.ViewHolder(containerView), LayoutContainer {
|
||||||
|
fun bind(item: DepictedItem, onDepictionClicked: (DepictedItem) -> Unit) {
|
||||||
|
containerView.setOnClickListener { onDepictionClicked(item) }
|
||||||
|
depicts_label.text = item.name
|
||||||
|
description.text = item.description
|
||||||
|
if (item.imageUrl?.isNotBlank() == true) {
|
||||||
|
depicts_image.setImageURI(item.imageUrl)
|
||||||
|
} else {
|
||||||
|
depicts_image.setActualImageResource(R.drawable.ic_wikidata_logo_24dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
|
import kotlinx.android.synthetic.main.list_item_load_more.*
|
||||||
|
|
||||||
|
class FooterAdapter(private val onRefreshClicked: () -> Unit) :
|
||||||
|
ListAdapter<FooterItem, FooterViewHolder>(object :
|
||||||
|
DiffUtil.ItemCallback<FooterItem>() {
|
||||||
|
override fun areItemsTheSame(oldItem: FooterItem, newItem: FooterItem) = oldItem == newItem
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: FooterItem, newItem: FooterItem) =
|
||||||
|
oldItem == newItem
|
||||||
|
}) {
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return getItem(position).ordinal
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
|
when (FooterItem.values()[viewType]) {
|
||||||
|
FooterItem.LoadingItem -> LoadingViewHolder(parent.inflate(R.layout.list_item_progress))
|
||||||
|
FooterItem.RefreshItem -> RefreshViewHolder(
|
||||||
|
parent.inflate(R.layout.list_item_load_more),
|
||||||
|
onRefreshClicked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: FooterViewHolder, position: Int) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class FooterViewHolder(override val containerView: View) :
|
||||||
|
RecyclerView.ViewHolder(containerView),
|
||||||
|
LayoutContainer
|
||||||
|
|
||||||
|
class LoadingViewHolder(containerView: View) : FooterViewHolder(containerView)
|
||||||
|
class RefreshViewHolder(containerView: View, onRefreshClicked: () -> Unit) :
|
||||||
|
FooterViewHolder(containerView) {
|
||||||
|
init {
|
||||||
|
listItemLoadMoreButton.setOnClickListener { onRefreshClicked() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class FooterItem { LoadingItem, RefreshItem }
|
||||||
|
|
||||||
|
fun ViewGroup.inflate(@LayoutRes layoutId: Int, attachToRoot: Boolean = false): View =
|
||||||
|
LayoutInflater.from(context).inflate(layoutId, this, attachToRoot)
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
|
import androidx.paging.PositionalDataSource
|
||||||
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
import io.reactivex.Completable
|
||||||
|
import io.reactivex.processors.PublishProcessor
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
|
data class SearchDepictionsDataSource constructor(
|
||||||
|
private val depictsClient: DepictsClient,
|
||||||
|
private val loadingStates: PublishProcessor<LoadingState>,
|
||||||
|
private val query: String
|
||||||
|
) : PositionalDataSource<DepictedItem>() {
|
||||||
|
|
||||||
|
private var lastExecutedRequest: (() -> Boolean)? = null
|
||||||
|
|
||||||
|
override fun loadInitial(
|
||||||
|
params: LoadInitialParams,
|
||||||
|
callback: LoadInitialCallback<DepictedItem>
|
||||||
|
) {
|
||||||
|
storeAndExecute {
|
||||||
|
loadingStates.offer(LoadingState.InitialLoad)
|
||||||
|
performWithTryCatch {
|
||||||
|
callback.onResult(
|
||||||
|
getItems(query, params.requestedLoadSize, params.requestedStartPosition),
|
||||||
|
params.requestedStartPosition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<DepictedItem>) {
|
||||||
|
storeAndExecute {
|
||||||
|
loadingStates.offer(LoadingState.Loading)
|
||||||
|
performWithTryCatch {
|
||||||
|
callback.onResult(getItems(query, params.loadSize, params.startPosition))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retryFailedRequest() {
|
||||||
|
Completable.fromAction { lastExecutedRequest?.invoke() }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getItems(query: String, limit: Int, offset: Int) =
|
||||||
|
depictsClient.searchForDepictions(query, limit, offset).blockingGet()
|
||||||
|
|
||||||
|
private fun storeAndExecute(function: () -> Boolean) {
|
||||||
|
function.also { lastExecutedRequest = it }.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performWithTryCatch(function: () -> Unit) = try {
|
||||||
|
function.invoke()
|
||||||
|
loadingStates.offer(LoadingState.Complete)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
loadingStates.offer(LoadingState.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,200 +0,0 @@
|
||||||
package fr.free.nrw.commons.explore.depictions;
|
|
||||||
|
|
||||||
import static android.view.View.GONE;
|
|
||||||
import static android.view.View.VISIBLE;
|
|
||||||
import static fr.free.nrw.commons.explore.depictions.DepictionAdapterDelegatesKt.depictionDelegate;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
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.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.DiffUtil.ItemCallback;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
|
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
|
||||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import kotlin.Unit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display depictions in search fragment
|
|
||||||
*/
|
|
||||||
public class SearchDepictionsFragment extends CommonsDaggerSupportFragment implements SearchDepictionsFragmentContract.View {
|
|
||||||
|
|
||||||
@BindView(R.id.imagesListBox)
|
|
||||||
RecyclerView depictionsRecyclerView;
|
|
||||||
@BindView(R.id.imageSearchInProgress)
|
|
||||||
ProgressBar progressBar;
|
|
||||||
@BindView(R.id.imagesNotFound)
|
|
||||||
TextView depictionNotFound;
|
|
||||||
@BindView(R.id.bottomProgressBar)
|
|
||||||
ProgressBar bottomProgressBar;
|
|
||||||
private RecyclerView.LayoutManager layoutManager;
|
|
||||||
private boolean isLoading = true;
|
|
||||||
private final int PAGE_SIZE = 25;
|
|
||||||
@Inject
|
|
||||||
SearchDepictionsFragmentPresenter presenter;
|
|
||||||
private DepictionAdapter depictionsAdapter;
|
|
||||||
private boolean isLastPage;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
|
|
||||||
final View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
|
|
||||||
ButterKnife.bind(this, rootView);
|
|
||||||
if (getActivity().getResources().getConfiguration().orientation
|
|
||||||
== Configuration.ORIENTATION_PORTRAIT) {
|
|
||||||
layoutManager = new LinearLayoutManager(getContext());
|
|
||||||
} else {
|
|
||||||
layoutManager = new GridLayoutManager(getContext(), 2);
|
|
||||||
}
|
|
||||||
depictionsRecyclerView.setLayoutManager(layoutManager);
|
|
||||||
depictionsAdapter = new DepictionAdapter(
|
|
||||||
depictedItem -> {
|
|
||||||
WikidataItemDetailsActivity.startYourself(getContext(), depictedItem);
|
|
||||||
presenter.saveQuery();
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
});
|
|
||||||
depictionsRecyclerView.setAdapter(depictionsAdapter);
|
|
||||||
depictionsRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
|
||||||
@Override
|
|
||||||
public void onScrollStateChanged(final RecyclerView recyclerView, final int newState) {
|
|
||||||
super.onScrollStateChanged(recyclerView, newState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
|
|
||||||
super.onScrolled(recyclerView, dx, dy);
|
|
||||||
|
|
||||||
final int visibleItemCount = layoutManager.getChildCount();
|
|
||||||
final int totalItemCount = layoutManager.getItemCount();
|
|
||||||
int firstVisibleItemPosition=0;
|
|
||||||
if(layoutManager instanceof GridLayoutManager){
|
|
||||||
firstVisibleItemPosition=((GridLayoutManager) layoutManager).findFirstVisibleItemPosition();
|
|
||||||
} else {
|
|
||||||
firstVisibleItemPosition=((LinearLayoutManager)layoutManager).findFirstVisibleItemPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the user isn't currently loading items and the last page hasn’t been reached,
|
|
||||||
* then it checks against the current position in view to decide whether or not to load more items.
|
|
||||||
*/
|
|
||||||
if (!isLoading && !isLastPage) {
|
|
||||||
if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount
|
|
||||||
&& firstVisibleItemPosition >= 0
|
|
||||||
&& totalItemCount >= PAGE_SIZE) {
|
|
||||||
loadMoreItems(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return rootView;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch PAGE_SIZE number of items
|
|
||||||
*/
|
|
||||||
private void loadMoreItems(final boolean reInitialise) {
|
|
||||||
presenter.updateDepictionList(presenter.getQuery(),PAGE_SIZE, reInitialise);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(final Context context) {
|
|
||||||
super.onAttach(context);
|
|
||||||
presenter.onAttachView(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when user selects "Items" from Search Activity
|
|
||||||
* to load the list of depictions from API
|
|
||||||
*
|
|
||||||
* @param query string searched in the Explore Activity
|
|
||||||
*/
|
|
||||||
public void updateDepictionList(final String query) {
|
|
||||||
presenter.initializeQuery(query);
|
|
||||||
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
|
|
||||||
handleNoInternet();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadMoreItems(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the UI updates for a error scenario
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void initErrorView() {
|
|
||||||
isLoading = false;
|
|
||||||
progressBar.setVisibility(GONE);
|
|
||||||
bottomProgressBar.setVisibility(GONE);
|
|
||||||
depictionNotFound.setVisibility(VISIBLE);
|
|
||||||
final String no_depiction = getString(R.string.depictions_not_found);
|
|
||||||
depictionNotFound.setText(String.format(Locale.getDefault(), no_depiction, presenter.getQuery()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the UI updates for no internet scenario
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void handleNoInternet() {
|
|
||||||
progressBar.setVisibility(GONE);
|
|
||||||
ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.no_internet);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If a non empty list is successfully returned from the api then modify the view
|
|
||||||
* like hiding empty labels, hiding progressbar and notifying the apdapter that list of items has been fetched from the API
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onSuccess(final List<DepictedItem> mediaList) {
|
|
||||||
isLoading = false;
|
|
||||||
progressBar.setVisibility(GONE);
|
|
||||||
depictionNotFound.setVisibility(GONE);
|
|
||||||
bottomProgressBar.setVisibility(GONE);
|
|
||||||
depictionsAdapter.addAll(mediaList);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadingDepictions(final boolean isLoading) {
|
|
||||||
depictionNotFound.setVisibility(GONE);
|
|
||||||
bottomProgressBar.setVisibility(VISIBLE);
|
|
||||||
progressBar.setVisibility(GONE);
|
|
||||||
this.isLoading = isLoading;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearAdapter() {
|
|
||||||
depictionsAdapter.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showSnackbar() {
|
|
||||||
ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.error_loading_depictions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inform the view that there are no more items to be loaded for this search query
|
|
||||||
* or reset the isLastPage for the current query
|
|
||||||
* @param isLastPage
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void setIsLastPage(final boolean isLastPage) {
|
|
||||||
this.isLastPage=isLastPage;
|
|
||||||
progressBar.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
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.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.MergeAdapter
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||||
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
import fr.free.nrw.commons.utils.ViewUtil
|
||||||
|
import kotlinx.android.synthetic.main.fragment_search_depictions.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display depictions in search fragment
|
||||||
|
*/
|
||||||
|
class SearchDepictionsFragment : CommonsDaggerSupportFragment(),
|
||||||
|
SearchDepictionsFragmentContract.View {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var presenter: SearchDepictionsFragmentContract.UserActionListener
|
||||||
|
|
||||||
|
private val depictionsAdapter by lazy {
|
||||||
|
DepictionAdapter { WikidataItemDetailsActivity.startYourself(context, it) }
|
||||||
|
}
|
||||||
|
private val loadingAdapter by lazy { FooterAdapter { presenter.retryFailedRequest() } }
|
||||||
|
private val mergeAdapter by lazy { MergeAdapter(depictionsAdapter, loadingAdapter) }
|
||||||
|
|
||||||
|
var searchResults: LiveData<PagedList<DepictedItem>>? = null
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
) = inflater.inflate(R.layout.fragment_search_depictions, container, false)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
depictionsSearchResultsList.apply {
|
||||||
|
layoutManager = GridLayoutManager(context, if (isPortrait) 1 else 2)
|
||||||
|
adapter = mergeAdapter
|
||||||
|
}
|
||||||
|
presenter.listFooterData.observe(viewLifecycleOwner, Observer(loadingAdapter::submitList))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
presenter.onAttachView(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
super.onDetach()
|
||||||
|
presenter.onDetachView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun observeSearchResults(searchResults: LiveData<PagedList<DepictedItem>>) {
|
||||||
|
this.searchResults?.removeObservers(viewLifecycleOwner)
|
||||||
|
this.searchResults = searchResults
|
||||||
|
searchResults.observe(viewLifecycleOwner, Observer {
|
||||||
|
depictionsAdapter.submitList(it)
|
||||||
|
depictionNotFound.visibility = if (it.loadedCount == 0) VISIBLE else GONE
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setEmptyViewText(query: String) {
|
||||||
|
depictionNotFound.text = getString(R.string.depictions_not_found, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hideInitialLoadProgress() {
|
||||||
|
depictionSearchInitialLoadProgress.visibility = GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showInitialLoadInProgress() {
|
||||||
|
depictionSearchInitialLoadProgress.visibility = VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showSnackbar() {
|
||||||
|
ViewUtil.showShortSnackbar(depictionsSearchResultsList, R.string.error_loading_depictions)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onQueryUpdated(query: String) {
|
||||||
|
presenter.onQueryUpdated(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Fragment.isPortrait get() = orientation == Configuration.ORIENTATION_PORTRAIT
|
||||||
|
|
||||||
|
private val Fragment.orientation get() = activity!!.resources.configuration.orientation
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
package fr.free.nrw.commons.explore.depictions;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.BasePresenter;
|
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The contract with with SearchDepictionsFragment and its presenter would talk to each other
|
|
||||||
*/
|
|
||||||
public interface SearchDepictionsFragmentContract {
|
|
||||||
|
|
||||||
interface View {
|
|
||||||
/**
|
|
||||||
* Handles the UI updates for a error scenario
|
|
||||||
*/
|
|
||||||
void initErrorView();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the UI updates for no internet scenario
|
|
||||||
*/
|
|
||||||
void handleNoInternet();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If a non empty list is successfully returned from the api then modify the view
|
|
||||||
* like hiding empty labels, hiding progressbar and notifying the apdapter that list of items has been fetched from the API
|
|
||||||
*/
|
|
||||||
void onSuccess(List<DepictedItem> mediaList);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* load depictions
|
|
||||||
*/
|
|
||||||
void loadingDepictions(boolean isLoading);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* clear adapter
|
|
||||||
*/
|
|
||||||
void clearAdapter();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* show snackbar
|
|
||||||
*/
|
|
||||||
void showSnackbar();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inform the view that there are no more items to be loaded for this search query
|
|
||||||
* or reset the isLastPage for the current query
|
|
||||||
* @param isLastPage
|
|
||||||
*/
|
|
||||||
void setIsLastPage(boolean isLastPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserActionListener extends BasePresenter<View> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when user selects "Items" from Search Activity
|
|
||||||
* to load the list of depictions from API
|
|
||||||
*
|
|
||||||
* @param query string searched in the Explore Activity
|
|
||||||
* @param reInitialise
|
|
||||||
*/
|
|
||||||
void updateDepictionList(String query, int pageSize, boolean reInitialise);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method saves Search Query in the Recent Searches Database.
|
|
||||||
*/
|
|
||||||
void saveQuery();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whenever a new query is initiated from the search activity clear the previous adapter
|
|
||||||
* and add new value of the query
|
|
||||||
*/
|
|
||||||
void initializeQuery(String query);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return query
|
|
||||||
*/
|
|
||||||
String getQuery();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import fr.free.nrw.commons.BasePresenter
|
||||||
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contract with with SearchDepictionsFragment and its presenter would talk to each other
|
||||||
|
*/
|
||||||
|
interface SearchDepictionsFragmentContract {
|
||||||
|
interface View {
|
||||||
|
fun showSnackbar()
|
||||||
|
fun observeSearchResults(searchResults: LiveData<PagedList<DepictedItem>>)
|
||||||
|
fun setEmptyViewText(query: String)
|
||||||
|
fun showInitialLoadInProgress()
|
||||||
|
fun hideInitialLoadProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserActionListener : BasePresenter<View?> {
|
||||||
|
val listFooterData: LiveData<List<FooterItem>>
|
||||||
|
fun onQueryUpdated(query: String)
|
||||||
|
fun retryFailedRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
package fr.free.nrw.commons.explore.depictions;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
|
|
||||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
|
||||||
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.upload.structure.depictions.DepictedItem;
|
|
||||||
import io.reactivex.Scheduler;
|
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
|
||||||
import java.lang.reflect.Proxy;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The presenter class for SearchDepictionsFragment
|
|
||||||
*/
|
|
||||||
public class SearchDepictionsFragmentPresenter extends CommonsDaggerSupportFragment implements SearchDepictionsFragmentContract.UserActionListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This creates a dynamic proxy instance of the class,
|
|
||||||
* proxy is to control access to the target object
|
|
||||||
* here our target object is the view.
|
|
||||||
* Thus we when onDettach method of fragment is called we replace the binding of view to our object with the proxy instance
|
|
||||||
*/
|
|
||||||
private static final SearchDepictionsFragmentContract.View DUMMY = (SearchDepictionsFragmentContract.View) Proxy
|
|
||||||
.newProxyInstance(
|
|
||||||
SearchDepictionsFragmentContract.View.class.getClassLoader(),
|
|
||||||
new Class[]{SearchDepictionsFragmentContract.View.class},
|
|
||||||
(proxy, method, methodArgs) -> null);
|
|
||||||
protected CompositeDisposable compositeDisposable = new CompositeDisposable();
|
|
||||||
private final Scheduler ioScheduler;
|
|
||||||
private final Scheduler mainThreadScheduler;
|
|
||||||
|
|
||||||
boolean isLoadingDepictions;
|
|
||||||
String query;
|
|
||||||
RecentSearchesDao recentSearchesDao;
|
|
||||||
DepictsClient depictsClient;
|
|
||||||
JsonKvStore basicKvStore;
|
|
||||||
private SearchDepictionsFragmentContract.View view = DUMMY;
|
|
||||||
private List<DepictedItem> queryList = new ArrayList<>();
|
|
||||||
int offset=0;
|
|
||||||
int size = 0;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public SearchDepictionsFragmentPresenter(@Named("default_preferences") JsonKvStore basicKvStore,
|
|
||||||
RecentSearchesDao recentSearchesDao,
|
|
||||||
DepictsClient depictsClient,
|
|
||||||
@Named(IO_THREAD) Scheduler ioScheduler,
|
|
||||||
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
|
|
||||||
this.basicKvStore = basicKvStore;
|
|
||||||
this.recentSearchesDao = recentSearchesDao;
|
|
||||||
this.depictsClient = depictsClient;
|
|
||||||
this.ioScheduler = ioScheduler;
|
|
||||||
this.mainThreadScheduler = mainThreadScheduler;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttachView(SearchDepictionsFragmentContract.View view) {
|
|
||||||
this.view = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetachView() {
|
|
||||||
this.view = DUMMY;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when user selects "Items" from Search Activity
|
|
||||||
* to load the list of depictions from API
|
|
||||||
*
|
|
||||||
* @param query string searched in the Explore Activity
|
|
||||||
* @param reInitialise
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void updateDepictionList(String query, int pageSize, boolean reInitialise) {
|
|
||||||
this.query = query;
|
|
||||||
view.loadingDepictions(true);
|
|
||||||
if (reInitialise) {
|
|
||||||
size = 0;
|
|
||||||
}
|
|
||||||
saveQuery();
|
|
||||||
compositeDisposable.add(depictsClient.searchForDepictions(query, 25, offset)
|
|
||||||
.subscribeOn(ioScheduler)
|
|
||||||
.observeOn(mainThreadScheduler)
|
|
||||||
.doOnSubscribe(disposable -> saveQuery())
|
|
||||||
.subscribe(this::handleSuccess, this::handleError));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs and handles API error scenario
|
|
||||||
*/
|
|
||||||
private void handleError(Throwable throwable) {
|
|
||||||
Timber.e(throwable, "Error occurred while loading queried depictions");
|
|
||||||
view.initErrorView();
|
|
||||||
view.showSnackbar();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method saves Search Query in the Recent Searches Database.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void saveQuery() {
|
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whenever a new query is initiated from the search activity clear the previous adapter
|
|
||||||
* and add new value of the query
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void initializeQuery(String query) {
|
|
||||||
this.query = query;
|
|
||||||
this.queryList.clear();
|
|
||||||
offset = 0;//Reset the offset on query change
|
|
||||||
compositeDisposable.clear();
|
|
||||||
view.setIsLastPage(false);
|
|
||||||
view.clearAdapter();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getQuery() {
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the success scenario
|
|
||||||
* it initializes the recycler view by adding items to the adapter
|
|
||||||
*/
|
|
||||||
public void handleSuccess(List<DepictedItem> mediaList) {
|
|
||||||
if (mediaList == null || mediaList.isEmpty()) {
|
|
||||||
if(queryList.isEmpty()){
|
|
||||||
view.initErrorView();
|
|
||||||
}else{
|
|
||||||
view.setIsLastPage(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.queryList.addAll(mediaList);
|
|
||||||
view.onSuccess(mediaList);
|
|
||||||
offset=queryList.size();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import fr.free.nrw.commons.di.CommonsApplicationModule
|
||||||
|
import fr.free.nrw.commons.upload.depicts.proxy
|
||||||
|
import io.reactivex.Scheduler
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The presenter class for SearchDepictionsFragment
|
||||||
|
*/
|
||||||
|
class SearchDepictionsFragmentPresenter @Inject constructor(
|
||||||
|
@param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler,
|
||||||
|
private val searchableDataSourceFactory: SearchableDepictionsDataSourceFactory
|
||||||
|
) : SearchDepictionsFragmentContract.UserActionListener {
|
||||||
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
private var view = DUMMY
|
||||||
|
private var currentQuery: String? = null
|
||||||
|
override val listFooterData = MutableLiveData<List<FooterItem>>().apply { value = emptyList() }
|
||||||
|
|
||||||
|
override fun onAttachView(view: SearchDepictionsFragmentContract.View) {
|
||||||
|
this.view = view
|
||||||
|
compositeDisposable.addAll(
|
||||||
|
searchableDataSourceFactory.searchResults.subscribe(view::observeSearchResults),
|
||||||
|
searchableDataSourceFactory.loadingStates
|
||||||
|
.observeOn(mainThreadScheduler)
|
||||||
|
.subscribe(::onLoadingState, Timber::e),
|
||||||
|
searchableDataSourceFactory.noItemsLoadedEvent.subscribe {
|
||||||
|
setEmptyViewText()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onLoadingState(it: LoadingState) = when (it) {
|
||||||
|
LoadingState.Loading -> listFooterData.postValue(listOf(FooterItem.LoadingItem))
|
||||||
|
LoadingState.Complete -> {
|
||||||
|
listFooterData.postValue(emptyList())
|
||||||
|
view.hideInitialLoadProgress()
|
||||||
|
}
|
||||||
|
LoadingState.InitialLoad -> view.showInitialLoadInProgress()
|
||||||
|
LoadingState.Error -> {
|
||||||
|
setEmptyViewText()
|
||||||
|
view.showSnackbar()
|
||||||
|
view.hideInitialLoadProgress()
|
||||||
|
listFooterData.postValue(listOf(FooterItem.RefreshItem))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setEmptyViewText() {
|
||||||
|
currentQuery?.let(view::setEmptyViewText)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun retryFailedRequest() {
|
||||||
|
searchableDataSourceFactory.retryFailedRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachView() {
|
||||||
|
view = DUMMY
|
||||||
|
compositeDisposable.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryUpdated(query: String) {
|
||||||
|
currentQuery = query
|
||||||
|
searchableDataSourceFactory.onQueryUpdated(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DUMMY: SearchDepictionsFragmentContract.View = proxy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.paging.Config
|
||||||
|
import androidx.paging.DataSource
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import androidx.paging.toLiveData
|
||||||
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
import io.reactivex.Flowable
|
||||||
|
import io.reactivex.processors.PublishProcessor
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val PAGE_SIZE = 50
|
||||||
|
private const val INITIAL_LOAD_SIZE = 50
|
||||||
|
|
||||||
|
class SearchableDepictionsDataSourceFactory @Inject constructor(
|
||||||
|
val searchDepictionsDataSourceFactoryFactory: SearchDepictionsDataSourceFactoryFactory,
|
||||||
|
val liveDataConverter: LiveDataConverter
|
||||||
|
) {
|
||||||
|
private val _loadingStates = PublishProcessor.create<LoadingState>()
|
||||||
|
val loadingStates: Flowable<LoadingState> = _loadingStates
|
||||||
|
private val _searchResults = PublishProcessor.create<LiveData<PagedList<DepictedItem>>>()
|
||||||
|
val searchResults: Flowable<LiveData<PagedList<DepictedItem>>> = _searchResults
|
||||||
|
private val _noItemsLoadedEvent = PublishProcessor.create<Unit>()
|
||||||
|
val noItemsLoadedEvent: Flowable<Unit> = _noItemsLoadedEvent
|
||||||
|
|
||||||
|
private var currentFactory: SearchDepictionsDataSourceFactory? = null
|
||||||
|
|
||||||
|
fun onQueryUpdated(query: String) {
|
||||||
|
_searchResults.offer(
|
||||||
|
liveDataConverter.convert(
|
||||||
|
searchDepictionsDataSourceFactoryFactory.create(query, _loadingStates)
|
||||||
|
.also { currentFactory = it }
|
||||||
|
) { _noItemsLoadedEvent.offer(Unit) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retryFailedRequest() {
|
||||||
|
currentFactory?.retryFailedRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LiveDataConverter @Inject constructor() {
|
||||||
|
fun convert(
|
||||||
|
dataSourceFactory: SearchDepictionsDataSourceFactory,
|
||||||
|
zeroItemsLoadedFunction: () -> Unit
|
||||||
|
): LiveData<PagedList<DepictedItem>> {
|
||||||
|
return dataSourceFactory.toLiveData(
|
||||||
|
Config(
|
||||||
|
pageSize = PAGE_SIZE,
|
||||||
|
initialLoadSizeHint = INITIAL_LOAD_SIZE,
|
||||||
|
enablePlaceholders = false
|
||||||
|
),
|
||||||
|
boundaryCallback = object : PagedList.BoundaryCallback<DepictedItem>() {
|
||||||
|
override fun onZeroItemsLoaded() {
|
||||||
|
zeroItemsLoadedFunction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchDepictionsDataSourceFactoryFactory {
|
||||||
|
fun create(query: String, loadingStates: PublishProcessor<LoadingState>)
|
||||||
|
: SearchDepictionsDataSourceFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchDepictionsDataSourceFactory constructor(
|
||||||
|
private val depictsClient: DepictsClient,
|
||||||
|
private val query: String,
|
||||||
|
private val loadingStates: PublishProcessor<LoadingState>
|
||||||
|
) : DataSource.Factory<Int, DepictedItem>() {
|
||||||
|
private var currentDataSource: SearchDepictionsDataSource? = null
|
||||||
|
override fun create() = SearchDepictionsDataSource(depictsClient, loadingStates, query)
|
||||||
|
.also { currentDataSource = it }
|
||||||
|
|
||||||
|
fun retryFailedRequest() {
|
||||||
|
currentDataSource?.retryFailedRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class LoadingState {
|
||||||
|
object InitialLoad : LoadingState()
|
||||||
|
object Loading : LoadingState()
|
||||||
|
object Complete : LoadingState()
|
||||||
|
object Error : LoadingState()
|
||||||
|
}
|
||||||
|
|
@ -109,5 +109,11 @@ class DepictsPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This creates a dynamic proxy instance of the class,
|
||||||
|
* proxy is to control access to the target object
|
||||||
|
* here our target object is the view.
|
||||||
|
* Thus we when onDettach method of fragment is called we replace the binding of view to our object with the proxy instance
|
||||||
|
*/
|
||||||
inline fun <reified T> proxy() = Proxy
|
inline fun <reified T> proxy() = Proxy
|
||||||
.newProxyInstance(T::class.java.classLoader, arrayOf(T::class.java)) { _, _, _ -> null } as T
|
.newProxyInstance(T::class.java.classLoader, arrayOf(T::class.java)) { _, _, _ -> null } as T
|
||||||
|
|
|
||||||
29
app/src/main/res/layout/fragment_search_depictions.xml
Normal file
29
app/src/main/res/layout/fragment_search_depictions.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingTop="@dimen/tiny_gap"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/depictionNotFound"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:gravity="center"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/depictionsSearchResultsList"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbarSize="@dimen/standard_gap"
|
||||||
|
android:scrollbars="vertical" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/depictionSearchInitialLoadProgress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
@ -1,37 +1,43 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:orientation="vertical" android:layout_width="match_parent"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="@dimen/tiny_gap">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/depicts_label"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="@dimen/tiny_gap">
|
android:paddingTop="@dimen/tiny_gap"
|
||||||
|
android:text="Label"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/depicts_image"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Really really really really long long long label" />
|
||||||
|
|
||||||
<com.facebook.drawee.view.SimpleDraweeView
|
<TextView
|
||||||
android:id="@+id/depicts_image"
|
android:id="@+id/description"
|
||||||
android:layout_width="70dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="70dp"
|
android:layout_height="wrap_content"
|
||||||
android:paddingRight="@dimen/tiny_gap"
|
android:paddingTop="@dimen/tiny_gap"
|
||||||
app:placeholderImage="@drawable/ic_wikidata_logo_24dp"
|
android:text="Description"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
tools:text="Really really really really long long long description description description"
|
||||||
app:layout_constraintTop_toTopOf="parent"/>
|
app:layout_constraintStart_toEndOf="@+id/depicts_image"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/depicts_label" />
|
||||||
|
|
||||||
<TextView
|
<com.facebook.drawee.view.SimpleDraweeView
|
||||||
android:id="@+id/depicts_label"
|
android:id="@+id/depicts_image"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="70dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="70dp"
|
||||||
android:text="Label"
|
android:paddingRight="@dimen/tiny_gap"
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:textStyle="bold"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:paddingTop="@dimen/tiny_gap"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintLeft_toRightOf="@+id/depicts_image"
|
app:placeholderImage="@drawable/ic_wikidata_logo_24dp" />
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/description"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Description"
|
|
||||||
android:paddingTop="@dimen/tiny_gap"
|
|
||||||
app:layout_constraintLeft_toRightOf="@+id/depicts_image"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/depicts_label" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
||||||
17
app/src/main/res/layout/list_item_load_more.xml
Normal file
17
app/src/main/res/layout/list_item_load_more.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="@dimen/small_gap">
|
||||||
|
<Button
|
||||||
|
android:id="@+id/listItemLoadMoreButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/load_more"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
13
app/src/main/res/layout/list_item_progress.xml
Normal file
13
app/src/main/res/layout/list_item_progress.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<ProgressBar
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
@ -628,6 +628,6 @@ Upload your first media by tapping on the add button.</string>
|
||||||
<string name="ask_to_turn_location_on">Turn on location?</string>
|
<string name="ask_to_turn_location_on">Turn on location?</string>
|
||||||
<string name="nearby_needs_location">Nearby needs location enabled to work properly</string>
|
<string name="nearby_needs_location">Nearby needs location enabled to work properly</string>
|
||||||
<string name="use_location_from_similar_image">Did you shoot these two pictures at the same place? Do you want to use the latitude/longitude of the picture on the right?</string>
|
<string name="use_location_from_similar_image">Did you shoot these two pictures at the same place? Do you want to use the latitude/longitude of the picture on the right?</string>
|
||||||
|
<string name="load_more">Load More</string>
|
||||||
<string name="nearby_no_results">No places found, try changing your search criteria.</string>
|
<string name="nearby_no_results">No places found, try changing your search criteria.</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -15,19 +15,19 @@ import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class CampaignsPresenterTest {
|
class CampaignsPresenterTest {
|
||||||
@Mock
|
@Mock
|
||||||
var okHttpJsonApiClient: OkHttpJsonApiClient? = null
|
lateinit var okHttpJsonApiClient: OkHttpJsonApiClient
|
||||||
|
|
||||||
lateinit var campaignsPresenter: CampaignsPresenter
|
lateinit var campaignsPresenter: CampaignsPresenter
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal var view: ICampaignsView? = null
|
internal lateinit var view: ICampaignsView
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal var campaignResponseDTO: CampaignResponseDTO? = null
|
internal lateinit var campaignResponseDTO: CampaignResponseDTO
|
||||||
lateinit var campaignsSingle: Single<CampaignResponseDTO>
|
lateinit var campaignsSingle: Single<CampaignResponseDTO>
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
var campaign: Campaign? = null
|
lateinit var campaign: Campaign
|
||||||
|
|
||||||
lateinit var testScheduler: TestScheduler
|
lateinit var testScheduler: TestScheduler
|
||||||
|
|
||||||
|
|
@ -41,37 +41,37 @@ class CampaignsPresenterTest {
|
||||||
testScheduler=TestScheduler()
|
testScheduler=TestScheduler()
|
||||||
campaignsSingle= Single.just(campaignResponseDTO)
|
campaignsSingle= Single.just(campaignResponseDTO)
|
||||||
campaignsPresenter= CampaignsPresenter(okHttpJsonApiClient,testScheduler,testScheduler)
|
campaignsPresenter= CampaignsPresenter(okHttpJsonApiClient,testScheduler,testScheduler)
|
||||||
campaignsPresenter?.onAttachView(view)
|
campaignsPresenter.onAttachView(view)
|
||||||
Mockito.`when`(okHttpJsonApiClient?.campaigns).thenReturn(campaignsSingle)
|
Mockito.`when`(okHttpJsonApiClient.campaigns).thenReturn(campaignsSingle)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getCampaignsTestNoCampaigns() {
|
fun getCampaignsTestNoCampaigns() {
|
||||||
campaignsPresenter.getCampaigns()
|
campaignsPresenter.getCampaigns()
|
||||||
verify(okHttpJsonApiClient)?.campaigns
|
verify(okHttpJsonApiClient).campaigns
|
||||||
testScheduler.triggerActions()
|
testScheduler.triggerActions()
|
||||||
verify(view)?.showCampaigns(null)
|
verify(view).showCampaigns(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getCampaignsTestNonEmptyCampaigns() {
|
fun getCampaignsTestNonEmptyCampaigns() {
|
||||||
campaignsPresenter.getCampaigns()
|
campaignsPresenter.getCampaigns()
|
||||||
var campaigns= ArrayList<Campaign>()
|
var campaigns= ArrayList<Campaign>()
|
||||||
campaigns.add(campaign!!)
|
campaigns.add(campaign)
|
||||||
val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
|
val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
|
||||||
simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
Mockito.`when`(campaignResponseDTO?.campaigns).thenReturn(campaigns)
|
Mockito.`when`(campaignResponseDTO.campaigns).thenReturn(campaigns)
|
||||||
var calendar = Calendar.getInstance()
|
var calendar = Calendar.getInstance()
|
||||||
calendar.add(Calendar.DATE,-1)
|
calendar.add(Calendar.DATE,-1)
|
||||||
val startDateString = simpleDateFormat.format(calendar.time).toString()
|
val startDateString = simpleDateFormat.format(calendar.time).toString()
|
||||||
calendar= Calendar.getInstance()
|
calendar= Calendar.getInstance()
|
||||||
calendar.add(Calendar.DATE,3)
|
calendar.add(Calendar.DATE,3)
|
||||||
val endDateString= simpleDateFormat.format(calendar.time).toString()
|
val endDateString= simpleDateFormat.format(calendar.time).toString()
|
||||||
Mockito.`when`(campaign?.endDate).thenReturn(endDateString)
|
Mockito.`when`(campaign.endDate).thenReturn(endDateString)
|
||||||
Mockito.`when`(campaign?.startDate).thenReturn(startDateString)
|
Mockito.`when`(campaign.startDate).thenReturn(startDateString)
|
||||||
Mockito.`when`(campaignResponseDTO?.campaigns).thenReturn(campaigns)
|
Mockito.`when`(campaignResponseDTO.campaigns).thenReturn(campaigns)
|
||||||
verify(okHttpJsonApiClient)?.campaigns
|
verify(okHttpJsonApiClient).campaigns
|
||||||
testScheduler.triggerActions()
|
testScheduler.triggerActions()
|
||||||
verify(view)?.showCampaigns(campaign)
|
verify(view).showCampaigns(campaign)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package fr.free.nrw.commons.depictions
|
package fr.free.nrw.commons.depictions
|
||||||
|
|
||||||
import org.mockito.Mockito.verify
|
|
||||||
import fr.free.nrw.commons.Media
|
import fr.free.nrw.commons.Media
|
||||||
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment
|
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment
|
||||||
import fr.free.nrw.commons.depictions.Media.DepictedImagesPresenter
|
import fr.free.nrw.commons.depictions.Media.DepictedImagesPresenter
|
||||||
|
|
@ -15,24 +14,26 @@ import org.junit.Test
|
||||||
import org.mockito.ArgumentMatchers
|
import org.mockito.ArgumentMatchers
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
class DepictedImagesPresenterTest {
|
class DepictedImagesPresenterTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal var view: DepictedImagesFragment? = null
|
internal lateinit var view: DepictedImagesFragment
|
||||||
|
|
||||||
var depictedImagesPresenter: DepictedImagesPresenter? = null
|
lateinit var depictedImagesPresenter: DepictedImagesPresenter
|
||||||
|
|
||||||
var jsonKvStore: JsonKvStore? = null
|
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
var depictsClient: DepictsClient? = null
|
lateinit var jsonKvStore: JsonKvStore
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
var mediaClient: MediaClient? = null
|
lateinit var depictsClient: DepictsClient
|
||||||
|
|
||||||
var testScheduler: TestScheduler? = null
|
@Mock
|
||||||
|
lateinit var mediaClient: MediaClient
|
||||||
|
|
||||||
|
lateinit var testScheduler: TestScheduler
|
||||||
|
|
||||||
val mediaList: ArrayList<Media> = ArrayList()
|
val mediaList: ArrayList<Media> = ArrayList()
|
||||||
|
|
||||||
|
|
@ -50,24 +51,26 @@ class DepictedImagesPresenterTest {
|
||||||
mediaList.add(mediaItem)
|
mediaList.add(mediaItem)
|
||||||
testObservable = Observable.just(mediaList)
|
testObservable = Observable.just(mediaList)
|
||||||
depictedImagesPresenter = DepictedImagesPresenter(jsonKvStore, depictsClient, mediaClient, testScheduler, testScheduler)
|
depictedImagesPresenter = DepictedImagesPresenter(jsonKvStore, depictsClient, mediaClient, testScheduler, testScheduler)
|
||||||
depictedImagesPresenter?.onAttachView(view)
|
depictedImagesPresenter.onAttachView(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun initList() {
|
fun initList() {
|
||||||
Mockito.`when`(depictsClient?.fetchImagesForDepictedItem(ArgumentMatchers.anyString(),
|
Mockito.`when`(
|
||||||
ArgumentMatchers.anyInt())).thenReturn(testObservable)
|
depictsClient.fetchImagesForDepictedItem(ArgumentMatchers.anyString(),
|
||||||
depictedImagesPresenter?.initList("rabbit")
|
ArgumentMatchers.anyInt())
|
||||||
depictedImagesPresenter?.handleSuccess(mediaList)
|
).thenReturn(testObservable)
|
||||||
|
depictedImagesPresenter.initList("rabbit")
|
||||||
|
depictedImagesPresenter.handleSuccess(mediaList)
|
||||||
verify(view)?.handleSuccess(mediaList)
|
verify(view)?.handleSuccess(mediaList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun replaceTitlesWithCaptions() {
|
fun replaceTitlesWithCaptions() {
|
||||||
var stringObservable: Single<String>? = Single.just(String())
|
var stringObservable: Single<String>? = Single.just(String())
|
||||||
Mockito.`when`(mediaClient?.getCaptionByWikibaseIdentifier(ArgumentMatchers.anyString()))?.thenReturn(stringObservable)
|
Mockito.`when`(mediaClient.getCaptionByWikibaseIdentifier(ArgumentMatchers.anyString()))?.thenReturn(stringObservable)
|
||||||
depictedImagesPresenter?.replaceTitlesWithCaptions("File:rabbit.jpg", 0)
|
depictedImagesPresenter.replaceTitlesWithCaptions("File:rabbit.jpg", 0)
|
||||||
testScheduler?.triggerActions()
|
testScheduler.triggerActions()
|
||||||
verify(view)?.handleLabelforImage("", 0)
|
verify(view)?.handleLabelforImage("", 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.Single
|
|
||||||
import io.reactivex.schedulers.TestScheduler
|
import io.reactivex.schedulers.TestScheduler
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -20,19 +19,20 @@ import org.mockito.MockitoAnnotations
|
||||||
class SubDepictionListPresenterTest {
|
class SubDepictionListPresenterTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal var view: SubDepictionListContract.View? = null
|
internal lateinit var view: SubDepictionListContract.View
|
||||||
|
|
||||||
var subDepictionListPresenter: SubDepictionListPresenter? = null
|
lateinit var subDepictionListPresenter: SubDepictionListPresenter
|
||||||
|
|
||||||
var testScheduler: TestScheduler? = null
|
lateinit var testScheduler: TestScheduler
|
||||||
|
|
||||||
internal var recentSearchesDao: RecentSearchesDao? = null
|
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal var depictsClient: DepictsClient? = null
|
internal lateinit var recentSearchesDao: RecentSearchesDao
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal var okHttpJsonApiClient: OkHttpJsonApiClient? = null
|
internal lateinit var depictsClient: DepictsClient
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
internal lateinit var okHttpJsonApiClient: OkHttpJsonApiClient
|
||||||
|
|
||||||
var testObservable: Observable<List<DepictedItem>>? = null
|
var testObservable: Observable<List<DepictedItem>>? = null
|
||||||
|
|
||||||
|
|
@ -49,22 +49,22 @@ class SubDepictionListPresenterTest {
|
||||||
depictedItems.add(depictedItem)
|
depictedItems.add(depictedItem)
|
||||||
testObservable = Observable.just(depictedItems)
|
testObservable = Observable.just(depictedItems)
|
||||||
subDepictionListPresenter = SubDepictionListPresenter(recentSearchesDao, depictsClient, okHttpJsonApiClient, testScheduler, testScheduler)
|
subDepictionListPresenter = SubDepictionListPresenter(recentSearchesDao, depictsClient, okHttpJsonApiClient, testScheduler, testScheduler)
|
||||||
subDepictionListPresenter?.onAttachView(view)
|
subDepictionListPresenter.onAttachView(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun initSubDepictionListForParentClass() {
|
fun initSubDepictionListForParentClass() {
|
||||||
Mockito.`when`(okHttpJsonApiClient?.getParentQIDs(ArgumentMatchers.anyString())).thenReturn(testObservable)
|
Mockito.`when`(okHttpJsonApiClient.getParentQIDs(ArgumentMatchers.anyString())).thenReturn(testObservable)
|
||||||
subDepictionListPresenter?.initSubDepictionList("Q9394", true)
|
subDepictionListPresenter.initSubDepictionList("Q9394", true)
|
||||||
testScheduler?.triggerActions()
|
testScheduler.triggerActions()
|
||||||
verify(view)?.onSuccess(depictedItems)
|
verify(view)?.onSuccess(depictedItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun initSubDepictionListForChildClass() {
|
fun initSubDepictionListForChildClass() {
|
||||||
Mockito.`when`(okHttpJsonApiClient?.getChildQIDs(ArgumentMatchers.anyString())).thenReturn(testObservable)
|
Mockito.`when`(okHttpJsonApiClient.getChildQIDs(ArgumentMatchers.anyString())).thenReturn(testObservable)
|
||||||
subDepictionListPresenter?.initSubDepictionList("Q9394", false)
|
subDepictionListPresenter.initSubDepictionList("Q9394", false)
|
||||||
testScheduler?.triggerActions()
|
testScheduler.triggerActions()
|
||||||
verify(view)?.onSuccess(depictedItems)
|
verify(view)?.onSuccess(depictedItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
|
import com.nhaarman.mockitokotlin2.spy
|
||||||
|
import com.nhaarman.mockitokotlin2.verify
|
||||||
|
import io.reactivex.processors.PublishProcessor
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.hamcrest.Matchers.`is`
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
class SearchDepictionsDataSourceFactoryTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var depictsClient: DepictsClient
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var loadingStates: PublishProcessor<LoadingState>
|
||||||
|
private lateinit var factory: SearchDepictionsDataSourceFactory
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
factory = SearchDepictionsDataSourceFactory(depictsClient, "test", loadingStates)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `create returns a dataSource`() {
|
||||||
|
assertThat(
|
||||||
|
factory.create(),
|
||||||
|
`is`(SearchDepictionsDataSource(depictsClient, loadingStates, "test"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Ignore("Rewrite with Mockk constructor mocks")
|
||||||
|
fun `retryFailedRequest invokes method if not null`() {
|
||||||
|
val spyFactory = spy(factory)
|
||||||
|
val dataSource = mock<SearchDepictionsDataSource>()
|
||||||
|
Mockito.doReturn(dataSource).`when`(spyFactory).create()
|
||||||
|
factory.retryFailedRequest()
|
||||||
|
verify(dataSource).retryFailedRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `retryFailedRequest does not invoke method if null`() {
|
||||||
|
factory.retryFailedRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
|
import androidx.paging.PositionalDataSource
|
||||||
|
import com.nhaarman.mockitokotlin2.*
|
||||||
|
import fr.free.nrw.commons.explore.depictions.LoadingState.*
|
||||||
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
import io.reactivex.Single
|
||||||
|
import io.reactivex.plugins.RxJavaPlugins
|
||||||
|
import io.reactivex.processors.PublishProcessor
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
class SearchDepictionsDataSourceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var depictsClient: DepictsClient
|
||||||
|
|
||||||
|
private lateinit var loadingStates: PublishProcessor<LoadingState>
|
||||||
|
private lateinit var searchDepictionsDataSource: SearchDepictionsDataSource
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
loadingStates = PublishProcessor.create()
|
||||||
|
searchDepictionsDataSource =
|
||||||
|
SearchDepictionsDataSource(depictsClient, loadingStates, "test")
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
RxJavaPlugins.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loadInitial returns results and emits InitialLoad & Complete`() {
|
||||||
|
val params = PositionalDataSource.LoadInitialParams(0, 1, 2, false)
|
||||||
|
val callback = mock<PositionalDataSource.LoadInitialCallback<DepictedItem>>()
|
||||||
|
whenever(depictsClient.searchForDepictions("test", 1, 0))
|
||||||
|
.thenReturn(Single.just(emptyList()))
|
||||||
|
val testSubscriber = loadingStates.test()
|
||||||
|
searchDepictionsDataSource.loadInitial(params, callback)
|
||||||
|
verify(callback).onResult(emptyList(), 0)
|
||||||
|
testSubscriber.assertValues(InitialLoad, Complete)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loadInitial onError does not return results and emits InitialLoad & Error`() {
|
||||||
|
val params = PositionalDataSource.LoadInitialParams(0, 1, 2, false)
|
||||||
|
val callback = mock<PositionalDataSource.LoadInitialCallback<DepictedItem>>()
|
||||||
|
whenever(depictsClient.searchForDepictions("test", 1, 0))
|
||||||
|
.thenThrow(RuntimeException())
|
||||||
|
val testSubscriber = loadingStates.test()
|
||||||
|
searchDepictionsDataSource.loadInitial(params, callback)
|
||||||
|
verify(callback, never()).onResult(any(), any())
|
||||||
|
testSubscriber.assertValues(InitialLoad, Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loadRange returns results and emits Loading & Complete`() {
|
||||||
|
val callback: PositionalDataSource.LoadRangeCallback<DepictedItem> = mock()
|
||||||
|
val params = PositionalDataSource.LoadRangeParams(0, 1)
|
||||||
|
whenever(depictsClient.searchForDepictions("test", params.loadSize, params.startPosition))
|
||||||
|
.thenReturn(Single.just(emptyList()))
|
||||||
|
val testSubscriber = loadingStates.test()
|
||||||
|
searchDepictionsDataSource.loadRange(params, callback)
|
||||||
|
verify(callback).onResult(emptyList())
|
||||||
|
testSubscriber.assertValues(Loading, Complete)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loadRange onError does not return results and emits Loading & Error`() {
|
||||||
|
val callback: PositionalDataSource.LoadRangeCallback<DepictedItem> = mock()
|
||||||
|
val params = PositionalDataSource.LoadRangeParams(0, 1)
|
||||||
|
whenever(depictsClient.searchForDepictions("test", params.loadSize, params.startPosition))
|
||||||
|
.thenThrow(RuntimeException())
|
||||||
|
val testSubscriber = loadingStates.test()
|
||||||
|
searchDepictionsDataSource.loadRange(params, callback)
|
||||||
|
verify(callback, never()).onResult(any())
|
||||||
|
testSubscriber.assertValues(Loading, Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `retryFailedRequest does nothing when null`() {
|
||||||
|
searchDepictionsDataSource.retryFailedRequest()
|
||||||
|
verifyNoMoreInteractions(depictsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `retryFailedRequest retries last request`() {
|
||||||
|
val callback: PositionalDataSource.LoadRangeCallback<DepictedItem> = mock()
|
||||||
|
val params = PositionalDataSource.LoadRangeParams(0, 1)
|
||||||
|
whenever(depictsClient.searchForDepictions("test", params.loadSize, params.startPosition))
|
||||||
|
.thenThrow(RuntimeException()).thenReturn(Single.just(emptyList()))
|
||||||
|
val testSubscriber = loadingStates.test()
|
||||||
|
searchDepictionsDataSource.loadRange(params, callback)
|
||||||
|
verify(callback, never()).onResult(any())
|
||||||
|
searchDepictionsDataSource.retryFailedRequest()
|
||||||
|
verify(callback).onResult(emptyList())
|
||||||
|
testSubscriber.assertValues(Loading, Error, Loading, Complete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import com.jraska.livedata.test
|
||||||
|
import com.nhaarman.mockitokotlin2.*
|
||||||
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
import io.reactivex.processors.PublishProcessor
|
||||||
|
import io.reactivex.schedulers.TestScheduler
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
class SearchDepictionsFragmentPresenterTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
var instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
internal lateinit var view: SearchDepictionsFragmentContract.View
|
||||||
|
|
||||||
|
private lateinit var searchDepictionsFragmentPresenter: SearchDepictionsFragmentPresenter
|
||||||
|
|
||||||
|
private lateinit var testScheduler: TestScheduler
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var searchableDepictionsDataSourceFactory: SearchableDepictionsDataSourceFactory
|
||||||
|
|
||||||
|
private var loadingStates: PublishProcessor<LoadingState> = PublishProcessor.create()
|
||||||
|
|
||||||
|
private var searchResults: PublishProcessor<LiveData<PagedList<DepictedItem>>> =
|
||||||
|
PublishProcessor.create()
|
||||||
|
|
||||||
|
private var noItemLoadedEvent: PublishProcessor<Unit> = PublishProcessor.create()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
whenever(searchableDepictionsDataSourceFactory.searchResults).thenReturn(searchResults)
|
||||||
|
whenever(searchableDepictionsDataSourceFactory.loadingStates).thenReturn(loadingStates)
|
||||||
|
whenever(searchableDepictionsDataSourceFactory.noItemsLoadedEvent)
|
||||||
|
.thenReturn(noItemLoadedEvent)
|
||||||
|
testScheduler = TestScheduler()
|
||||||
|
searchDepictionsFragmentPresenter = SearchDepictionsFragmentPresenter(
|
||||||
|
testScheduler,
|
||||||
|
searchableDepictionsDataSourceFactory
|
||||||
|
)
|
||||||
|
searchDepictionsFragmentPresenter.onAttachView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `searchResults emission updates the view`() {
|
||||||
|
val pagedListLiveData = mock<LiveData<PagedList<DepictedItem>>>()
|
||||||
|
searchResults.offer(pagedListLiveData)
|
||||||
|
verify(view).observeSearchResults(pagedListLiveData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Loading offers a loading list item`() {
|
||||||
|
onLoadingState(LoadingState.Loading)
|
||||||
|
searchDepictionsFragmentPresenter.listFooterData.test()
|
||||||
|
.assertValue(listOf(FooterItem.LoadingItem))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Complete offers an empty list item and hides initial loader`() {
|
||||||
|
onLoadingState(LoadingState.Complete)
|
||||||
|
searchDepictionsFragmentPresenter.listFooterData.test()
|
||||||
|
.assertValue(emptyList())
|
||||||
|
verify(view).hideInitialLoadProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `InitialLoad shows initial loader`() {
|
||||||
|
onLoadingState(LoadingState.InitialLoad)
|
||||||
|
verify(view).showInitialLoadInProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Error offers a refresh list item, hides initial loader and shows error with a set text`() {
|
||||||
|
searchDepictionsFragmentPresenter.onQueryUpdated("test")
|
||||||
|
onLoadingState(LoadingState.Error)
|
||||||
|
verify(view).setEmptyViewText("test")
|
||||||
|
verify(view).showSnackbar()
|
||||||
|
verify(view).hideInitialLoadProgress()
|
||||||
|
searchDepictionsFragmentPresenter.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()
|
||||||
|
searchDepictionsFragmentPresenter.listFooterData.test()
|
||||||
|
.assertValue(listOf(FooterItem.RefreshItem))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `no Items event sets empty view text`() {
|
||||||
|
searchDepictionsFragmentPresenter.onQueryUpdated("test")
|
||||||
|
noItemLoadedEvent.offer(Unit)
|
||||||
|
verify(view).setEmptyViewText("test")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `retryFailedRequest calls retry`() {
|
||||||
|
searchDepictionsFragmentPresenter.retryFailedRequest()
|
||||||
|
verify(searchableDepictionsDataSourceFactory).retryFailedRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onDetachView stops subscriptions`() {
|
||||||
|
searchDepictionsFragmentPresenter.onDetachView()
|
||||||
|
onLoadingState(LoadingState.Loading)
|
||||||
|
searchDepictionsFragmentPresenter.listFooterData.test()
|
||||||
|
.assertValue(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onQueryUpdated updates dataSourceFactory`() {
|
||||||
|
searchDepictionsFragmentPresenter.onQueryUpdated("test")
|
||||||
|
verify(searchableDepictionsDataSourceFactory).onQueryUpdated("test")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onLoadingState(loadingState: LoadingState) {
|
||||||
|
loadingStates.offer(loadingState)
|
||||||
|
testScheduler.triggerActions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
package fr.free.nrw.commons.explore.depictions
|
|
||||||
|
|
||||||
import com.nhaarman.mockitokotlin2.whenever
|
|
||||||
import depictedItem
|
|
||||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
|
||||||
import io.reactivex.Single
|
|
||||||
import io.reactivex.schedulers.TestScheduler
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.mockito.Mock
|
|
||||||
import org.mockito.Mockito.verify
|
|
||||||
import org.mockito.MockitoAnnotations
|
|
||||||
|
|
||||||
class SearchDepictionsPresenterTest {
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
internal lateinit var view: SearchDepictionsFragmentContract.View
|
|
||||||
|
|
||||||
private lateinit var searchDepictionsFragmentPresenter: SearchDepictionsFragmentPresenter
|
|
||||||
|
|
||||||
private lateinit var testScheduler: TestScheduler
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private lateinit var jsonKvStore: JsonKvStore
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
lateinit var recentSearchesDao: RecentSearchesDao
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
lateinit var depictsClient: DepictsClient
|
|
||||||
|
|
||||||
@Before
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun setUp() {
|
|
||||||
MockitoAnnotations.initMocks(this)
|
|
||||||
testScheduler = TestScheduler()
|
|
||||||
searchDepictionsFragmentPresenter = SearchDepictionsFragmentPresenter(
|
|
||||||
jsonKvStore,
|
|
||||||
recentSearchesDao,
|
|
||||||
depictsClient,
|
|
||||||
testScheduler,
|
|
||||||
testScheduler
|
|
||||||
)
|
|
||||||
searchDepictionsFragmentPresenter.onAttachView(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun updateDepictionList() {
|
|
||||||
val expectedList = listOf(depictedItem())
|
|
||||||
whenever(depictsClient.searchForDepictions("rabbit", 25, 0))
|
|
||||||
.thenReturn(Single.just(expectedList))
|
|
||||||
searchDepictionsFragmentPresenter.updateDepictionList("rabbit", 25, false)
|
|
||||||
testScheduler.triggerActions()
|
|
||||||
verify(view)?.onSuccess(expectedList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import com.nhaarman.mockitokotlin2.*
|
||||||
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
import io.reactivex.processors.PublishProcessor
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
class SearchableDepictionsDataSourceFactoryTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var searchDepictionsDataSourceFactoryFactory: SearchDepictionsDataSourceFactoryFactory
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var liveDataConverter: LiveDataConverter
|
||||||
|
|
||||||
|
private lateinit var factory: SearchableDepictionsDataSourceFactory
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
factory = SearchableDepictionsDataSourceFactory(
|
||||||
|
searchDepictionsDataSourceFactoryFactory,
|
||||||
|
liveDataConverter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onQueryUpdated emits new liveData`() {
|
||||||
|
val (_, liveData) = expectNewLiveData()
|
||||||
|
factory.searchResults.test()
|
||||||
|
.also { factory.onQueryUpdated("test") }
|
||||||
|
.assertValue(liveData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onQueryUpdated invokes livedatconverter with no items emitter`() {
|
||||||
|
val (captor, _) = expectNewLiveData()
|
||||||
|
factory.onQueryUpdated("test")
|
||||||
|
factory.noItemsLoadedEvent.test()
|
||||||
|
.also { captor.firstValue.invoke() }
|
||||||
|
.assertValue(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Just for coverage, no way to really assert this
|
||||||
|
* */
|
||||||
|
@Test
|
||||||
|
fun `retryFailedRequest does nothing without a factory`() {
|
||||||
|
factory.retryFailedRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `retryFailedRequest retries with a factory`() {
|
||||||
|
val (_, _, dataSourceFactory) = expectNewLiveData()
|
||||||
|
factory.onQueryUpdated("test")
|
||||||
|
factory.retryFailedRequest()
|
||||||
|
verify(dataSourceFactory).retryFailedRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expectNewLiveData(): Triple<KArgumentCaptor<() -> Unit>, LiveData<PagedList<DepictedItem>>, SearchDepictionsDataSourceFactory> {
|
||||||
|
val dataSourceFactory: SearchDepictionsDataSourceFactory = mock()
|
||||||
|
whenever(
|
||||||
|
searchDepictionsDataSourceFactoryFactory.create(
|
||||||
|
"test",
|
||||||
|
factory.loadingStates as PublishProcessor<LoadingState>
|
||||||
|
)
|
||||||
|
).thenReturn(dataSourceFactory)
|
||||||
|
val captor = argumentCaptor<() -> Unit>()
|
||||||
|
val liveData: LiveData<PagedList<DepictedItem>> = mock()
|
||||||
|
whenever(liveDataConverter.convert(eq(dataSourceFactory), captor.capture()))
|
||||||
|
.thenReturn(liveData)
|
||||||
|
return Triple(captor, liveData, dataSourceFactory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ import org.mockito.InjectMocks
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.Mockito.`when`
|
import org.mockito.Mockito.`when`
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
import java.util.ArrayList
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -21,11 +21,11 @@ import java.util.ArrayList
|
||||||
class UploadPresenterTest {
|
class UploadPresenterTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal var repository: UploadRepository? = null
|
internal lateinit var repository: UploadRepository
|
||||||
@Mock
|
@Mock
|
||||||
internal var view: UploadContract.View? = null
|
internal lateinit var view: UploadContract.View
|
||||||
@Mock
|
@Mock
|
||||||
var contribution: Contribution? = null
|
lateinit var contribution: Contribution
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var uploadableFile: UploadableFile
|
private lateinit var uploadableFile: UploadableFile
|
||||||
|
|
@ -34,7 +34,7 @@ class UploadPresenterTest {
|
||||||
private lateinit var anotherUploadableFile: UploadableFile
|
private lateinit var anotherUploadableFile: UploadableFile
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
var uploadPresenter: UploadPresenter? = null
|
lateinit var uploadPresenter: UploadPresenter
|
||||||
|
|
||||||
private var uploadableFiles: ArrayList<UploadableFile> = ArrayList()
|
private var uploadableFiles: ArrayList<UploadableFile> = ArrayList()
|
||||||
|
|
||||||
|
|
@ -45,11 +45,11 @@ class UploadPresenterTest {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockitoAnnotations.initMocks(this)
|
MockitoAnnotations.initMocks(this)
|
||||||
uploadPresenter?.onAttachView(view)
|
uploadPresenter.onAttachView(view)
|
||||||
`when`(repository?.buildContributions()).thenReturn(Observable.just(contribution))
|
`when`(repository.buildContributions()).thenReturn(Observable.just(contribution))
|
||||||
uploadableFiles.add(uploadableFile)
|
uploadableFiles.add(uploadableFile)
|
||||||
`when`(view?.uploadableFiles).thenReturn(uploadableFiles)
|
`when`(view.uploadableFiles).thenReturn(uploadableFiles)
|
||||||
`when`(uploadableFile?.filePath).thenReturn("data://test")
|
`when`(uploadableFile.filePath).thenReturn("data://test")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,12 +57,12 @@ class UploadPresenterTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun handleSubmitTestUserLoggedIn() {
|
fun handleSubmitTestUserLoggedIn() {
|
||||||
`when`(view?.isLoggedIn).thenReturn(true)
|
`when`(view.isLoggedIn).thenReturn(true)
|
||||||
uploadPresenter?.handleSubmit()
|
uploadPresenter.handleSubmit()
|
||||||
verify(view)?.isLoggedIn
|
verify(view).isLoggedIn
|
||||||
verify(view)?.showProgress(true)
|
verify(view).showProgress(true)
|
||||||
verify(repository)?.buildContributions()
|
verify(repository).buildContributions()
|
||||||
verify(repository)?.buildContributions()
|
verify(repository).buildContributions()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -70,10 +70,10 @@ class UploadPresenterTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun handleSubmitTestUserNotLoggedIn() {
|
fun handleSubmitTestUserNotLoggedIn() {
|
||||||
`when`(view?.isLoggedIn).thenReturn(false)
|
`when`(view.isLoggedIn).thenReturn(false)
|
||||||
uploadPresenter?.handleSubmit()
|
uploadPresenter.handleSubmit()
|
||||||
verify(view)?.isLoggedIn
|
verify(view).isLoggedIn
|
||||||
verify(view)?.askUserToLogIn()
|
verify(view).askUserToLogIn()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,9 +88,9 @@ class UploadPresenterTest {
|
||||||
fun hideTopCardWhenReachedTheLastFile(){
|
fun hideTopCardWhenReachedTheLastFile(){
|
||||||
deletePictureBaseTest()
|
deletePictureBaseTest()
|
||||||
uploadableFiles.add(uploadableFile)
|
uploadableFiles.add(uploadableFile)
|
||||||
uploadPresenter?.deletePictureAtIndex(0)
|
uploadPresenter.deletePictureAtIndex(0)
|
||||||
verify(view)?.showHideTopCard(false)
|
verify(view).showHideTopCard(false)
|
||||||
verify(repository)?.deletePicture(ArgumentMatchers.anyString())
|
verify(repository).deletePicture(ArgumentMatchers.anyString())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -100,11 +100,11 @@ class UploadPresenterTest {
|
||||||
fun testDeleteWhenSingleUpload(){
|
fun testDeleteWhenSingleUpload(){
|
||||||
deletePictureBaseTest()
|
deletePictureBaseTest()
|
||||||
uploadableFiles.add(uploadableFile)
|
uploadableFiles.add(uploadableFile)
|
||||||
uploadPresenter?.deletePictureAtIndex(0)
|
uploadPresenter.deletePictureAtIndex(0)
|
||||||
verify(view)?.showHideTopCard(false)
|
verify(view).showHideTopCard(false)
|
||||||
verify(repository)?.deletePicture(ArgumentMatchers.anyString())
|
verify(repository).deletePicture(ArgumentMatchers.anyString())
|
||||||
verify(view)?.showMessage(ArgumentMatchers.anyInt())//As there is only one while which we are asking for deletion, upload should be cancelled and this flow should be triggered
|
verify(view).showMessage(ArgumentMatchers.anyInt())//As there is only one while which we are asking for deletion, upload should be cancelled and this flow should be triggered
|
||||||
verify(view)?.finish()
|
verify(view).finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -115,8 +115,8 @@ class UploadPresenterTest {
|
||||||
deletePictureBaseTest()
|
deletePictureBaseTest()
|
||||||
uploadableFiles.add(uploadableFile)
|
uploadableFiles.add(uploadableFile)
|
||||||
uploadableFiles.add(anotherUploadableFile)
|
uploadableFiles.add(anotherUploadableFile)
|
||||||
uploadPresenter?.deletePictureAtIndex(0)
|
uploadPresenter.deletePictureAtIndex(0)
|
||||||
verify(view)?.onUploadMediaDeleted(0)
|
verify(view).onUploadMediaDeleted(0)
|
||||||
verify(view)?.updateTopCardTitle()
|
verify(view).updateTopCardTitle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ ROOM_VERSION=2.2.3
|
||||||
PREFERENCE_VERSION=1.1.0
|
PREFERENCE_VERSION=1.1.0
|
||||||
CORE_KTX_VERSION=1.2.0
|
CORE_KTX_VERSION=1.2.0
|
||||||
ADAPTER_DELEGATES_VERSION=4.3.0
|
ADAPTER_DELEGATES_VERSION=4.3.0
|
||||||
|
PAGING_VERSION=2.1.2
|
||||||
MULTIDEX_VERSION=2.0.1
|
MULTIDEX_VERSION=2.0.1
|
||||||
|
|
||||||
systemProp.http.proxyPort=0
|
systemProp.http.proxyPort=0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue