#3818 Convert SubDepictionListFragment to use Pagination - replace SubDepictionList with Child and Parent Fragments - replace contracts with simple presenter declarations - move classes to appropriate packages - delete unused network models - delete duplicated paging classes

This commit is contained in:
Sean Mac Gillicuddy 2020-06-18 10:51:01 +01:00
parent 8318f78a71
commit e5e110eafa
80 changed files with 476 additions and 1467 deletions

View file

@ -5,7 +5,7 @@ import androidx.test.runner.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import fr.free.nrw.commons.explore.categories.ExploreActivity
import fr.free.nrw.commons.explore.ExploreActivity
@RunWith(AndroidJUnit4::class)
class ExploreActivityTest {

View file

@ -118,12 +118,12 @@
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".depictions.WikidataItemDetailsActivity"
android:name=".explore.depictions.WikidataItemDetailsActivity"
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".explore.categories.ExploreActivity"
android:name=".explore.ExploreActivity"
android:label="@string/title_activity_explore"
android:parentActivityName=".contributions.MainActivity"
android:configChanges="orientation|screenSize|keyboard" />

View file

@ -51,7 +51,7 @@ import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.explore.categories.ExploreActivity;
import fr.free.nrw.commons.explore.ExploreActivity;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.ConfigUtils;

View file

@ -1,23 +0,0 @@
package fr.free.nrw.commons.depictions
import dagger.Binds
import dagger.Module
import fr.free.nrw.commons.depictions.Media.DepictedImagesContract
import fr.free.nrw.commons.depictions.Media.DepictedImagesPresenter
import fr.free.nrw.commons.depictions.subClass.SubDepictionListContract
import fr.free.nrw.commons.depictions.subClass.SubDepictionListPresenter
/**
* The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
*/
@Module
abstract class DepictionModule {
@Binds
abstract fun SubDepictionListPresenter.bindsSubDepictionListPresenter()
: SubDepictionListContract.UserActionListener
@Binds
abstract fun DepictedImagesPresenter.bindsDepictedImagesContractPresenter()
: DepictedImagesContract.Presenter
}

View file

@ -1,12 +0,0 @@
package fr.free.nrw.commons.depictions.Media
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.explore.PagingContract
/**
* Contract with which DepictedImagesFragment and its presenter will talk to each other
*/
interface DepictedImagesContract {
interface View : PagingContract.View<Media>
interface Presenter : PagingContract.Presenter<Media>
}

View file

@ -1,57 +0,0 @@
package fr.free.nrw.commons.depictions.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Model class for object obtained while parsing depiction response
*/
public class Continue {
@SerializedName("sroffset")
@Expose
private Integer sroffset;
@SerializedName("continue")
@Expose
private String _continue;
/**
* No args constructor for use in serialization
*
*/
public Continue() {
}
/**
*
* @param sroffset
* @param _continue
*/
public Continue(Integer sroffset, String _continue) {
super();
this.sroffset = sroffset;
this._continue = _continue;
}
/**
* gets sroffset from Continue object
*/
public Integer getSroffset() {
return sroffset;
}
public void setSroffset(Integer sroffset) {
this.sroffset = sroffset;
}
/**
* gets continue string from Continue object
*/
public String getContinue() {
return _continue;
}
public void setContinue(String _continue) {
this._continue = _continue;
}
}

View file

@ -1,60 +0,0 @@
package fr.free.nrw.commons.depictions.models;
import java.util.List;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Model class for object obtained while parsing depiction response
*
* the getSearch() function is used to parse media
*/
public class Query {
@SerializedName("searchinfo")
@Expose
private Searchinfo searchinfo;
@SerializedName("search")
@Expose
private List<Search> search = null;
/**
* No args constructor for use in serialization
*
*/
public Query() {
}
/**
*
* @param search
* @param searchinfo
*/
public Query(Searchinfo searchinfo, List<Search> search) {
super();
this.searchinfo = searchinfo;
this.search = search;
}
/**
* return searchInfo
*/
public Searchinfo getSearchinfo() {
return searchinfo;
}
public void setSearchinfo(Searchinfo searchinfo) {
this.searchinfo = searchinfo;
}
/**
* the getSearch() function is used to parse media
*/
public List<Search> getSearch() {
return search;
}
public void setSearch(List<Search> search) {
this.search = search;
}
}

View file

@ -1,140 +0,0 @@
package fr.free.nrw.commons.depictions.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Model class for object obtained while parsing depiction response
* this class contains all the details of for the media object
*/
public class Search {
@SerializedName("ns")
@Expose
private Integer ns;
@SerializedName("title")
@Expose
private String title;
@SerializedName("pageid")
@Expose
private Integer pageid;
@SerializedName("size")
@Expose
private Integer size;
@SerializedName("wordcount")
@Expose
private Integer wordcount;
@SerializedName("snippet")
@Expose
private String snippet;
@SerializedName("timestamp")
@Expose
private String timestamp;
/**
* No args constructor for use in serialization
*
*/
public Search() {
}
/**
*
* @param timestamp
* @param title
* @param ns
* @param snippet
* @param wordcount
* @param size
* @param pageid
*/
public Search(Integer ns, String title, Integer pageid, Integer size, Integer wordcount, String snippet, String timestamp) {
super();
this.ns = ns;
this.title = title;
this.pageid = pageid;
this.size = size;
this.wordcount = wordcount;
this.snippet = snippet;
this.timestamp = timestamp;
}
/**
* returns ns int from Search object
*/
public Integer getNs() {
return ns;
}
public void setNs(Integer ns) {
this.ns = ns;
}
/**
* returns title string from Search object
*/
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
/**
* returns pageid int from Search object
*/
public Integer getPageid() {
return pageid;
}
public void setPageid(Integer pageid) {
this.pageid = pageid;
}
/**
* returns size int from Search object
*/
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
/**
* returns wordcount int from Search object
*/
public Integer getWordcount() {
return wordcount;
}
public void setWordcount(Integer wordcount) {
this.wordcount = wordcount;
}
/**
* returns snippet String from Search object
*/
public String getSnippet() {
return snippet;
}
public void setSnippet(String snippet) {
this.snippet = snippet;
}
/**
* returns ns int from Search object
*/
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
}

View file

@ -1,42 +0,0 @@
package fr.free.nrw.commons.depictions.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Model class for object obtained while parsing query object
*/
public class Searchinfo {
@SerializedName("totalhits")
@Expose
private Integer totalhits;
/**
* No args constructor for use in serialization
*
*/
public Searchinfo() {
}
/**
*
* @param totalhits
*/
public Searchinfo(Integer totalhits) {
super();
this.totalhits = totalhits;
}
/**
* returns "totalhint" integer in SearchInfo object
*/
public Integer getTotalhits() {
return totalhits;
}
public void setTotalhits(Integer totalhits) {
this.totalhits = totalhits;
}
}

View file

@ -1,11 +0,0 @@
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 }
)

View file

@ -1,32 +0,0 @@
package fr.free.nrw.commons.depictions.subClass;
import fr.free.nrw.commons.BasePresenter;
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
*/
public interface SubDepictionListContract {
interface View {
void onSuccess(List<DepictedItem> mediaList);
void initErrorView();
void showSnackbar();
void setIsLastPage(boolean b);
}
interface UserActionListener extends BasePresenter<View> {
void initSubDepictionList(String qid, Boolean isParentClass) throws IOException;
String getQuery();
}
}

View file

@ -1,144 +0,0 @@
package fr.free.nrw.commons.depictions.subClass;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
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.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
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.io.IOException;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import kotlin.Unit;
/**
* Fragment for parent classes and child classes of Depicted items in Explore
*/
public class SubDepictionListFragment extends DaggerFragment implements SubDepictionListContract.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;
/**
* Keeps a record of whether current instance of the fragment if of SubClass or ParentClass
*/
private boolean isParentClass = false;
private SubDepictionAdapter depictionsAdapter;
RecyclerView.LayoutManager layoutManager;
/**
* Stores entityId for the depiction
*/
private String entityId;
/**
* Stores name of the depiction searched
*/
private String depictsName;
@Inject SubDepictionListPresenter presenter;
private void initViews() {
if (getArguments() != null) {
depictsName = getArguments().getString("wikidataItemName");
entityId = getArguments().getString("entityId");
isParentClass = getArguments().getBoolean("isParentClass");
if (entityId != null) {
initList(entityId, isParentClass);
}
}
}
private void initList(String qid, Boolean isParentClass) {
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
} else {
progressBar.setVisibility(View.VISIBLE);
try {
presenter.initSubDepictionList(qid, isParentClass);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_browse_image, container, false);
ButterKnife.bind(this, v);
presenter.onAttachView(this);
isParentClass = false;
depictionNotFound.setVisibility(GONE);
if (getActivity().getResources().getConfiguration().orientation
== Configuration.ORIENTATION_PORTRAIT) {
layoutManager = new LinearLayoutManager(getContext());
} else {
layoutManager = new GridLayoutManager(getContext(), 2);
}
initViews();
depictionsRecyclerView.setLayoutManager(layoutManager);
depictionsAdapter = new SubDepictionAdapter(depictedItem -> {
// Open SubDepiction Details page
getActivity().finish();
WikidataItemDetailsActivity.startYourself(getContext(), depictedItem);
return Unit.INSTANCE;
});
depictionsRecyclerView.setAdapter(depictionsAdapter);
return v;
}
private void handleNoInternet() {
progressBar.setVisibility(GONE);
ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.no_internet);
}
@Override
public void onSuccess(List<DepictedItem> mediaList) {
progressBar.setVisibility(View.GONE);
depictionNotFound.setVisibility(GONE);
bottomProgressBar.setVisibility(GONE);
depictionsAdapter.addAll(mediaList);
}
@Override
public void initErrorView() {
progressBar.setVisibility(GONE);
bottomProgressBar.setVisibility(GONE);
depictionNotFound.setVisibility(VISIBLE);
String no_depiction = getString(isParentClass? R.string.no_parent_classes: R.string.no_child_classes);
depictionNotFound.setText(String.format(Locale.getDefault(), no_depiction, depictsName));
}
@Override
public void showSnackbar() {
ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.error_loading_depictions);
}
@Override
public void setIsLastPage(boolean b) {
}
}

View file

@ -1,116 +0,0 @@
package fr.free.nrw.commons.depictions.subClass;
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.explore.depictions.DepictsClient;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import java.io.IOException;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
/**
* Presenter for parent classes and child classes of Depicted items in Explore
*/
public class SubDepictionListPresenter implements SubDepictionListContract.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 SubDepictionListContract.View DUMMY = (SubDepictionListContract.View) Proxy
.newProxyInstance(
SubDepictionListContract.View.class.getClassLoader(),
new Class[]{SubDepictionListContract.View.class},
(proxy, method, methodArgs) -> null);
private final Scheduler ioScheduler;
private final Scheduler mainThreadScheduler;
private SubDepictionListContract.View view = DUMMY;
RecentSearchesDao recentSearchesDao;
/**
* Value of the search query
*/
public String query;
protected CompositeDisposable compositeDisposable = new CompositeDisposable();
DepictsClient depictsClient;
private List<DepictedItem> queryList = new ArrayList<>();
OkHttpJsonApiClient okHttpJsonApiClient;
@Inject
public SubDepictionListPresenter(RecentSearchesDao recentSearchesDao, DepictsClient depictsClient, OkHttpJsonApiClient okHttpJsonApiClient, @Named(IO_THREAD) Scheduler ioScheduler,
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
this.recentSearchesDao = recentSearchesDao;
this.ioScheduler = ioScheduler;
this.mainThreadScheduler = mainThreadScheduler;
this.depictsClient = depictsClient;
this.okHttpJsonApiClient = okHttpJsonApiClient;
}
@Override
public void onAttachView(SubDepictionListContract.View view) {
this.view = view;
}
@Override
public void onDetachView() {
this.view = DUMMY;
}
@Override
public void initSubDepictionList(String qid, Boolean isParentClass) throws IOException {
if (isParentClass) {
compositeDisposable.add(okHttpJsonApiClient.getParentQIDs(qid)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.subscribe(this::handleSuccess, this::handleError));
} else {
compositeDisposable.add(okHttpJsonApiClient.getChildQIDs(qid)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.subscribe(this::handleSuccess, this::handleError));
}
}
@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);
}
}
/**
* Logs and handles API error scenario
*/
private void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading queried depictions");
view.initErrorView();
view.showSnackbar();
}
}

View file

@ -11,9 +11,9 @@ import fr.free.nrw.commons.bookmarks.BookmarksActivity;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.explore.categories.ExploreActivity;
import fr.free.nrw.commons.explore.ExploreActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.review.ReviewActivity;
import fr.free.nrw.commons.settings.SettingsActivity;

View file

@ -11,7 +11,7 @@ import dagger.android.support.AndroidSupportInjectionModule;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionsModule;
import fr.free.nrw.commons.depictions.DepictionModule;
import fr.free.nrw.commons.explore.depictions.DepictionModule;
import fr.free.nrw.commons.explore.SearchModule;
import fr.free.nrw.commons.review.ReviewController;
import fr.free.nrw.commons.settings.SettingsFragment;

View file

@ -8,10 +8,11 @@ import fr.free.nrw.commons.category.CategoryImagesListFragment;
import fr.free.nrw.commons.category.SubCategoryListFragment;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.contributions.ContributionsListFragment;
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment;
import fr.free.nrw.commons.depictions.subClass.SubDepictionListFragment;
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment;
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment;
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment;
import fr.free.nrw.commons.explore.categories.SearchCategoryFragment;
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragment;
import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment;
import fr.free.nrw.commons.explore.media.SearchMediaFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailFragment;
@ -51,9 +52,6 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract DepictedImagesFragment bindDepictedImagesFragment();
@ContributesAndroidInjector
abstract SubDepictionListFragment bindSubDepictionListFragment();
@ContributesAndroidInjector
abstract SubCategoryListFragment bindSubCategoryListFragment();
@ -95,4 +93,10 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract MediaLicenseFragment bindMediaLicenseFragment();
@ContributesAndroidInjector
abstract ParentDepictionsFragment bindParentDepictionsFragment();
@ContributesAndroidInjector
abstract ChildDepictionsFragment bindChildDepictionsFragment();
}

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.explore.categories;
package fr.free.nrw.commons.explore;
import android.content.Context;
import android.content.Intent;
@ -19,8 +19,6 @@ import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.category.CategoryImagesListFragment;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.explore.ViewPagerAdapter;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import java.util.ArrayList;

View file

@ -1,54 +0,0 @@
package fr.free.nrw.commons.explore
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)

View file

@ -20,7 +20,7 @@ import fr.free.nrw.commons.Media;
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.depictions.search.SearchDepictionsFragment;
import fr.free.nrw.commons.explore.media.SearchMediaFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;

View file

@ -1,14 +0,0 @@
package fr.free.nrw.commons.explore
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.explore.categories.SearchCategoriesFragmentContract
import fr.free.nrw.commons.explore.categories.PageableCategoriesDataSource
import io.reactivex.Scheduler
import javax.inject.Inject
import javax.inject.Named
class SearchCategoriesFragmentPresenter @Inject constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableCategoriesDataSource
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
SearchCategoriesFragmentContract.Presenter

View file

@ -2,11 +2,12 @@ package fr.free.nrw.commons.explore
import dagger.Binds
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.categories.SearchCategoriesFragmentPresenter
import fr.free.nrw.commons.explore.categories.SearchCategoriesFragmentPresenterImpl
import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragmentPresenter
import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragmentPresenterImpl
import fr.free.nrw.commons.explore.media.SearchMediaFragmentPresenter
import fr.free.nrw.commons.explore.media.SearchMediaFragmentPresenterImpl
/**
* The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
@ -14,14 +15,14 @@ import fr.free.nrw.commons.explore.media.SearchMediaFragmentPresenter
@Module
abstract class SearchModule {
@Binds
abstract fun SearchDepictionsFragmentPresenter.bindsSearchDepictionsFragmentPresenter()
: SearchDepictionsFragmentContract.Presenter
abstract fun SearchDepictionsFragmentPresenterImpl.bindsSearchDepictionsFragmentPresenter()
: SearchDepictionsFragmentPresenter
@Binds
abstract fun SearchCategoriesFragmentPresenter.bindsSearchCategoriesFragmentPresenter()
: SearchCategoriesFragmentContract.Presenter
abstract fun SearchCategoriesFragmentPresenterImpl.bindsSearchCategoriesFragmentPresenter()
: SearchCategoriesFragmentPresenter
@Binds
abstract fun SearchMediaFragmentPresenter.bindsSearchMediaFragmentPresenter()
: SearchMediaFragmentContract.Presenter
abstract fun SearchMediaFragmentPresenterImpl.bindsSearchMediaFragmentPresenter()
: SearchMediaFragmentPresenter
}

View file

@ -1,8 +1,8 @@
package fr.free.nrw.commons.explore.categories
import fr.free.nrw.commons.category.CategoryClient
import fr.free.nrw.commons.explore.LiveDataConverter
import fr.free.nrw.commons.explore.PageableBaseDataSource
import fr.free.nrw.commons.explore.paging.LiveDataConverter
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
import javax.inject.Inject
class PageableCategoriesDataSource @Inject constructor(

View file

@ -6,8 +6,8 @@ import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CATEGORY_PREFIX
import fr.free.nrw.commons.explore.BaseViewHolder
import fr.free.nrw.commons.explore.inflate
import fr.free.nrw.commons.explore.paging.BaseViewHolder
import fr.free.nrw.commons.explore.paging.inflate
import kotlinx.android.synthetic.main.item_recent_searches.*

View file

@ -1,8 +0,0 @@
package fr.free.nrw.commons.explore.categories
import fr.free.nrw.commons.explore.PagingContract
interface SearchCategoriesFragmentContract {
interface View : PagingContract.View<String>
interface Presenter : PagingContract.Presenter<String>
}

View file

@ -0,0 +1,16 @@
package fr.free.nrw.commons.explore.categories
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.explore.paging.BasePagingPresenter
import fr.free.nrw.commons.explore.paging.PagingContract
import io.reactivex.Scheduler
import javax.inject.Inject
import javax.inject.Named
interface SearchCategoriesFragmentPresenter : PagingContract.Presenter<String>
class SearchCategoriesFragmentPresenterImpl @Inject constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableCategoriesDataSource
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
SearchCategoriesFragmentPresenter

View file

@ -2,8 +2,7 @@ package fr.free.nrw.commons.explore.categories
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryDetailsActivity
import fr.free.nrw.commons.explore.BasePagingFragment
import fr.free.nrw.commons.explore.PagingContract
import fr.free.nrw.commons.explore.paging.BasePagingFragment
import javax.inject.Inject
/**
@ -11,7 +10,7 @@ import javax.inject.Inject
*/
class SearchCategoryFragment : BasePagingFragment<String>() {
@Inject
lateinit var presenter: SearchCategoriesFragmentContract.Presenter
lateinit var presenter: SearchCategoriesFragmentPresenter
override val errorTextId: Int = R.string.error_loading_categories

View file

@ -6,7 +6,7 @@ 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.explore.inflate
import fr.free.nrw.commons.explore.paging.inflate
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_depictions.*

View file

@ -1,21 +0,0 @@
package fr.free.nrw.commons.explore.depictions
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import fr.free.nrw.commons.R
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import kotlinx.android.synthetic.main.item_depictions.*
fun depictionDelegate(onDepictionClicked: (DepictedItem) -> Unit) =
adapterDelegateLayoutContainer<DepictedItem, DepictedItem>(R.layout.item_depictions) {
containerView.setOnClickListener { onDepictionClicked(item) }
bind {
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)
}
}
}

View file

@ -0,0 +1,29 @@
package fr.free.nrw.commons.explore.depictions
import dagger.Binds
import dagger.Module
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsPresenter
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsPresenterImpl
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesPresenter
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesPresenterImpl
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsPresenter
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsPresenterImpl
/**
* The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
*/
@Module
abstract class DepictionModule {
@Binds
abstract fun ParentDepictionsPresenterImpl.bindsParentDepictionPresenter()
: ParentDepictionsPresenter
@Binds
abstract fun ChildDepictionsPresenterImpl.bindsChildDepictionPresenter()
: ChildDepictionsPresenter
@Binds
abstract fun DepictedImagesPresenterImpl.bindsDepictedImagesContractPresenter()
: DepictedImagesPresenter
}

View file

@ -1,31 +1,21 @@
package fr.free.nrw.commons.explore.depictions
import fr.free.nrw.commons.depictions.subClass.models.SparqlResponse
import fr.free.nrw.commons.media.MediaInterface
import fr.free.nrw.commons.mwapi.SparqlResponse
import fr.free.nrw.commons.upload.depicts.DepictsInterface
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.utils.CommonsDateUtil
import io.reactivex.Observable
import io.reactivex.Single
import org.wikipedia.wikidata.Entities
import java.math.BigInteger
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.text.ParseException
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
const val LARGE_IMAGE_SIZE = "640px"
const val THUMB_IMAGE_SIZE = "70px"
/**
* Depicts Client to handle custom calls to Commons Wikibase APIs
*/
@Singleton
class DepictsClient @Inject constructor(
private val depictsInterface: DepictsInterface,
private val mediaInterface: MediaInterface
private val depictsInterface: DepictsInterface
) {
/**
@ -40,10 +30,6 @@ class DepictsClient @Inject constructor(
.map { it.entities().values.map(::DepictedItem) }
}
private fun getUrl(title: String): String {
return getImageUrl(title, LARGE_IMAGE_SIZE)
}
fun getEntities(ids: String): Single<Entities> {
return depictsInterface.getEntities(ids)
}
@ -57,63 +43,4 @@ class DepictsClient @Inject constructor(
.flatMap { getEntities(it).toObservable() }
.map { it.entities().values.map(::DepictedItem) }
}
companion object {
/**
* Get url for the image from media of depictions
* Ex: Tiger_Woods
* Value: https://upload.wikimedia.org/wikipedia/commons/thumb/6/67/Tiger_Woods.jpg/70px-Tiger_Woods.jpg
*/
fun getImageUrl(title: String, size: String): String {
return title.substringAfter(":")
.replace(" ", "_")
.let {
val MD5Hash = getMd5(it)
"https://upload.wikimedia.org/wikipedia/commons/thumb/${MD5Hash[0]}/${MD5Hash[0]}${MD5Hash[1]}/$it/$size-$it"
}
}
/**
* Generates MD5 hash for the filename
*/
fun getMd5(input: String): String {
return try {
// Static getInstance method is called with hashing MD5
val md = MessageDigest.getInstance("MD5")
// digest() method is called to calculate message digest
// of an input digest() return array of byte
val messageDigest = md.digest(input.toByteArray())
// Convert byte array into signum representation
val no = BigInteger(1, messageDigest)
// Convert message digest into hex value
var hashtext = no.toString(16)
while (hashtext.length < 32) {
hashtext = "0$hashtext"
}
hashtext
} // For specifying wrong message digest algorithms
catch (e: NoSuchAlgorithmException) {
throw RuntimeException(e)
}
}
/**
* Parse the date string into the required format
* @param dateStr
* @return date in the required format
*/
private fun safeParseDate(dateStr: String): Date? {
return try {
CommonsDateUtil.getIso8601DateFormatShort().parse(dateStr)
} catch (e: ParseException) {
null
}
}
}
}

View file

@ -0,0 +1,13 @@
package fr.free.nrw.commons.explore.depictions
import fr.free.nrw.commons.R
import fr.free.nrw.commons.explore.paging.BasePagingFragment
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
abstract class PageableDepictionsFragment : BasePagingFragment<DepictedItem>() {
override val errorTextId: Int = R.string.error_loading_depictions
override val pagedListAdapter by lazy {
DepictionAdapter { WikidataItemDetailsActivity.startYourself(context, it) }
}
}

View file

@ -1,63 +0,0 @@
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)
}
}

View file

@ -1,27 +0,0 @@
package fr.free.nrw.commons.explore.depictions
import fr.free.nrw.commons.R
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity
import fr.free.nrw.commons.explore.BasePagingFragment
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import javax.inject.Inject
/**
* Display depictions in search fragment
*/
class SearchDepictionsFragment : BasePagingFragment<DepictedItem>(),
SearchDepictionsFragmentContract.View {
@Inject
lateinit var presenter: SearchDepictionsFragmentContract.Presenter
override val errorTextId: Int = R.string.error_loading_depictions
override val injectedPresenter
get() = presenter
override val pagedListAdapter by lazy {
DepictionAdapter { WikidataItemDetailsActivity.startYourself(context, it) }
}
override fun getEmptyText(query: String) = getString(R.string.depictions_not_found, query)
}

View file

@ -1,12 +0,0 @@
package fr.free.nrw.commons.explore.depictions
import fr.free.nrw.commons.explore.PagingContract
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 : PagingContract.View<DepictedItem>
interface Presenter : PagingContract.Presenter<DepictedItem>
}

View file

@ -1,88 +0,0 @@
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()
}

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.depictions;
package fr.free.nrw.commons.explore.depictions;
import android.content.Context;
import android.content.Intent;
@ -13,8 +13,9 @@ import butterknife.ButterKnife;
import com.google.android.material.tabs.TabLayout;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment;
import fr.free.nrw.commons.depictions.subClass.SubDepictionListFragment;
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment;
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment;
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment;
import fr.free.nrw.commons.explore.ViewPagerAdapter;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
@ -87,28 +88,23 @@ public class WikidataItemDetailsActivity extends NavigationBaseActivity implemen
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
depictionImagesListFragment = new DepictedImagesFragment();
SubDepictionListFragment subDepictionListFragment = new SubDepictionListFragment();
SubDepictionListFragment parentDepictionListFragment = new SubDepictionListFragment();
ChildDepictionsFragment childDepictionsFragment = new ChildDepictionsFragment();
ParentDepictionsFragment parentDepictionsFragment = new ParentDepictionsFragment();
wikidataItemName = getIntent().getStringExtra("wikidataItemName");
String entityId = getIntent().getStringExtra("entityId");
if (getIntent() != null && wikidataItemName != null) {
Bundle arguments = new Bundle();
arguments.putString("wikidataItemName", wikidataItemName);
arguments.putString("entityId", entityId);
arguments.putBoolean("isParentClass", false);
depictionImagesListFragment.setArguments(arguments);
subDepictionListFragment.setArguments(arguments);
Bundle parentClassArguments = new Bundle();
parentClassArguments.putString("wikidataItemName", wikidataItemName);
parentClassArguments.putString("entityId", entityId);
parentClassArguments.putBoolean("isParentClass", true);
parentDepictionListFragment.setArguments(parentClassArguments);
parentDepictionsFragment.setArguments(arguments);
childDepictionsFragment.setArguments(arguments);
}
fragmentList.add(depictionImagesListFragment);
titleList.add(getResources().getString(R.string.title_for_media));
fragmentList.add(subDepictionListFragment);
fragmentList.add(childDepictionsFragment);
titleList.add(getResources().getString(R.string.title_for_child_classes));
fragmentList.add(parentDepictionListFragment);
fragmentList.add(parentDepictionsFragment);
titleList.add(getResources().getString(R.string.title_for_parent_classes));
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPager.setOffscreenPageLimit(2);
@ -183,7 +179,6 @@ public class WikidataItemDetailsActivity extends NavigationBaseActivity implemen
*/
public static void startYourself(Context context, DepictedItem depictedItem) {
Intent intent = new Intent(context, WikidataItemDetailsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra("wikidataItemName", depictedItem.getName());
intent.putExtra("entityId", depictedItem.getId());
context.startActivity(intent);

View file

@ -0,0 +1,24 @@
package fr.free.nrw.commons.explore.depictions.child
import android.os.Bundle
import android.view.View
import fr.free.nrw.commons.R
import fr.free.nrw.commons.explore.depictions.PageableDepictionsFragment
import javax.inject.Inject
class ChildDepictionsFragment: PageableDepictionsFragment() {
@Inject
lateinit var presenter: ChildDepictionsPresenter
override val injectedPresenter
get() = presenter
override fun getEmptyText(query: String) =
getString(R.string.no_child_classes, arguments!!.getString("wikidataItemName")!!)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onQueryUpdated(arguments!!.getString("entityId")!!)
}
}

View file

@ -0,0 +1,17 @@
package fr.free.nrw.commons.explore.depictions.child
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.explore.paging.BasePagingPresenter
import fr.free.nrw.commons.explore.paging.PagingContract
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.Scheduler
import javax.inject.Inject
import javax.inject.Named
interface ChildDepictionsPresenter : PagingContract.Presenter<DepictedItem>
class ChildDepictionsPresenterImpl @Inject constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableChildDepictionsDataSource
) : BasePagingPresenter<DepictedItem>(mainThreadScheduler, dataSourceFactory),
ChildDepictionsPresenter

View file

@ -0,0 +1,18 @@
package fr.free.nrw.commons.explore.depictions.child
import fr.free.nrw.commons.explore.paging.LiveDataConverter
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import javax.inject.Inject
class PageableChildDepictionsDataSource @Inject constructor(
liveDataConverter: LiveDataConverter,
private val okHttpJsonApiClient: OkHttpJsonApiClient
) : PageableBaseDataSource<DepictedItem>(liveDataConverter) {
override val loadFunction = { _: Int, startPosition: Int ->
if (startPosition == 0) okHttpJsonApiClient.getChildDepictions(query).blockingFirst()
else emptyList()
}
}

View file

@ -1,14 +1,14 @@
package fr.free.nrw.commons.depictions.Media
package fr.free.nrw.commons.explore.depictions.media
import android.os.Bundle
import android.view.View
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity
import fr.free.nrw.commons.explore.media.PageableMediaFragment
import javax.inject.Inject
class DepictedImagesFragment : PageableMediaFragment(), DepictedImagesContract.View {
class DepictedImagesFragment : PageableMediaFragment() {
@Inject
lateinit var presenter: DepictedImagesContract.Presenter
lateinit var presenter: DepictedImagesPresenter
override val injectedPresenter
get() = presenter

View file

@ -1,17 +1,20 @@
package fr.free.nrw.commons.depictions.Media
package fr.free.nrw.commons.explore.depictions.media
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.explore.BasePagingPresenter
import fr.free.nrw.commons.explore.paging.BasePagingPresenter
import fr.free.nrw.commons.explore.paging.PagingContract
import io.reactivex.Scheduler
import javax.inject.Inject
import javax.inject.Named
interface DepictedImagesPresenter : PagingContract.Presenter<Media>
/**
* Presenter for DepictedImagesFragment
*/
class DepictedImagesPresenter @Inject constructor(
class DepictedImagesPresenterImpl @Inject constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableDepictedMediaDataSource
) : BasePagingPresenter<Media>(mainThreadScheduler, dataSourceFactory),
DepictedImagesContract.Presenter
DepictedImagesPresenter

View file

@ -1,9 +1,9 @@
package fr.free.nrw.commons.depictions.Media
package fr.free.nrw.commons.explore.depictions.media
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.explore.LiveDataConverter
import fr.free.nrw.commons.explore.PageableBaseDataSource
import fr.free.nrw.commons.explore.depictions.LoadFunction
import fr.free.nrw.commons.explore.paging.LiveDataConverter
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
import fr.free.nrw.commons.explore.depictions.search.LoadFunction
import fr.free.nrw.commons.media.MediaClient
import javax.inject.Inject

View file

@ -0,0 +1,18 @@
package fr.free.nrw.commons.explore.depictions.parent
import fr.free.nrw.commons.explore.paging.LiveDataConverter
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import javax.inject.Inject
class PageableParentDepictionsDataSource @Inject constructor(
liveDataConverter: LiveDataConverter,
private val okHttpJsonApiClient: OkHttpJsonApiClient
) : PageableBaseDataSource<DepictedItem>(liveDataConverter) {
override val loadFunction = { _: Int, startPosition: Int ->
if (startPosition == 0) okHttpJsonApiClient.getParentDepictions(query).blockingFirst()
else emptyList()
}
}

View file

@ -0,0 +1,24 @@
package fr.free.nrw.commons.explore.depictions.parent
import android.os.Bundle
import android.view.View
import fr.free.nrw.commons.R
import fr.free.nrw.commons.explore.depictions.PageableDepictionsFragment
import javax.inject.Inject
class ParentDepictionsFragment : PageableDepictionsFragment() {
@Inject
lateinit var presenter: ParentDepictionsPresenter
override val injectedPresenter
get() = presenter
override fun getEmptyText(query: String) =
getString(R.string.no_parent_classes, arguments!!.getString("wikidataItemName")!!)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onQueryUpdated(arguments!!.getString("entityId")!!)
}
}

View file

@ -0,0 +1,19 @@
package fr.free.nrw.commons.explore.depictions.parent
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.explore.paging.BasePagingPresenter
import fr.free.nrw.commons.explore.paging.PagingContract
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.Scheduler
import javax.inject.Inject
import javax.inject.Named
interface ParentDepictionsPresenter : PagingContract.Presenter<DepictedItem>
class ParentDepictionsPresenterImpl @Inject constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableParentDepictionsDataSource
) : BasePagingPresenter<DepictedItem>(mainThreadScheduler, dataSourceFactory),
ParentDepictionsPresenter {
}

View file

@ -1,8 +1,9 @@
package fr.free.nrw.commons.explore.depictions
package fr.free.nrw.commons.explore.depictions.search
import fr.free.nrw.commons.explore.LiveDataConverter
import fr.free.nrw.commons.explore.LoadingState
import fr.free.nrw.commons.explore.PageableBaseDataSource
import fr.free.nrw.commons.explore.paging.LiveDataConverter
import fr.free.nrw.commons.explore.paging.LoadingState
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.processors.PublishProcessor
import javax.inject.Inject

View file

@ -0,0 +1,18 @@
package fr.free.nrw.commons.explore.depictions.search
import fr.free.nrw.commons.R
import fr.free.nrw.commons.explore.depictions.PageableDepictionsFragment
import javax.inject.Inject
/**
* Display depictions in search fragment
*/
class SearchDepictionsFragment : PageableDepictionsFragment() {
@Inject
lateinit var presenter: SearchDepictionsFragmentPresenter
override val injectedPresenter
get() = presenter
override fun getEmptyText(query: String) = getString(R.string.depictions_not_found, query)
}

View file

@ -1,17 +1,19 @@
package fr.free.nrw.commons.explore.depictions
package fr.free.nrw.commons.explore.depictions.search
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.explore.BasePagingPresenter
import fr.free.nrw.commons.explore.paging.BasePagingPresenter
import fr.free.nrw.commons.explore.paging.PagingContract
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.Scheduler
import javax.inject.Inject
import javax.inject.Named
interface SearchDepictionsFragmentPresenter : PagingContract.Presenter<DepictedItem>
/**
* The presenter class for SearchDepictionsFragment
*/
class SearchDepictionsFragmentPresenter @Inject constructor(
class SearchDepictionsFragmentPresenterImpl @Inject constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableDepictionsDataSource
) : BasePagingPresenter<DepictedItem>(mainThreadScheduler, dataSourceFactory),
SearchDepictionsFragmentContract.Presenter
SearchDepictionsFragmentPresenter

View file

@ -1,9 +1,9 @@
package fr.free.nrw.commons.explore.media
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.explore.LiveDataConverter
import fr.free.nrw.commons.explore.PageableBaseDataSource
import fr.free.nrw.commons.explore.depictions.LoadFunction
import fr.free.nrw.commons.explore.paging.LiveDataConverter
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
import fr.free.nrw.commons.explore.depictions.search.LoadFunction
import fr.free.nrw.commons.media.MediaClient
import javax.inject.Inject

View file

@ -4,7 +4,7 @@ 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.BasePagingFragment
import fr.free.nrw.commons.explore.paging.BasePagingFragment
import kotlinx.android.synthetic.main.fragment_search_paginated.*

View file

@ -6,8 +6,8 @@ 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 fr.free.nrw.commons.explore.paging.BaseViewHolder
import fr.free.nrw.commons.explore.paging.inflate
import kotlinx.android.synthetic.main.layout_category_images.*
class PagedMediaAdapter(private val onImageClicked: (Int) -> Unit) :

View file

@ -7,15 +7,15 @@ import javax.inject.Inject
/**
* Displays the image search screen.
*/
class SearchMediaFragment : PageableMediaFragment(), SearchMediaFragmentContract.View {
class SearchMediaFragment : PageableMediaFragment(){
@Inject
lateinit var presenter: SearchMediaFragmentContract.Presenter
lateinit var presenter: SearchMediaFragmentPresenter
override val injectedPresenter
get() = presenter
override fun onItemClicked(position: Int) {
(context as SearchActivity?)!!.onSearchImageClicked(position)
(context as SearchActivity).onSearchImageClicked(position)
}
override fun notifyViewPager() {

View file

@ -1,10 +0,0 @@
package fr.free.nrw.commons.explore.media
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.explore.PagingContract
interface SearchMediaFragmentContract {
interface View : PagingContract.View<Media>
interface Presenter : PagingContract.Presenter<Media>
}

View file

@ -2,13 +2,16 @@ 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.BasePagingPresenter
import fr.free.nrw.commons.explore.paging.PagingContract
import fr.free.nrw.commons.explore.paging.BasePagingPresenter
import io.reactivex.Scheduler
import javax.inject.Inject
import javax.inject.Named
class SearchMediaFragmentPresenter @Inject constructor(
interface SearchMediaFragmentPresenter : PagingContract.Presenter<Media>
class SearchMediaFragmentPresenterImpl @Inject constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableMediaDataSource
) : BasePagingPresenter<Media>(mainThreadScheduler, dataSourceFactory),
SearchMediaFragmentContract.Presenter
SearchMediaFragmentPresenter

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.explore
package fr.free.nrw.commons.explore.paging
import android.content.Context
import android.content.res.Configuration

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.explore
package fr.free.nrw.commons.explore.paging
import androidx.lifecycle.MutableLiveData
import fr.free.nrw.commons.upload.depicts.proxy

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.explore
package fr.free.nrw.commons.explore.paging
import android.view.View
import androidx.recyclerview.widget.RecyclerView

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.explore.depictions
package fr.free.nrw.commons.explore.paging
import android.view.LayoutInflater
import android.view.View
@ -26,7 +26,9 @@ class FooterAdapter(private val onRefreshClicked: () -> Unit) :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
when (FooterItem.values()[viewType]) {
FooterItem.LoadingItem -> LoadingViewHolder(parent.inflate(R.layout.list_item_progress))
FooterItem.LoadingItem -> LoadingViewHolder(
parent.inflate(R.layout.list_item_progress)
)
FooterItem.RefreshItem -> RefreshViewHolder(
parent.inflate(R.layout.list_item_load_more),
onRefreshClicked

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.explore
package fr.free.nrw.commons.explore.paging
import androidx.lifecycle.LiveData
import androidx.paging.PagedList

View file

@ -1,13 +1,13 @@
package fr.free.nrw.commons.explore
package fr.free.nrw.commons.explore.paging
import androidx.paging.PositionalDataSource
import fr.free.nrw.commons.explore.depictions.LoadFunction
import fr.free.nrw.commons.explore.depictions.search.LoadFunction
import io.reactivex.Completable
import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
abstract class SearchDataSource<T>(
abstract class PagingDataSource<T>(
private val loadingStates: PublishProcessor<LoadingState>
) : PositionalDataSource<T>() {
@ -60,7 +60,7 @@ abstract class SearchDataSource<T>(
fun <T> dataSource(
loadingStates: PublishProcessor<LoadingState>,
loadFunction: LoadFunction<T>
) = object : SearchDataSource<T>(loadingStates) {
) = object : PagingDataSource<T>(loadingStates) {
override fun getItems(loadSize: Int, startPosition: Int): List<T> {
return loadFunction(loadSize, startPosition)
}

View file

@ -1,12 +1,12 @@
package fr.free.nrw.commons.explore
package fr.free.nrw.commons.explore.paging
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.explore.depictions.LoadFunction
import fr.free.nrw.commons.explore.depictions.LoadingStates
import fr.free.nrw.commons.explore.depictions.search.LoadFunction
import fr.free.nrw.commons.explore.depictions.search.LoadingStates
import io.reactivex.Flowable
import io.reactivex.processors.PublishProcessor
import javax.inject.Inject
@ -18,7 +18,10 @@ abstract class PageableBaseDataSource<T>(private val liveDataConverter: LiveData
lateinit var query: String
private val dataSourceFactoryFactory: () -> PagingDataSourceFactory<T> = {
dataSourceFactory(_loadingStates, loadFunction)
dataSourceFactory(
_loadingStates,
loadFunction
)
}
private val _loadingStates = PublishProcessor.create<LoadingState>()
val loadingStates: Flowable<LoadingState> = _loadingStates
@ -67,11 +70,14 @@ class LiveDataConverter @Inject constructor() {
abstract class PagingDataSourceFactory<T>(val loadingStates: LoadingStates) :
DataSource.Factory<Int, T>() {
private var currentDataSource: SearchDataSource<T>? = null
private var currentDataSource: PagingDataSource<T>? = null
abstract val loadFunction: LoadFunction<T>
override fun create() =
dataSource(loadingStates, loadFunction).also { currentDataSource = it }
dataSource(
loadingStates,
loadFunction
).also { currentDataSource = it }
fun retryFailedRequest() {
currentDataSource?.retryFailedRequest()

View file

@ -48,7 +48,7 @@ import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.delete.DeleteHelper;
import fr.free.nrw.commons.delete.ReasonBuilder;
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.ui.widget.HtmlTextView;
import fr.free.nrw.commons.utils.ViewUtilWrapper;

View file

@ -6,7 +6,6 @@ import com.google.gson.Gson;
import fr.free.nrw.commons.achievements.FeaturedImages;
import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import fr.free.nrw.commons.depictions.subClass.models.SparqlResponse;
import fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
@ -210,7 +209,7 @@ public class OkHttpJsonApiClient {
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
* bridge -> suspended bridge, aqueduct, etc
*/
public Observable<List<DepictedItem>> getChildQIDs(String qid) throws IOException {
public Observable<List<DepictedItem>> getChildDepictions(String qid) throws IOException {
return depictedItemsFrom(sparqlQuery(qid, "/queries/subclasses_query.rq"));
}
@ -218,7 +217,7 @@ public class OkHttpJsonApiClient {
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
* bridge -> suspended bridge, aqueduct, etc
*/
public Observable<List<DepictedItem>> getParentQIDs(String qid) throws IOException {
public Observable<List<DepictedItem>> getParentDepictions(String qid) throws IOException {
return depictedItemsFrom(sparqlQuery(qid, "/queries/parentclasses_query.rq"));
}

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.depictions.subClass.models
package fr.free.nrw.commons.mwapi
data class SparqlResponse(val results: Result)

View file

@ -42,7 +42,7 @@ import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.LogoutClient;
import fr.free.nrw.commons.bookmarks.BookmarksActivity;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.explore.categories.ExploreActivity;
import fr.free.nrw.commons.explore.ExploreActivity;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.logging.CommonsLogSender;
import fr.free.nrw.commons.review.ReviewActivity;

View file

@ -1,7 +1,5 @@
package fr.free.nrw.commons.upload.structure.depictions
import fr.free.nrw.commons.explore.depictions.DepictsClient.Companion.getImageUrl
import fr.free.nrw.commons.explore.depictions.THUMB_IMAGE_SIZE
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.upload.WikidataItem
import fr.free.nrw.commons.wikidata.WikidataProperties
@ -9,8 +7,13 @@ import fr.free.nrw.commons.wikidata.WikidataProperties.*
import org.wikipedia.wikidata.DataValue
import org.wikipedia.wikidata.Entities
import org.wikipedia.wikidata.Statement_partial
import java.math.BigInteger
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.*
const val THUMB_IMAGE_SIZE = "70px"
/**
* Model class for Depicted Item in Upload and Explore
*/
@ -76,3 +79,40 @@ operator fun Entities.Entity.get(property: WikidataProperties) =
private fun Map<String, Entities.Label>.byLanguageOrFirstOrEmpty() =
let { it[Locale.getDefault().language] ?: it.values.firstOrNull() }?.value() ?: ""
private fun getImageUrl(title: String, size: String): String {
return title.substringAfter(":")
.replace(" ", "_")
.let {
val MD5Hash = getMd5(it)
"https://upload.wikimedia.org/wikipedia/commons/thumb/${MD5Hash[0]}/${MD5Hash[0]}${MD5Hash[1]}/$it/$size-$it"
}
}
/**
* Generates MD5 hash for the filename
*/
private fun getMd5(input: String): String {
return try {
// Static getInstance method is called with hashing MD5
val md = MessageDigest.getInstance("MD5")
// digest() method is called to calculate message digest
// of an input digest() return array of byte
val messageDigest = md.digest(input.toByteArray())
// Convert byte array into signum representation
val no = BigInteger(1, messageDigest)
// Convert message digest into hex value
var hashtext = no.toString(16)
while (hashtext.length < 32) {
hashtext = "0$hashtext"
}
hashtext
} // For specifying wrong message digest algorithms
catch (e: NoSuchAlgorithmException) {
throw RuntimeException(e)
}
}

View file

@ -1,29 +0,0 @@
<?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>

View file

@ -10,7 +10,7 @@
tools:targetApi="n_mr1">
<intent
android:action="android.intent.action.VIEW"
android:targetClass="fr.free.nrw.commons.explore.categories.ExploreActivity"
android:targetClass="fr.free.nrw.commons.explore.ExploreActivity"
android:targetPackage="fr.free.nrw.commons.beta" />
</shortcut>

View file

@ -1,70 +0,0 @@
package fr.free.nrw.commons.depictions
import fr.free.nrw.commons.depictions.subClass.SubDepictionListContract
import fr.free.nrw.commons.depictions.subClass.SubDepictionListPresenter
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.Observable
import io.reactivex.schedulers.TestScheduler
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
class SubDepictionListPresenterTest {
@Mock
internal lateinit var view: SubDepictionListContract.View
lateinit var subDepictionListPresenter: SubDepictionListPresenter
lateinit var testScheduler: TestScheduler
@Mock
internal lateinit var recentSearchesDao: RecentSearchesDao
@Mock
internal lateinit var depictsClient: DepictsClient
@Mock
internal lateinit var okHttpJsonApiClient: OkHttpJsonApiClient
var testObservable: Observable<List<DepictedItem>>? = null
@Mock
lateinit var depictedItem: DepictedItem
val depictedItems: ArrayList<DepictedItem> = ArrayList()
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
testScheduler = TestScheduler()
depictedItems.add(depictedItem)
testObservable = Observable.just(depictedItems)
subDepictionListPresenter = SubDepictionListPresenter(recentSearchesDao, depictsClient, okHttpJsonApiClient, testScheduler, testScheduler)
subDepictionListPresenter.onAttachView(view)
}
@Test
fun initSubDepictionListForParentClass() {
Mockito.`when`(okHttpJsonApiClient.getParentQIDs(ArgumentMatchers.anyString())).thenReturn(testObservable)
subDepictionListPresenter.initSubDepictionList("Q9394", true)
testScheduler.triggerActions()
verify(view)?.onSuccess(depictedItems)
}
@Test
fun initSubDepictionListForChildClass() {
Mockito.`when`(okHttpJsonApiClient.getChildQIDs(ArgumentMatchers.anyString())).thenReturn(testObservable)
subDepictionListPresenter.initSubDepictionList("Q9394", false)
testScheduler.triggerActions()
verify(view)?.onSuccess(depictedItems)
}
}

View file

@ -7,6 +7,7 @@ import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.explore.paging.*
import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.TestScheduler
import org.junit.Before

View file

@ -3,7 +3,10 @@ package fr.free.nrw.commons.explore
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.explore.depictions.LoadFunction
import fr.free.nrw.commons.explore.depictions.search.LoadFunction
import fr.free.nrw.commons.explore.paging.LiveDataConverter
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
import fr.free.nrw.commons.explore.paging.PagingDataSourceFactory
import org.junit.Before
import org.junit.Ignore
import org.junit.Test

View file

@ -4,6 +4,9 @@ import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.spy
import com.nhaarman.mockitokotlin2.verify
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.explore.paging.LoadingState
import fr.free.nrw.commons.explore.paging.PagingDataSource
import fr.free.nrw.commons.explore.paging.PagingDataSourceFactory
import io.reactivex.processors.PublishProcessor
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.instanceOf
@ -37,7 +40,7 @@ class PagingDataSourceFactoryTest {
fun `create returns a dataSource`() {
assertThat(
factory.create(),
instanceOf(SearchDataSource::class.java)
instanceOf(PagingDataSource::class.java)
)
}
@ -45,7 +48,7 @@ class PagingDataSourceFactoryTest {
@Ignore("Rewrite with Mockk constructor mocks")
fun `retryFailedRequest invokes method if not null`() {
val spyFactory = spy(factory)
val dataSource = mock<SearchDataSource<String>>()
val dataSource = mock<PagingDataSource<String>>()
Mockito.doReturn(dataSource).`when`(spyFactory).create()
factory.retryFailedRequest()
verify(dataSource).retryFailedRequest()

View file

@ -2,7 +2,9 @@ package fr.free.nrw.commons.explore
import androidx.paging.PositionalDataSource
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.explore.depictions.LoadingStates
import fr.free.nrw.commons.explore.depictions.search.LoadingStates
import fr.free.nrw.commons.explore.paging.LoadingState
import fr.free.nrw.commons.explore.paging.PagingDataSource
import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers
@ -12,10 +14,10 @@ import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
class SearchDataSourceTest {
class PagingDataSourceTest {
private lateinit var loadingStates: PublishProcessor<LoadingState>
private lateinit var searchDepictionsDataSource: TestSearchDataSource
private lateinit var searchDepictionsDataSource: TestPagingDataSource
@Mock
private lateinit var mockGetItems: MockGetItems
@ -26,7 +28,7 @@ class SearchDataSourceTest {
MockitoAnnotations.initMocks(this)
loadingStates = PublishProcessor.create()
searchDepictionsDataSource =
TestSearchDataSource(
TestPagingDataSource(
loadingStates,
mockGetItems
)
@ -109,8 +111,8 @@ class SearchDataSourceTest {
}
}
class TestSearchDataSource(loadingStates: LoadingStates, val mockGetItems: MockGetItems) :
SearchDataSource<String>(loadingStates) {
class TestPagingDataSource(loadingStates: LoadingStates, val mockGetItems: MockGetItems) :
PagingDataSource<String>(loadingStates) {
override fun getItems(loadSize: Int, startPosition: Int): List<String> =
mockGetItems.getItems(loadSize, startPosition)
}

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.explore.depictions
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.explore.depictions.search.PageableDepictionsDataSource
import io.reactivex.Single
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers
@ -12,10 +13,14 @@ class PageableDepictionsDataSourceTest {
@Test
fun `loadFunction loads depictions`() {
val depictsClient: DepictsClient = mock()
whenever(depictsClient.searchForDepictions("test", 0, 1)).thenReturn(Single.just(emptyList()))
whenever(depictsClient.searchForDepictions("test", 0, 1))
.thenReturn(Single.just(emptyList()))
val pageableDepictionsDataSource = PageableDepictionsDataSource(mock(), depictsClient)
pageableDepictionsDataSource.onQueryUpdated("test")
assertThat(pageableDepictionsDataSource.loadFunction.invoke(0, 1), Matchers.`is`(emptyList()))
assertThat(
pageableDepictionsDataSource.loadFunction.invoke(0, 1),
Matchers.`is`(emptyList())
)
}
}

View file

@ -1,53 +0,0 @@
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()
}
}

View file

@ -1,106 +0,0 @@
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)
}
}

View file

@ -1,80 +0,0 @@
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)
}
}

View file

@ -0,0 +1,44 @@
package fr.free.nrw.commons.explore.depictions.child
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever
import depictedItem
import fr.free.nrw.commons.explore.paging.LiveDataConverter
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import io.reactivex.Observable
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
class PageableChildDepictionsDataSourceTest {
@Mock
lateinit var okHttpJsonApiClient: OkHttpJsonApiClient
@Mock
lateinit var liveDataConverter: LiveDataConverter
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
}
@Test
fun `loadFunction loads from api at position 0`() {
val dataSource =
PageableChildDepictionsDataSource(liveDataConverter, okHttpJsonApiClient)
dataSource.onQueryUpdated("test")
whenever(okHttpJsonApiClient.getChildDepictions("test"))
.thenReturn(Observable.just(listOf(depictedItem())))
assertThat(dataSource.loadFunction(-1, 0), `is`(listOf(depictedItem())))
}
@Test
fun `loadFunction loads nothing at any other position`() {
val dataSource =
PageableChildDepictionsDataSource(liveDataConverter, okHttpJsonApiClient)
assertThat(dataSource.loadFunction(-1, 1), `is`(emptyList()))
verifyZeroInteractions(okHttpJsonApiClient)
}
}

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.depictions.Media
package fr.free.nrw.commons.explore.depictions.media
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever

View file

@ -0,0 +1,47 @@
package fr.free.nrw.commons.explore.depictions.parent
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever
import depictedItem
import fr.free.nrw.commons.explore.depictions.child.PageableChildDepictionsDataSource
import fr.free.nrw.commons.explore.paging.LiveDataConverter
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import io.reactivex.Observable
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
class PageableParentDepictionsDataSourceTest {
@Mock
lateinit var okHttpJsonApiClient: OkHttpJsonApiClient
@Mock
lateinit var liveDataConverter: LiveDataConverter
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
}
@Test
fun `loadFunction loads from api at position 0`() {
val dataSource =
PageableParentDepictionsDataSource(liveDataConverter, okHttpJsonApiClient)
dataSource.onQueryUpdated("test")
whenever(okHttpJsonApiClient.getParentDepictions("test"))
.thenReturn(Observable.just(listOf(depictedItem())))
assertThat(dataSource.loadFunction(-1, 0), `is`(listOf(depictedItem())))
}
@Test
fun `loadFunction loads nothing at any other position`() {
val dataSource =
PageableChildDepictionsDataSource(liveDataConverter, okHttpJsonApiClient)
assertThat(dataSource.loadFunction(-1, 1), `is`(emptyList()))
verifyZeroInteractions(okHttpJsonApiClient)
}
}