With lazy loading of contributions (#3566)

This commit is contained in:
Vivek Maskara 2020-05-28 04:54:41 -07:00 committed by GitHub
parent c216fdf0d4
commit d863a404f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1397 additions and 928 deletions

View file

@ -2,7 +2,6 @@ package fr.free.nrw.commons.contributions;
import android.os.Parcel;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.upload.UploadMediaDetail;
@ -13,7 +12,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.wikipedia.dataclient.mwapi.MwQueryLogEvent;
import java.util.Objects;
@Entity(tableName = "contribution")
public class Contribution extends Media {
@ -24,24 +23,21 @@ public class Contribution extends Media {
public static final int STATE_QUEUED = 2;
public static final int STATE_IN_PROGRESS = 3;
@PrimaryKey (autoGenerate = true)
private long _id;
private int state;
private long transferred;
private String decimalCoords;
private String dateCreatedSource;
private WikidataPlace wikidataPlace;
/**
* Each depiction loaded in depictions activity is associated with a wikidata entity id,
* this Id is in turn used to upload depictions to wikibase
* Each depiction loaded in depictions activity is associated with a wikidata entity id, this Id
* is in turn used to upload depictions to wikibase
*/
private List<DepictedItem> depictedItems = new ArrayList<>();
private String mimeType;
/**
* This hasmap stores the list of multilingual captions, where
* key of the HashMap is the language and value is the caption in the corresponding language
* Ex: key = "en", value: "<caption in short in English>"
* key = "de" , value: "<caption in german>"
* This hasmap stores the list of multilingual captions, where key of the HashMap is the language
* and value is the caption in the corresponding language Ex: key = "en", value: "<caption in
* short in English>" key = "de" , value: "<caption in german>"
*/
private Map<String, String> captions = new HashMap<>();
@ -55,20 +51,13 @@ public class Contribution extends Media {
UploadMediaDetail.formatList(item.getUploadMediaDetails()),
sessionManager.getAuthorName(),
categories);
captions = UploadMediaDetail.formatCaptions(item.getUploadMediaDetails());
captions = UploadMediaDetail.formatCaptions(item.getUploadMediaDetails());
decimalCoords = item.getGpsCoords().getDecimalCoords();
dateCreatedSource = "";
this.depictedItems = depictedItems;
wikidataPlace = WikidataPlace.from(item.getPlace());
}
public Contribution(final MwQueryLogEvent queryLogEvent, final String user) {
super(queryLogEvent.title(),queryLogEvent.date(), user);
decimalCoords = "";
dateCreatedSource = "";
state = STATE_COMPLETED;
}
public void setDateCreatedSource(final String dateCreatedSource) {
this.dateCreatedSource = dateCreatedSource;
}
@ -108,14 +97,6 @@ public class Contribution extends Media {
return wikidataPlace;
}
public long get_id() {
return _id;
}
public void set_id(final long _id) {
this._id = _id;
}
public String getDecimalCoords() {
return decimalCoords;
}
@ -128,29 +109,30 @@ public class Contribution extends Media {
this.depictedItems = depictedItems;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
public String getMimeType() {
return mimeType;
}
public String getMimeType() {
return mimeType;
public void setMimeType(final String mimeType) {
this.mimeType = mimeType;
}
/**
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
* This is a replacement of the previously used titles for images (titles were not multilingual)
* Also now captions replace the previous convention of using title for filename
*
* Captions are a feature part of Structured data. They are meant to store short, multilingual
* descriptions about files This is a replacement of the previously used titles for images (titles
* were not multilingual) Also now captions replace the previous convention of using title for
* filename
* <p>
* key of the HashMap is the language and value is the caption in the corresponding language
*
* <p>
* returns list of captions stored in hashmap
*/
public Map<String, String> getCaptions() {
return captions;
return captions;
}
public void setCaptions(Map<String, String> captions) {
this.captions = captions;
this.captions = captions;
}
@Override
@ -161,7 +143,6 @@ public class Contribution extends Media {
@Override
public void writeToParcel(final Parcel dest, final int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(_id);
dest.writeInt(state);
dest.writeLong(transferred);
dest.writeString(decimalCoords);
@ -169,9 +150,24 @@ public class Contribution extends Media {
dest.writeSerializable((HashMap) captions);
}
/**
* Constructor that takes Media object and state as parameters and builds a new Contribution object
* @param media
* @param state
*/
public Contribution(Media media, int state) {
super(media.getPageId(),
media.getLocalUri(), media.getThumbUrl(), media.getImageUrl(), media.getFilename(),
media.getDescription(),
media.getDiscussion(),
media.getDataLength(), media.getDateCreated(), media.getDateUploaded(),
media.getLicense(), media.getLicenseUrl(), media.getCreator(), media.getCategories(),
media.isRequestedDeletion(), media.getCoordinates());
this.state = state;
}
protected Contribution(final Parcel in) {
super(in);
_id = in.readLong();
state = in.readInt();
transferred = in.readLong();
decimalCoords = in.readString();
@ -190,4 +186,35 @@ public class Contribution extends Media {
return new Contribution[size];
}
};
/**
* Equals implementation of Contributions that compares all parameters for checking equality
*/
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Contribution)) {
return false;
}
final Contribution that = (Contribution) o;
return getState() == that.getState() && getTransferred() == that.getTransferred() && Objects
.equals(getDecimalCoords(), that.getDecimalCoords()) && Objects
.equals(getDateCreatedSource(), that.getDateCreatedSource()) && Objects
.equals(getWikidataPlace(), that.getWikidataPlace()) && Objects
.equals(getDepictedItems(), that.getDepictedItems()) && Objects
.equals(getMimeType(), that.getMimeType()) && Objects
.equals(getCaptions(), that.getCaptions());
}
/**
* Hash code implementation of contributions that considers all parameters for calculating hash.
*/
@Override
public int hashCode() {
return Objects
.hash(getState(), getTransferred(), getDecimalCoords(), getDateCreatedSource(),
getWikidataPlace(), getDepictedItems(), getMimeType(), getCaptions());
}
}

View file

@ -0,0 +1,89 @@
package fr.free.nrw.commons.contributions
import androidx.paging.PagedList.BoundaryCallback
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Scheduler
import io.reactivex.disposables.CompositeDisposable
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import javax.inject.Named
/**
* Class that extends PagedList.BoundaryCallback for contributions list It defines the action that
* is triggered for various boundary conditions in the list
*/
class ContributionBoundaryCallback @Inject constructor(
private val repository: ContributionsRepository,
private val sessionManager: SessionManager,
private val mediaClient: MediaClient,
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler
) : BoundaryCallback<Contribution>() {
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
/**
* It is triggered when the list has no items User's Contributions are then fetched from the
* network
*/
override fun onZeroItemsLoaded() {
fetchContributions()
}
/**
* It is triggered when the user scrolls to the top of the list User's Contributions are then
* fetched from the network
* */
override fun onItemAtFrontLoaded(itemAtFront: Contribution) {
fetchContributions()
}
/**
* It is triggered when the user scrolls to the end of the list. User's Contributions are then
* fetched from the network
*/
override fun onItemAtEndLoaded(itemAtEnd: Contribution) {
fetchContributions()
}
/**
* Fetches contributions using the MediaWiki API
*/
fun fetchContributions() {
if (mediaClient.doesMediaListForUserHaveMorePages(sessionManager.userName).not()) {
return
}
compositeDisposable.add(
mediaClient.getMediaListForUser(sessionManager.userName)
.map { mediaList: List<Media?> ->
mediaList.map {
Contribution(it, Contribution.STATE_COMPLETED)
}
}
.subscribeOn(ioThreadScheduler)
.subscribe(
::saveContributionsToDB
) { error: Throwable ->
Timber.e(
"Failed to fetch contributions: %s",
error.message
)
}
)
}
/**
* Saves the contributions the the local DB
*/
private fun saveContributionsToDB(contributions: List<Contribution>) {
compositeDisposable.add(
repository.save(contributions)
.subscribeOn(ioThreadScheduler)
.subscribe { longs: List<Long?>? ->
repository["last_fetch_timestamp"] = System.currentTimeMillis()
}
)
}
}

View file

@ -1,6 +1,6 @@
package fr.free.nrw.commons.contributions;
import androidx.lifecycle.LiveData;
import androidx.paging.DataSource;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
@ -15,40 +15,55 @@ import java.util.List;
@Dao
public abstract class ContributionDao {
@Query("SELECT * FROM contribution order by dateUploaded DESC")
abstract LiveData<List<Contribution>> fetchContributions();
@Query("SELECT * FROM contribution order by dateUploaded DESC")
abstract DataSource.Factory<Integer, Contribution> fetchContributions();
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract Single<Long> save(Contribution contribution);
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract void saveSynchronous(Contribution contribution);
public Completable deleteAllAndSave(List<Contribution> contributions){
return Completable.fromAction(() -> deleteAllAndSaveTransaction(contributions));
}
public Completable save(final Contribution contribution) {
return Completable
.fromAction(() -> saveSynchronous(contribution));
}
@Transaction
public void deleteAllAndSaveTransaction(List<Contribution> contributions){
deleteAll(Contribution.STATE_COMPLETED);
save(contributions);
}
@Transaction
public void deleteAndSaveContribution(final Contribution oldContribution,
final Contribution newContribution) {
deleteSynchronous(oldContribution);
saveSynchronous(newContribution);
}
@Insert
public abstract void save(List<Contribution> contribution);
public Completable saveAndDelete(final Contribution oldContribution,
final Contribution newContribution) {
return Completable
.fromAction(() -> deleteAndSaveContribution(oldContribution, newContribution));
}
@Delete
public abstract Single<Integer> delete(Contribution contribution);
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract Single<List<Long>> save(List<Contribution> contribution);
@Query("SELECT * from contribution WHERE filename=:fileName")
public abstract List<Contribution> getContributionWithTitle(String fileName);
@Delete
public abstract void deleteSynchronous(Contribution contribution);
@Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)")
public abstract Single<Integer> updateStates(int state, int[] toUpdateStates);
public Completable delete(final Contribution contribution) {
return Completable
.fromAction(() -> deleteSynchronous(contribution));
}
@Query("Delete FROM contribution")
public abstract void deleteAll();
@Query("SELECT * from contribution WHERE filename=:fileName")
public abstract List<Contribution> getContributionWithTitle(String fileName);
@Query("Delete FROM contribution WHERE state = :state")
public abstract void deleteAll(int state);
@Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)")
public abstract Single<Integer> updateStates(int state, int[] toUpdateStates);
@Update
public abstract Single<Integer> update(Contribution contribution);
@Query("Delete FROM contribution")
public abstract void deleteAll();
@Update
public abstract void updateSynchronous(Contribution contribution);
public Completable update(final Contribution contribution) {
return Completable
.fromAction(() -> updateSynchronous(contribution));
}
}

View file

@ -22,7 +22,6 @@ import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.Random;
import timber.log.Timber;
public class ContributionViewHolder extends RecyclerView.ViewHolder {
@ -39,23 +38,22 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
private int position;
private Contribution contribution;
private Random random = new Random();
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
private final MediaClient mediaClient;
ContributionViewHolder(View parent, Callback callback,
MediaClient mediaClient) {
ContributionViewHolder(final View parent, final Callback callback,
final MediaClient mediaClient) {
super(parent);
this.mediaClient = mediaClient;
ButterKnife.bind(this, parent);
this.callback=callback;
}
public void init(int position, Contribution contribution) {
public void init(final int position, final Contribution contribution) {
this.contribution = contribution;
fetchAndDisplayCaption(contribution);
this.position = position;
String imageSource = chooseImageSource(contribution.getThumbUrl(), contribution.getLocalUri());
final String imageSource = chooseImageSource(contribution.getThumbUrl(), contribution.getLocalUri());
if (!TextUtils.isEmpty(imageSource)) {
final ImageRequest imageRequest =
ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
@ -84,8 +82,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
stateView.setVisibility(View.GONE);
progressView.setVisibility(View.VISIBLE);
failedImageOptions.setVisibility(View.GONE);
long total = contribution.getDataLength();
long transferred = contribution.getTransferred();
final long total = contribution.getDataLength();
final long transferred = contribution.getTransferred();
if (transferred == 0 || transferred >= total) {
progressView.setIndeterminate(true);
} else {
@ -107,14 +105,14 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
*
* @param contribution
*/
private void fetchAndDisplayCaption(Contribution contribution) {
private void fetchAndDisplayCaption(final Contribution contribution) {
if ((contribution.getState() != Contribution.STATE_COMPLETED)) {
titleView.setText(contribution.getDisplayTitle());
} else {
final String pageId = contribution.getPageId();
if (pageId != null) {
Timber.d("Fetching caption for %s", contribution.getFilename());
String wikibaseMediaId = PAGE_ID_PREFIX
final String wikibaseMediaId = PAGE_ID_PREFIX
+ pageId; // Create Wikibase media id from the page id. Example media id: M80618155 for https://commons.wikimedia.org/wiki/File:Tantanmen.jpeg with has the pageid 80618155
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseMediaId)
.subscribeOn(Schedulers.io())
@ -141,7 +139,7 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
* @return
*/
@Nullable
private String chooseImageSource(String thumbUrl, Uri localUri) {
private String chooseImageSource(final String thumbUrl, final Uri localUri) {
return !TextUtils.isEmpty(thumbUrl) ? thumbUrl :
localUri != null ? localUri.toString() :
null;

View file

@ -12,16 +12,6 @@ public class ContributionsContract {
public interface View {
void showWelcomeTip(boolean numberOfUploads);
void showProgress(boolean shouldShow);
void showNoContributionsUI(boolean shouldShow);
void setUploadCount(int count);
void showContributions(List<Contribution> contributionList);
void showMessage(String localizedMessage);
}
@ -31,8 +21,6 @@ public class ContributionsContract {
void deleteUpload(Contribution contribution);
Media getItemAtPosition(int i);
void updateContribution(Contribution contribution);
void fetchMediaDetails(Contribution contribution);

View file

@ -19,7 +19,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
import androidx.fragment.app.FragmentTransaction;
import butterknife.BindView;
@ -30,8 +29,7 @@ import fr.free.nrw.commons.campaigns.Campaign;
import fr.free.nrw.commons.campaigns.CampaignView;
import fr.free.nrw.commons.campaigns.CampaignsPresenter;
import fr.free.nrw.commons.campaigns.ICampaignsView;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.contributions.ContributionsListFragment.SourceRefresher;
import fr.free.nrw.commons.contributions.ContributionsListFragment.Callback;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
@ -54,7 +52,6 @@ import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
@ -62,11 +59,10 @@ import timber.log.Timber;
public class ContributionsFragment
extends CommonsDaggerSupportFragment
implements
MediaDetailProvider,
OnBackStackChangedListener,
SourceRefresher,
LocationUpdateListener,
ICampaignsView, ContributionsContract.View {
MediaDetailProvider,
ICampaignsView, ContributionsContract.View, Callback {
@Inject @Named("default_preferences") JsonKvStore store;
@Inject NearbyController nearbyController;
@Inject OkHttpJsonApiClient okHttpJsonApiClient;
@ -78,8 +74,8 @@ public class ContributionsFragment
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private ContributionsListFragment contributionsListFragment;
private MediaDetailPagerFragment mediaDetailPagerFragment;
private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
private MediaDetailPagerFragment mediaDetailPagerFragment;
static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
@BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView;
@ -113,7 +109,6 @@ public class ContributionsFragment
}
};
private boolean shouldShowMediaDetailsFragment;
private int numberOfContributions;
private boolean isAuthCookieAcquired;
@Override
@ -128,7 +123,6 @@ public class ContributionsFragment
ButterKnife.bind(this, view);
presenter.onAttachView(this);
contributionsPresenter.onAttachView(this);
contributionsPresenter.setLifeCycleOwner(this.getViewLifecycleOwner());
campaignView.setVisibility(View.GONE);
checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
@ -141,103 +135,21 @@ public class ContributionsFragment
if (savedInstanceState != null) {
mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager()
.findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
.findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
contributionsListFragment = (ContributionsListFragment) getChildFragmentManager()
.findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG);
.findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG);
shouldShowMediaDetailsFragment = savedInstanceState.getBoolean("mediaDetailsVisible");
}
initFragments();
if(shouldShowMediaDetailsFragment){
showMediaDetailPagerFragment();
}else{
showContributionsListFragment();
}
if (!ConfigUtils.isBetaFlavour()) {
setUploadCount();
}
getChildFragmentManager().registerFragmentLifecycleCallbacks(
new FragmentManager.FragmentLifecycleCallbacks() {
@Override public void onFragmentResumed(FragmentManager fm, Fragment f) {
super.onFragmentResumed(fm, f);
//If media detail pager fragment is visible, hide the campaigns view [might not be the best way to do, this but yeah, this proves to work for now]
Timber.e("onFragmentResumed %s", f.getClass().getName());
if (f instanceof MediaDetailPagerFragment) {
campaignView.setVisibility(View.GONE);
}
}
@Override public void onFragmentDetached(FragmentManager fm, Fragment f) {
super.onFragmentDetached(fm, f);
Timber.e("onFragmentDetached %s", f.getClass().getName());
//If media detail pager fragment is detached, ContributionsList fragment is gonna be visible, [becomes tightly coupled though]
if (f instanceof MediaDetailPagerFragment) {
fetchCampaigns();
}
}
}, true);
return view;
}
/**
* Initialose the ContributionsListFragment and MediaDetailPagerFragment fragment
*/
private void initFragments() {
if (null == contributionsListFragment) {
contributionsListFragment = new ContributionsListFragment();
}
contributionsListFragment.setCallback(new Callback() {
@Override
public void retryUpload(Contribution contribution) {
ContributionsFragment.this.retryUpload(contribution);
}
@Override
public void deleteUpload(Contribution contribution) {
contributionsPresenter.deleteUpload(contribution);
}
@Override
public void openMediaDetail(int position) {
showDetail(position);
}
@Override
public Contribution getContributionForPosition(int position) {
return (Contribution) contributionsPresenter.getItemAtPosition(position);
}
@Override
public void fetchMediaUriFor(Contribution contribution) {
Timber.d("Fetching thumbnail for %s", contribution.getFilename());
contributionsPresenter.fetchMediaDetails(contribution);
}
});
if(null==mediaDetailPagerFragment){
mediaDetailPagerFragment=new MediaDetailPagerFragment();
}
}
/**
* Replaces the root frame layout with the given fragment
* @param fragment
* @param tag
*/
private void showFragment(Fragment fragment, String tag) {
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.replace(R.id.root_frame, fragment, tag);
transaction.addToBackStack(CONTRIBUTION_LIST_FRAGMENT_TAG);
transaction.commit();
getChildFragmentManager().executePendingTransactions();
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
@ -265,7 +177,7 @@ public class ContributionsFragment
if (nearbyNotificationCardView != null) {
if (store.getBoolean("displayNearbyCardView", true)) {
if (nearbyNotificationCardView.cardViewVisibilityState
== NearbyNotificationCardView.CardViewVisibilityState.READY) {
== NearbyNotificationCardView.CardViewVisibilityState.READY) {
nearbyNotificationCardView.setVisibility(View.VISIBLE);
}
} else {
@ -275,20 +187,22 @@ public class ContributionsFragment
showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG);
}
/**
* Replace FrameLayout with MediaDetailPagerFragment, user will see details of selected media.
* Creates new one if null.
*/
private void showMediaDetailPagerFragment() {
// hide tabs on media detail view is visible
((MainActivity)getActivity()).hideTabs();
((MainActivity) getActivity()).hideTabs();
// hide nearby card view on media detail is visible
nearbyNotificationCardView.setVisibility(View.GONE);
showFragment(mediaDetailPagerFragment,MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
showFragment(mediaDetailPagerFragment, MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
}
private void setupViewForMediaDetails() {
campaignView.setVisibility(View.GONE);
nearbyNotificationCardView.setVisibility(View.GONE);
((MainActivity)getActivity()).hideTabs();
}
@Override
public void onBackStackChanged() {
((MainActivity)getActivity()).initBackButton();
@ -307,43 +221,42 @@ public class ContributionsFragment
}
private void initFragments() {
if (null == contributionsListFragment) {
contributionsListFragment = new ContributionsListFragment(this);
}
if (shouldShowMediaDetailsFragment) {
showMediaDetailPagerFragment();
} else {
showContributionsListFragment();
}
showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG);
}
/**
* Replaces the root frame layout with the given fragment
*
* @param fragment
* @param tag
*/
private void showFragment(Fragment fragment, String tag) {
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.replace(R.id.root_frame, fragment, tag);
transaction.addToBackStack(CONTRIBUTION_LIST_FRAGMENT_TAG);
transaction.commit();
getChildFragmentManager().executePendingTransactions();
}
public Intent getUploadServiceIntent(){
Intent intent = new Intent(getActivity(), UploadService.class);
intent.setAction(UploadService.ACTION_START_SERVICE);
return intent;
}
/**
* Replace whatever is in the current contributionsFragmentContainer view with
* mediaDetailPagerFragment, and preserve previous state in back stack.
* Called when user selects a contribution.
*/
private void showDetail(int i) {
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
mediaDetailPagerFragment = new MediaDetailPagerFragment();
showMediaDetailPagerFragment();
}
mediaDetailPagerFragment.showImage(i);
}
@Override
public void refreshSource() {
contributionsPresenter.fetchContributions();
}
@Override
public Media getMediaAtPosition(int i) {
return contributionsPresenter.getItemAtPosition(i);
}
@Override
public int getTotalMediaCount() {
return numberOfContributions;
}
@SuppressWarnings("ConstantConditions")
private void setUploadCount() {
compositeDisposable.add(okHttpJsonApiClient
.getUploadCount(((MainActivity)getActivity()).sessionManager.getCurrentAccount().name)
.subscribeOn(Schedulers.io())
@ -373,8 +286,6 @@ public class ContributionsFragment
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
boolean mediaDetailsVisible = mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible();
outState.putBoolean("mediaDetailsVisible", mediaDetailsVisible);
}
@Override
@ -384,13 +295,6 @@ public class ContributionsFragment
firstLocationUpdate = true;
locationManager.addLocationListener(this);
boolean isSettingsChanged = store.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
store.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
if (isSettingsChanged) {
refreshSource();
}
if (store.getBoolean("displayNearbyCardView", true)) {
checkPermissionsAndShowNearbyCardView();
if (nearbyNotificationCardView.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) {
@ -403,10 +307,6 @@ public class ContributionsFragment
}
fetchCampaigns();
if(isAuthCookieAcquired){
contributionsPresenter.fetchContributions();
}
}
private void checkPermissionsAndShowNearbyCardView() {
@ -463,17 +363,11 @@ public class ContributionsFragment
}
private void updateNearbyNotification(@Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null && nearbyPlacesInfo.placeList.size() > 0) {
Place closestNearbyPlace = nearbyPlacesInfo.placeList.get(0);
String distance = formatDistanceBetween(curLatLng, closestNearbyPlace.location);
closestNearbyPlace.setDistance(distance);
nearbyNotificationCardView.updateContent(closestNearbyPlace);
if (mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible()) {
nearbyNotificationCardView.setVisibility(View.GONE);
}else {
nearbyNotificationCardView.setVisibility(View.VISIBLE);
}
} else {
// Means that no close nearby place is found
nearbyNotificationCardView.setVisibility(View.GONE);
@ -553,37 +447,13 @@ public class ContributionsFragment
presenter.onDetachView();
}
@Override
public void showWelcomeTip(boolean shouldShow) {
contributionsListFragment.showWelcomeTip(shouldShow);
}
@Override
public void showProgress(boolean shouldShow) {
contributionsListFragment.showProgress(shouldShow);
}
@Override
public void showNoContributionsUI(boolean shouldShow) {
contributionsListFragment.showNoContributionsUI(shouldShow);
}
@Override
public void setUploadCount(int count) {
this.numberOfContributions=count;
}
@Override
public void showContributions(List<Contribution> contributionList) {
contributionsListFragment.setContributions(contributionList);
}
/**
* Retry upload when it is failed
*
* @param contribution contribution to be retried
*/
private void retryUpload(Contribution contribution) {
@Override
public void retryUpload(Contribution contribution) {
if (NetworkUtils.isInternetConnectionEstablished(getContext())) {
if (contribution.getState() == STATE_FAILED && null != uploadService) {
uploadService.queue(contribution);
@ -596,5 +466,29 @@ public class ContributionsFragment
}
}
/**
* Replace whatever is in the current contributionsFragmentContainer view with
* mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects a
* contribution.
*/
@Override
public void showDetail(int position) {
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
mediaDetailPagerFragment = new MediaDetailPagerFragment();
showMediaDetailPagerFragment();
}
mediaDetailPagerFragment.showImage(position);
}
@Override
public Media getMediaAtPosition(int i) {
return contributionsListFragment.getMediaAtPosition(i);
}
@Override
public int getTotalMediaCount() {
return contributionsListFragment.getTotalMediaCount();
}
}

View file

@ -1,68 +1,71 @@
package fr.free.nrw.commons.contributions;
package fr.free.nrw.commons.contributions;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.DiffUtil;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.media.MediaClient;
import java.util.ArrayList;
import java.util.List;
/**
* Represents The View Adapter for the List of Contributions
* Represents The View Adapter for the List of Contributions
*/
public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionViewHolder> {
public class ContributionsListAdapter extends
PagedListAdapter<Contribution, ContributionViewHolder> {
private Callback callback;
private final Callback callback;
private final MediaClient mediaClient;
private List<Contribution> contributions;
public ContributionsListAdapter(Callback callback,
MediaClient mediaClient) {
ContributionsListAdapter(final Callback callback,
final MediaClient mediaClient) {
super(DIFF_CALLBACK);
this.callback = callback;
this.mediaClient = mediaClient;
contributions = new ArrayList<>();
}
/**
* Creates the new View Holder which will be used to display items(contributions)
* using the onBindViewHolder(viewHolder,position)
* Uses DiffUtil to calculate the changes in the list
* It has methods that check ID and the content of the items to determine if its a new item
*/
@NonNull
@Override
public ContributionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ContributionViewHolder viewHolder = new ContributionViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.layout_contribution, parent, false), callback, mediaClient);
return viewHolder;
}
private static final DiffUtil.ItemCallback<Contribution> DIFF_CALLBACK =
new DiffUtil.ItemCallback<Contribution>() {
@Override
public boolean areItemsTheSame(final Contribution oldContribution, final Contribution newContribution) {
return oldContribution.getPageId().equals(newContribution.getPageId());
}
@Override
public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) {
final Contribution contribution = contributions.get(position);
if (TextUtils.isEmpty(contribution.getThumbUrl())
&& contribution.getState() == Contribution.STATE_COMPLETED) {
callback.fetchMediaUriFor(contribution);
}
@Override
public boolean areContentsTheSame(final Contribution oldContribution, final Contribution newContribution) {
return oldContribution.equals(newContribution);
}
};
/**
* Initializes the view holder with contribution data
*/
@Override
public void onBindViewHolder(@NonNull final ContributionViewHolder holder, final int position) {
final Contribution contribution = getItem(position);
holder.init(position, contribution);
}
@Override
public int getItemCount() {
return contributions.size();
}
public void setContributions(@NonNull List<Contribution> contributionList) {
contributions = contributionList;
notifyDataSetChanged();
Contribution getContributionForPosition(final int position) {
return getItem(position);
}
/**
* Creates the new View Holder which will be used to display items(contributions) using the
* onBindViewHolder(viewHolder,position)
*/
@NonNull
@Override
public long getItemId(int position) {
return contributions.get(position).get_id();
public ContributionViewHolder onCreateViewHolder(@NonNull final ViewGroup parent,
final int viewType) {
final ContributionViewHolder viewHolder = new ContributionViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.layout_contribution, parent, false), callback, mediaClient);
return viewHolder;
}
public interface Callback {
@ -72,9 +75,5 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
void deleteUpload(Contribution contribution);
void openMediaDetail(int contribution);
Contribution getContributionForPosition(int position);
void fetchMediaUriFor(Contribution contribution);
}
}

View file

@ -0,0 +1,24 @@
package fr.free.nrw.commons.contributions;
import fr.free.nrw.commons.BasePresenter;
import java.util.List;
/**
* The contract for Contributions list View & Presenter
*/
public class ContributionsListContract {
public interface View {
void showWelcomeTip(boolean numberOfUploads);
void showProgress(boolean shouldShow);
void showNoContributionsUI(boolean shouldShow);
}
public interface UserActionListener extends BasePresenter<View> {
void deleteUpload(Contribution contribution);
}
}

View file

@ -5,6 +5,7 @@ import static android.view.View.VISIBLE;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -16,218 +17,217 @@ 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 androidx.recyclerview.widget.RecyclerView.LayoutManager;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaClient;
import java.util.ArrayList;
import java.util.List;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import javax.inject.Inject;
import javax.inject.Named;
/**
* Created by root on 01.06.2018.
*/
public class ContributionsListFragment extends CommonsDaggerSupportFragment {
public class ContributionsListFragment extends CommonsDaggerSupportFragment implements
ContributionsListContract.View, ContributionsListAdapter.Callback {
private static final String VISIBLE_ITEM_ID = "visible_item_id";
@BindView(R.id.contributionsList)
RecyclerView rvContributionsList;
@BindView(R.id.loadingContributionsProgressBar)
ProgressBar progressBar;
@BindView(R.id.fab_plus)
FloatingActionButton fabPlus;
@BindView(R.id.fab_camera)
FloatingActionButton fabCamera;
@BindView(R.id.fab_gallery)
FloatingActionButton fabGallery;
@BindView(R.id.noContributionsYet)
TextView noContributionsYet;
@BindView(R.id.fab_layout)
LinearLayout fab_layout;
private static final String RV_STATE = "rv_scroll_state";
@Inject @Named("default_preferences") JsonKvStore kvStore;
@Inject ContributionController controller;
@Inject MediaClient mediaClient;
@BindView(R.id.contributionsList)
RecyclerView rvContributionsList;
@BindView(R.id.loadingContributionsProgressBar)
ProgressBar progressBar;
@BindView(R.id.fab_plus)
FloatingActionButton fabPlus;
@BindView(R.id.fab_camera)
FloatingActionButton fabCamera;
@BindView(R.id.fab_gallery)
FloatingActionButton fabGallery;
@BindView(R.id.noContributionsYet)
TextView noContributionsYet;
@BindView(R.id.fab_layout)
LinearLayout fab_layout;
private Animation fab_close;
private Animation fab_open;
private Animation rotate_forward;
private Animation rotate_backward;
@Inject
ContributionController controller;
@Inject
MediaClient mediaClient;
@Inject
ContributionsListPresenter contributionsListPresenter;
private Animation fab_close;
private Animation fab_open;
private Animation rotate_forward;
private Animation rotate_backward;
private boolean isFabOpen = false;
private boolean isFabOpen;
private ContributionsListAdapter adapter;
private ContributionsListAdapter adapter;
private Callback callback;
private String lastVisibleItemID;
private final Callback callback;
private int SPAN_COUNT=3;
private List<Contribution> contributions=new ArrayList<>();
private final int SPAN_COUNT_LANDSCAPE = 3;
private final int SPAN_COUNT_PORTRAIT = 1;
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
ButterKnife.bind(this, view);
initAdapter();
return view;
ContributionsListFragment(final Callback callback) {
this.callback = callback;
}
public View onCreateView(
final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
ButterKnife.bind(this, view);
contributionsListPresenter.onAttachView(this);
initAdapter();
return view;
}
private void initAdapter() {
adapter = new ContributionsListAdapter(this, mediaClient);
}
@Override
public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initRecyclerView();
initializeAnimations();
setListeners();
}
private void initRecyclerView() {
final GridLayoutManager layoutManager = new GridLayoutManager(getContext(),
getSpanCount(getResources().getConfiguration().orientation));
rvContributionsList.setLayoutManager(layoutManager);
contributionsListPresenter.setup();
contributionsListPresenter.contributionList.observe(this, adapter::submitList);
rvContributionsList.setAdapter(adapter);
}
private int getSpanCount(final int orientation) {
return orientation == Configuration.ORIENTATION_LANDSCAPE ?
SPAN_COUNT_LANDSCAPE : SPAN_COUNT_PORTRAIT;
}
@Override
public void onConfigurationChanged(final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// check orientation
fab_layout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
rvContributionsList
.setLayoutManager(new GridLayoutManager(getContext(), getSpanCount(newConfig.orientation)));
}
private void initializeAnimations() {
fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open);
fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close);
rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward);
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward);
}
private void setListeners() {
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
fabCamera.setOnClickListener(view -> {
controller.initiateCameraPick(getActivity());
animateFAB(isFabOpen);
});
fabGallery.setOnClickListener(view -> {
controller.initiateGalleryPick(getActivity(), true);
animateFAB(isFabOpen);
});
}
private void animateFAB(final boolean isFabOpen) {
this.isFabOpen = !isFabOpen;
if (fabPlus.isShown()) {
if (isFabOpen) {
fabPlus.startAnimation(rotate_backward);
fabCamera.startAnimation(fab_close);
fabGallery.startAnimation(fab_close);
fabCamera.hide();
fabGallery.hide();
} else {
fabPlus.startAnimation(rotate_forward);
fabCamera.startAnimation(fab_open);
fabGallery.startAnimation(fab_open);
fabCamera.show();
fabGallery.show();
}
this.isFabOpen = !isFabOpen;
}
}
public void setCallback(Callback callback) {
this.callback = callback;
/**
* Shows welcome message if user has no contributions yet i.e. new user.
*/
public void showWelcomeTip(final boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
/**
* Responsible to set progress bar invisible and visible
*
* @param shouldShow True when contributions list should be hidden.
*/
public void showProgress(final boolean shouldShow) {
progressBar.setVisibility(shouldShow ? VISIBLE : GONE);
}
public void showNoContributionsUI(final boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
final GridLayoutManager layoutManager = (GridLayoutManager) rvContributionsList.getLayoutManager();
outState.putParcelable(RV_STATE, layoutManager.onSaveInstanceState());
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (null != savedInstanceState) {
final Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(RV_STATE);
rvContributionsList.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState);
}
}
private void initAdapter() {
adapter = new ContributionsListAdapter(callback, mediaClient);
adapter.setHasStableIds(true);
}
@Override
public void retryUpload(final Contribution contribution) {
callback.retryUpload(contribution);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initRecyclerView();
initializeAnimations();
setListeners();
}
@Override
public void deleteUpload(final Contribution contribution) {
contributionsListPresenter.deleteUpload(contribution);
}
private void initRecyclerView() {
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
rvContributionsList.setLayoutManager(new GridLayoutManager(getContext(),SPAN_COUNT));
} else {
rvContributionsList.setLayoutManager(new LinearLayoutManager(getContext()));
}
@Override
public void openMediaDetail(final int position) {
callback.showDetail(position);
}
rvContributionsList.setAdapter(adapter);
adapter.setContributions(contributions);
}
public Media getMediaAtPosition(final int i) {
return adapter.getContributionForPosition(i);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// check orientation
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
fab_layout.setOrientation(LinearLayout.HORIZONTAL);
rvContributionsList.setLayoutManager(new GridLayoutManager(getContext(),SPAN_COUNT));
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
fab_layout.setOrientation(LinearLayout.VERTICAL);
rvContributionsList.setLayoutManager(new LinearLayoutManager(getContext()));
}
}
public int getTotalMediaCount() {
return adapter.getItemCount();
}
private void initializeAnimations() {
fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open);
fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close);
rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward);
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward);
}
public interface Callback {
private void setListeners() {
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
fabCamera.setOnClickListener(view -> {
controller.initiateCameraPick(getActivity());
animateFAB(isFabOpen);
});
fabGallery.setOnClickListener(view -> {
controller.initiateGalleryPick(getActivity(), true);
animateFAB(isFabOpen);
});
}
private void animateFAB(boolean isFabOpen) {
this.isFabOpen = !isFabOpen;
if (fabPlus.isShown()){
if (isFabOpen) {
fabPlus.startAnimation(rotate_backward);
fabCamera.startAnimation(fab_close);
fabGallery.startAnimation(fab_close);
fabCamera.hide();
fabGallery.hide();
} else {
fabPlus.startAnimation(rotate_forward);
fabCamera.startAnimation(fab_open);
fabGallery.startAnimation(fab_open);
fabCamera.show();
fabGallery.show();
}
this.isFabOpen=!isFabOpen;
}
}
/**
* Shows welcome message if user has no contributions yet i.e. new user.
*/
public void showWelcomeTip(boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
/**
* Responsible to set progress bar invisible and visible
*
* @param shouldShow True when contributions list should be hidden.
*/
public void showProgress(boolean shouldShow) {
progressBar.setVisibility(shouldShow ? VISIBLE : GONE);
}
public void showNoContributionsUI(boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
public void setContributions(List<Contribution> contributionList) {
this.contributions.clear();
this.contributions.addAll(contributionList);
adapter.setContributions(contributions);
}
public interface SourceRefresher {
void refreshSource();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
LayoutManager layoutManager = rvContributionsList.getLayoutManager();
int lastVisibleItemPosition=0;
if(layoutManager instanceof LinearLayoutManager){
lastVisibleItemPosition= ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition();
}else if(layoutManager instanceof GridLayoutManager){
lastVisibleItemPosition=((GridLayoutManager)layoutManager).findLastCompletelyVisibleItemPosition();
}
String idOfItemWithPosition = findIdOfItemWithPosition(lastVisibleItemPosition);
if (null != idOfItemWithPosition) {
outState.putString(VISIBLE_ITEM_ID, idOfItemWithPosition);
}
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if(null!=savedInstanceState){
lastVisibleItemID =savedInstanceState.getString(VISIBLE_ITEM_ID, null);
}
}
/**
* Gets the id of the contribution from the db
* @param position
* @return
*/
@Nullable
private String findIdOfItemWithPosition(int position) {
Contribution contributionForPosition = callback.getContributionForPosition(position);
if (null != contributionForPosition) {
return contributionForPosition.getFilename();
}
return null;
}
void retryUpload(Contribution contribution);
void showDetail(int position);
}
}

View file

@ -0,0 +1,71 @@
package fr.free.nrw.commons.contributions;
import androidx.lifecycle.LiveData;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import fr.free.nrw.commons.contributions.ContributionsListContract.UserActionListener;
import fr.free.nrw.commons.di.CommonsApplicationModule;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import javax.inject.Inject;
import javax.inject.Named;
/**
* The presenter class for Contributions
*/
public class ContributionsListPresenter implements UserActionListener {
private final ContributionBoundaryCallback contributionBoundaryCallback;
private final ContributionsRepository repository;
private final Scheduler ioThreadScheduler;
private final CompositeDisposable compositeDisposable;
LiveData<PagedList<Contribution>> contributionList;
@Inject
ContributionsListPresenter(
final ContributionBoundaryCallback contributionBoundaryCallback,
final ContributionsRepository repository,
@Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) {
this.contributionBoundaryCallback = contributionBoundaryCallback;
this.repository = repository;
this.ioThreadScheduler = ioThreadScheduler;
compositeDisposable = new CompositeDisposable();
}
@Override
public void onAttachView(final ContributionsListContract.View view) {
}
/**
* Setup the paged list. This method sets the configuration for paged list and ties it up with the
* live data object. This method can be tweaked to update the lazy loading behavior of the
* contributions list
*/
void setup() {
final PagedList.Config pagedListConfig =
(new PagedList.Config.Builder())
.setPrefetchDistance(50)
.setPageSize(10).build();
contributionList = (new LivePagedListBuilder(repository.fetchContributions(), pagedListConfig)
.setBoundaryCallback(contributionBoundaryCallback)).build();
}
@Override
public void onDetachView() {
compositeDisposable.clear();
}
/**
* Delete a failed contribution from the local db
*/
@Override
public void deleteUpload(final Contribution contribution) {
compositeDisposable.add(repository
.deleteContributionFromDB(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
}

View file

@ -1,14 +1,13 @@
package fr.free.nrw.commons.contributions;
import androidx.lifecycle.LiveData;
import androidx.paging.DataSource.Factory;
import io.reactivex.Completable;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import io.reactivex.Completable;
import io.reactivex.Single;
/**
@ -59,23 +58,23 @@ class ContributionsLocalDataSource {
* @param contribution
* @return
*/
public Single<Integer> deleteContribution(Contribution contribution) {
public Completable deleteContribution(Contribution contribution) {
return contributionDao.delete(contribution);
}
public LiveData<List<Contribution>> getContributions() {
public Factory<Integer, Contribution> getContributions() {
return contributionDao.fetchContributions();
}
public Completable saveContributions(List<Contribution> contributions) {
return contributionDao.deleteAllAndSave(contributions);
public Single<List<Long>> saveContributions(List<Contribution> contributions) {
return contributionDao.save(contributions);
}
public void set(String key, long value) {
defaultKVStore.putLong(key,value);
}
public Single<Integer> updateContribution(Contribution contribution) {
public Completable updateContribution(Contribution contribution) {
return contributionDao.update(contribution);
}
}

View file

@ -9,8 +9,8 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
import fr.free.nrw.commons.db.AppDatabase;
import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.mwapi.UserClient;
import fr.free.nrw.commons.utils.NetworkUtils;
@ -25,6 +25,9 @@ import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
import javax.inject.Inject;
import javax.inject.Named;
/**
* The presenter class for Contributions
*/
@ -35,25 +38,10 @@ public class ContributionsPresenter implements UserActionListener {
private final Scheduler ioThreadScheduler;
private CompositeDisposable compositeDisposable;
private ContributionsContract.View view;
private List<Contribution> contributionList=new ArrayList<>();
@Inject
Context context;
@Inject
UserClient userClient;
@Inject
AppDatabase appDatabase;
@Inject
SessionManager sessionManager;
@Inject
MediaDataExtractor mediaDataExtractor;
private LifecycleOwner lifeCycleOwner;
@Inject
ContributionsPresenter(ContributionsRepository repository, @Named(CommonsApplicationModule.MAIN_THREAD) Scheduler mainThreadScheduler,@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) {
this.repository = repository;
@ -61,74 +49,12 @@ public class ContributionsPresenter implements UserActionListener {
this.ioThreadScheduler=ioThreadScheduler;
}
private String user;
@Override
public void onAttachView(ContributionsContract.View view) {
this.view = view;
compositeDisposable=new CompositeDisposable();
}
public void setLifeCycleOwner(LifecycleOwner lifeCycleOwner){
this.lifeCycleOwner=lifeCycleOwner;
}
public void fetchContributions() {
Timber.d("fetch Contributions");
LiveData<List<Contribution>> liveDataContributions = repository.fetchContributions();
if(null!=lifeCycleOwner) {
liveDataContributions.observe(lifeCycleOwner, this::showContributions);
}
if (NetworkUtils.isInternetConnectionEstablished(CommonsApplication.getInstance()) && shouldFetchContributions()) {
Timber.d("fetching contributions: ");
view.showProgress(true);
this.user = sessionManager.getUserName();
view.showContributions(Collections.emptyList());
compositeDisposable.add(userClient.logEvents(user)
.subscribeOn(ioThreadScheduler)
.observeOn(mainThreadScheduler)
.doOnNext(mwQueryLogEvent -> Timber.d("Received image %s", mwQueryLogEvent.title()))
.filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted()).doOnNext(mwQueryLogEvent -> Timber.d("Image %s passed filters", mwQueryLogEvent.title()))
.map(image -> new Contribution(image, user))
.toList()
.subscribe(this::saveContributionsToDB, error -> {
Timber.e("Failed to fetch contributions: %s", error.getMessage());
}));
}
}
private void showContributions(@NonNull List<Contribution> contributions) {
view.showProgress(false);
if (contributions.isEmpty()) {
view.showWelcomeTip(true);
view.showNoContributionsUI(true);
} else {
view.showWelcomeTip(false);
view.showNoContributionsUI(false);
view.setUploadCount(contributions.size());
view.showContributions(contributions);
this.contributionList.clear();
this.contributionList.addAll(contributions);
}
}
private void saveContributionsToDB(List<Contribution> contributions) {
Timber.e("Fetched: "+contributions.size()+" contributions "+" saving to db");
repository.save(contributions).subscribeOn(ioThreadScheduler).subscribe();
repository.set("last_fetch_timestamp",System.currentTimeMillis());
}
private boolean shouldFetchContributions() {
long lastFetchTimestamp = repository.getLong("last_fetch_timestamp");
Timber.d("last fetch timestamp: %s", lastFetchTimestamp);
if(lastFetchTimestamp!=0){
return System.currentTimeMillis()-lastFetchTimestamp>15*60*100;
}
Timber.d("should fetch contributions: %s", true);
return true;
}
@Override
public void onDetachView() {
this.view = null;
@ -146,24 +72,10 @@ public class ContributionsPresenter implements UserActionListener {
*/
@Override
public void deleteUpload(Contribution contribution) {
compositeDisposable.add(repository.deleteContributionFromDB(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
/**
* Returns a contribution at the specified cursor position
*
* @param i
* @return
*/
@Nullable
@Override
public Media getItemAtPosition(int i) {
if (i == -1 || contributionList.size() < i+1) {
return null;
}
return contributionList.get(i);
compositeDisposable.add(repository
.deleteContributionFromDB(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
@Override

View file

@ -1,12 +1,11 @@
package fr.free.nrw.commons.contributions;
import androidx.lifecycle.LiveData;
import androidx.paging.DataSource.Factory;
import io.reactivex.Completable;
import java.util.List;
import javax.inject.Inject;
import io.reactivex.Completable;
import io.reactivex.Single;
/**
@ -33,7 +32,7 @@ public class ContributionsRepository {
* @param contribution
* @return
*/
public Single<Integer> deleteContributionFromDB(Contribution contribution) {
public Completable deleteContributionFromDB(Contribution contribution) {
return localDataSource.deleteContribution(contribution);
}
@ -46,11 +45,11 @@ public class ContributionsRepository {
return localDataSource.getContributionWithFileName(fileName);
}
public LiveData<List<Contribution>> fetchContributions() {
public Factory<Integer, Contribution> fetchContributions() {
return localDataSource.getContributions();
}
public Completable save(List<Contribution> contributions) {
public Single<List<Long>> save(List<Contribution> contributions) {
return localDataSource.saveContributions(contributions);
}
@ -58,11 +57,7 @@ public class ContributionsRepository {
localDataSource.set(key,value);
}
public long getLong(String key) {
return localDataSource.getLong(key);
}
public Single<Integer> updateContribution(Contribution contribution) {
public Completable updateContribution(Contribution contribution) {
return localDataSource.updateContribution(contribution);
}
}

View file

@ -2,7 +2,6 @@ package fr.free.nrw.commons.contributions;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -12,7 +11,6 @@ import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
@ -20,16 +18,9 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import java.util.List;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.BuildConfig;
import com.google.android.material.tabs.TabLayout;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.location.LocationServiceManager;
@ -44,10 +35,10 @@ import fr.free.nrw.commons.upload.UploadService;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
import javax.inject.Inject;
import timber.log.Timber;
import static android.content.ContentResolver.requestSync;
public class MainActivity extends NavigationBaseActivity implements FragmentManager.OnBackStackChangedListener {
@BindView(R.id.tab_layout)