diff --git a/app/build.gradle b/app/build.gradle index 19a66ab7d..9520726a1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,6 +42,11 @@ dependencies { implementation 'com.dinuscxj:circleprogressbar:1.1.1' implementation 'com.karumi:dexter:5.0.0' implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" + + //paging + implementation "androidx.paging:paging-runtime-ktx:2.1.2" + implementation "androidx.paging:paging-rxjava2-ktx:2.1.2" + kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:$ADAPTER_DELEGATES_VERSION" implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION" diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 1dae32f48..214660334 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -6,6 +6,7 @@ import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.room.Entity; +import androidx.room.PrimaryKey; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.media.Depictions; import fr.free.nrw.commons.utils.CommonsDateUtil; @@ -15,6 +16,8 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.Objects; +import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.wikipedia.dataclient.mwapi.MwQueryPage; import org.wikipedia.gallery.ExtMetadata; @@ -50,6 +53,8 @@ public class Media implements Parcelable { /** * Wikibase Identifier associated with media files */ + @PrimaryKey + @NonNull private String pageId; private List categories; // as loaded at runtime? /** @@ -64,14 +69,28 @@ public class Media implements Parcelable { * Provides local constructor */ public Media() { + pageId = UUID.randomUUID().toString(); } + public static final Creator CREATOR = new Creator() { + @Override + public Media createFromParcel(final Parcel source) { + return new Media(source); + } + + @Override + public Media[] newArray(final int size) { + return new Media[size]; + } + }; + /** * Provides a minimal constructor * * @param filename Media filename */ - public Media(String filename) { + public Media(final String filename) { + this(); this.filename = filename; } @@ -86,11 +105,13 @@ public class Media implements Parcelable { * @param dateUploaded Media date uploaded * @param creator Media creator */ - public Media(Uri localUri, String imageUrl, String filename, - String description, - long dataLength, Date dateCreated, Date dateUploaded, String creator) { + public Media(final Uri localUri, final String imageUrl, final String filename, + final String description, + final long dataLength, final Date dateCreated, final Date dateUploaded, + final String creator) { + this(); this.localUri = localUri; - this.thumbUrl = imageUrl; + thumbUrl = imageUrl; this.imageUrl = imageUrl; this.filename = filename; this.description = description; @@ -100,17 +121,80 @@ public class Media implements Parcelable { this.creator = creator; } - public Media(Uri localUri, String filename, - String description, String creator, List categories) { + /** + * Constructor with all parameters + */ + public Media(final String pageId, + final Uri localUri, + final String thumbUrl, + final String imageUrl, + final String filename, + final String description, + final String discussion, + final long dataLength, + final Date dateCreated, + final Date dateUploaded, + final String license, + final String licenseUrl, + final String creator, + final List categories, + final boolean requestedDeletion, + final LatLng coordinates) { + this.pageId = pageId; + this.localUri = localUri; + this.thumbUrl = thumbUrl; + this.imageUrl = imageUrl; + this.filename = filename; + this.description = description; + this.discussion = discussion; + this.dataLength = dataLength; + this.dateCreated = dateCreated; + this.dateUploaded = dateUploaded; + this.license = license; + this.licenseUrl = licenseUrl; + this.creator = creator; + this.categories = categories; + this.requestedDeletion = requestedDeletion; + this.coordinates = coordinates; + } + + public Media(final Uri localUri, final String filename, + final String description, final String creator, final List categories) { this(localUri,null, filename, description, -1, null, new Date(), creator); this.categories = categories; } - public Media(String title, Date date, String user) { + public Media(final String title, final Date date, final String user) { this(null, null, title, "", -1, date, date, user); } + protected Media(final Parcel in) { + localUri = in.readParcelable(Uri.class.getClassLoader()); + thumbUrl = in.readString(); + imageUrl = in.readString(); + filename = in.readString(); + thumbnailTitle = in.readString(); + caption = in.readString(); + description = in.readString(); + discussion = in.readString(); + dataLength = in.readLong(); + final long tmpDateCreated = in.readLong(); + dateCreated = tmpDateCreated == -1 ? null : new Date(tmpDateCreated); + final long tmpDateUploaded = in.readLong(); + dateUploaded = tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded); + license = in.readString(); + licenseUrl = in.readString(); + creator = in.readString(); + pageId = in.readString(); + final ArrayList list = new ArrayList<>(); + in.readStringList(list); + categories = list; + in.readParcelable(Depictions.class.getClassLoader()); + requestedDeletion = in.readByte() != 0; + coordinates = in.readParcelable(LatLng.class.getClassLoader()); + } + /** * Creating Media object from MWQueryPage. * Earlier only basic details were set for the media object but going forward, @@ -120,14 +204,14 @@ public class Media implements Parcelable { * @return Media object */ @Nullable - public static Media from(MwQueryPage page) { - ImageInfo imageInfo = page.imageInfo(); + public static Media from(final MwQueryPage page) { + final ImageInfo imageInfo = page.imageInfo(); if (imageInfo == null) { return new Media(); // null is not allowed } - ExtMetadata metadata = imageInfo.getMetadata(); + final ExtMetadata metadata = imageInfo.getMetadata(); if (metadata == null) { - Media media = new Media(null, imageInfo.getOriginalUrl(), + final Media media = new Media(null, imageInfo.getOriginalUrl(), page.title(), "", 0, null, null, null); if (!StringUtils.isBlank(imageInfo.getThumbUrl())) { media.setThumbUrl(imageInfo.getThumbUrl()); @@ -135,7 +219,7 @@ public class Media implements Parcelable { return media; } - Media media = new Media(null, + final Media media = new Media(null, imageInfo.getOriginalUrl(), page.title(), "", @@ -158,11 +242,12 @@ public class Media implements Parcelable { media.setDescription(metadata.imageDescription()); media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories())); - String latitude = metadata.getGpsLatitude(); - String longitude = metadata.getGpsLongitude(); + final String latitude = metadata.getGpsLatitude(); + final String longitude = metadata.getGpsLongitude(); if (!StringUtils.isBlank(latitude) && !StringUtils.isBlank(longitude)) { - LatLng latLng = new LatLng(Double.parseDouble(latitude), Double.parseDouble(longitude), 0); + final LatLng latLng = new LatLng(Double.parseDouble(latitude), + Double.parseDouble(longitude), 0); media.setCoordinates(latLng); } @@ -175,29 +260,17 @@ public class Media implements Parcelable { * @param metadata * @return */ - private static String getArtist(ExtMetadata metadata) { + private static String getArtist(final ExtMetadata metadata) { try { - String artistHtml = metadata.artist(); + final String artistHtml = metadata.artist(); return artistHtml.substring(artistHtml.indexOf("title=\""), artistHtml.indexOf("\">")) .replace("title=\"User:", ""); - } catch (Exception ex) { + } catch (final Exception ex) { return ""; } } - /** - * @return pageId for the current media object*/ - public String getPageId() { - return pageId; - } - - /** - *sets pageId for the current media object - */ - public void setPageId(String pageId) { - this.pageId = pageId; - } - + @Nullable public String getThumbUrl() { return thumbUrl; } @@ -210,11 +283,13 @@ public class Media implements Parcelable { return filename != null ? getPageTitle().getDisplayTextWithoutNamespace().replaceFirst("[.][^.]+$", "") : ""; } - /** - * Set Caption(if available) as the thumbnail title of the image - */ - public void setThumbnailTitle(String title) { - this.thumbnailTitle = title; + @Nullable + private static Date safeParseDate(final String dateStr) { + try { + return CommonsDateUtil.getMediaSimpleDateFormat().parse(dateStr); + } catch (final ParseException e) { + return null; + } } /** @@ -260,19 +335,17 @@ public class Media implements Parcelable { } /** - * Sets the name of the file. - * @param filename the new name of the file - */ - public void setFilename(String filename) { - this.filename = filename; + * @return pageId for the current media object*/ + @NonNull + public String getPageId() { + return pageId; } /** - * Sets the discussion of the file. - * @param discussion + *sets pageId for the current media object */ - public void setDiscussion(String discussion) { - this.discussion = discussion; + public void setPageId(final String pageId) { + this.pageId = pageId; } /** @@ -310,11 +383,11 @@ public class Media implements Parcelable { } /** - * Sets the file description. - * @param description the new description of the file + * Sets the name of the file. + * @param filename the new name of the file */ - public void setDescription(String description) { - this.description = description; + public void setFilename(final String filename) { + this.filename = filename; } /** @@ -326,11 +399,11 @@ public class Media implements Parcelable { } /** - * Sets the dataLength of the file. - * @param dataLength as a long + * Sets the discussion of the file. + * @param discussion */ - public void setDataLength(long dataLength) { - this.dataLength = dataLength; + public void setDiscussion(final String discussion) { + this.discussion = discussion; } /** @@ -342,11 +415,11 @@ public class Media implements Parcelable { } /** - * Sets the creation date of the file. - * @param date creation date as a Date + * Sets the file description. + * @param description the new description of the file */ - public void setDateCreated(Date date) { - this.dateCreated = date; + public void setDescription(final String description) { + this.description = description; } /** @@ -368,11 +441,11 @@ public class Media implements Parcelable { } /** - * Sets the creator name of the file. - * @param creator creator name as a string + * Sets the dataLength of the file. + * @param dataLength as a long */ - public void setCreator(String creator) { - this.creator = creator; + public void setDataLength(final long dataLength) { + this.dataLength = dataLength; } /** @@ -383,8 +456,11 @@ public class Media implements Parcelable { return license; } - public void setThumbUrl(String thumbUrl) { - this.thumbUrl = thumbUrl; + /** + * Set Caption(if available) as the thumbnail title of the image + */ + public void setThumbnailTitle(final String title) { + thumbnailTitle = title; } public String getLicenseUrl() { @@ -392,16 +468,11 @@ public class Media implements Parcelable { } /** - * Sets the license name of the file. - * @param license license name as a String + * Sets the creator name of the file. + * @param creator creator name as a string */ - public void setLicenseInformation(String license, String licenseUrl) { - this.license = license; - - if (!licenseUrl.startsWith("http://") && !licenseUrl.startsWith("https://")) { - licenseUrl = "https://" + licenseUrl; - } - this.licenseUrl = licenseUrl; + public void setCreator(final String creator) { + this.creator = creator; } /** @@ -413,12 +484,8 @@ public class Media implements Parcelable { return coordinates; } - /** - * Sets the coordinates of where the file was created. - * @param coordinates file coordinates as a LatLng - */ - public void setCoordinates(@Nullable LatLng coordinates) { - this.coordinates = coordinates; + public void setThumbUrl(final String thumbUrl) { + this.thumbUrl = thumbUrl; } /** @@ -430,6 +497,27 @@ public class Media implements Parcelable { return categories; } + /** + * Sets the license name of the file. + * @param license license name as a String + */ + public void setLicenseInformation(final String license, String licenseUrl) { + this.license = license; + + if (!licenseUrl.startsWith("http://") && !licenseUrl.startsWith("https://")) { + licenseUrl = "https://" + licenseUrl; + } + this.licenseUrl = licenseUrl; + } + + /** + * Sets the coordinates of where the file was created. + * @param coordinates file coordinates as a LatLng + */ + public void setCoordinates(@Nullable final LatLng coordinates) { + this.coordinates = coordinates; + } + /** * Sets the categories the file falls under. *

@@ -437,26 +525,10 @@ public class Media implements Parcelable { * and then add the specified ones. * @param categories file categories as a list of Strings */ - public void setCategories(List categories) { + public void setCategories(final List categories) { this.categories = categories; } - @Nullable private static Date safeParseDate(String dateStr) { - try { - return CommonsDateUtil.getIso8601DateFormatShort().parse(dateStr); - } catch (ParseException e) { - return null; - } - } - - /** - * Set requested deletion to true - * @param requestedDeletion - */ - public void setRequestedDeletion(boolean requestedDeletion){ - this.requestedDeletion = requestedDeletion; - } - /** * Get the value of requested deletion * @return boolean requestedDeletion @@ -465,12 +537,20 @@ public class Media implements Parcelable { return requestedDeletion; } + /** + * Set requested deletion to true + * @param requestedDeletion + */ + public void setRequestedDeletion(final boolean requestedDeletion) { + this.requestedDeletion = requestedDeletion; + } + /** * Sets the license name of the file. * * @param license license name as a String */ - public void setLicense(String license) { + public void setLicense(final String license) { this.license = license; } @@ -482,15 +562,10 @@ public class Media implements Parcelable { * This function sets captions * @param caption */ - public void setCaption(String caption) { + public void setCaption(final String caption) { this.caption = caption; } - /* Sets depictions for the current media obtained fro Wikibase API*/ - public void setDepictions(Depictions depictions) { - this.depictions = depictions; - } - public void setLocalUri(@Nullable final Uri localUri) { this.localUri = localUri; } @@ -516,6 +591,19 @@ public class Media implements Parcelable { return 0; } + /* Sets depictions for the current media obtained fro Wikibase API*/ + public void setDepictions(final Depictions depictions) { + this.depictions = depictions; + } + + /** + * Sets the creation date of the file. + * @param date creation date as a Date + */ + public void setDateCreated(final Date date) { + dateCreated = date; + } + /** * Creates a way to transfer information between two or more * activities. @@ -523,63 +611,70 @@ public class Media implements Parcelable { * @param flags Parcel flag */ @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(this.localUri, flags); - dest.writeString(this.thumbUrl); - dest.writeString(this.imageUrl); - dest.writeString(this.filename); - dest.writeString(this.thumbnailTitle); - dest.writeString(this.caption); - dest.writeString(this.description); - dest.writeString(this.discussion); - dest.writeLong(this.dataLength); - dest.writeLong(this.dateCreated != null ? this.dateCreated.getTime() : -1); - dest.writeLong(this.dateUploaded != null ? this.dateUploaded.getTime() : -1); - dest.writeString(this.license); - dest.writeString(this.licenseUrl); - dest.writeString(this.creator); - dest.writeString(this.pageId); - dest.writeStringList(this.categories); - dest.writeParcelable(this.depictions, flags); - dest.writeByte(this.requestedDeletion ? (byte) 1 : (byte) 0); - dest.writeParcelable(this.coordinates, flags); + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeParcelable(localUri, flags); + dest.writeString(thumbUrl); + dest.writeString(imageUrl); + dest.writeString(filename); + dest.writeString(thumbnailTitle); + dest.writeString(caption); + dest.writeString(description); + dest.writeString(discussion); + dest.writeLong(dataLength); + dest.writeLong(dateCreated != null ? dateCreated.getTime() : -1); + dest.writeLong(dateUploaded != null ? dateUploaded.getTime() : -1); + dest.writeString(license); + dest.writeString(licenseUrl); + dest.writeString(creator); + dest.writeString(pageId); + dest.writeStringList(categories); + dest.writeParcelable(depictions, flags); + dest.writeByte(requestedDeletion ? (byte) 1 : (byte) 0); + dest.writeParcelable(coordinates, flags); } - protected Media(Parcel in) { - this.localUri = in.readParcelable(Uri.class.getClassLoader()); - this.thumbUrl = in.readString(); - this.imageUrl = in.readString(); - this.filename = in.readString(); - this.thumbnailTitle = in.readString(); - this.caption = in.readString(); - this.description = in.readString(); - this.discussion = in.readString(); - this.dataLength = in.readLong(); - long tmpDateCreated = in.readLong(); - this.dateCreated = tmpDateCreated == -1 ? null : new Date(tmpDateCreated); - long tmpDateUploaded = in.readLong(); - this.dateUploaded = tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded); - this.license = in.readString(); - this.licenseUrl = in.readString(); - this.creator = in.readString(); - this.pageId = in.readString(); - final ArrayList list = new ArrayList<>(); - in.readStringList(list); - this.categories=list; - in.readParcelable(Depictions.class.getClassLoader()); - this.requestedDeletion = in.readByte() != 0; - this.coordinates = in.readParcelable(LatLng.class.getClassLoader()); + /** + * Equals implementation that matches all parameters for equality check + */ + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Media)) { + return false; + } + final Media media = (Media) o; + return getDataLength() == media.getDataLength() && + isRequestedDeletion() == media.isRequestedDeletion() && + Objects.equals(getLocalUri(), media.getLocalUri()) && + Objects.equals(getThumbUrl(), media.getThumbUrl()) && + Objects.equals(getImageUrl(), media.getImageUrl()) && + Objects.equals(getFilename(), media.getFilename()) && + Objects.equals(getThumbnailTitle(), media.getThumbnailTitle()) && + Objects.equals(getCaption(), media.getCaption()) && + Objects.equals(getDescription(), media.getDescription()) && + Objects.equals(getDiscussion(), media.getDiscussion()) && + Objects.equals(getDateCreated(), media.getDateCreated()) && + Objects.equals(getDateUploaded(), media.getDateUploaded()) && + Objects.equals(getLicense(), media.getLicense()) && + Objects.equals(getLicenseUrl(), media.getLicenseUrl()) && + Objects.equals(getCreator(), media.getCreator()) && + getPageId().equals(media.getPageId()) && + Objects.equals(getCategories(), media.getCategories()) && + Objects.equals(getDepictions(), media.getDepictions()) && + Objects.equals(getCoordinates(), media.getCoordinates()); } - public static final Creator CREATOR = new Creator() { - @Override - public Media createFromParcel(Parcel source) { - return new Media(source); - } - - @Override - public Media[] newArray(int size) { - return new Media[size]; - } - }; + /** + * Hashcode implementation that uses all parameters for calculating hash + */ + @Override + public int hashCode() { + return Objects + .hash(getLocalUri(), getThumbUrl(), getImageUrl(), getFilename(), getThumbnailTitle(), + getCaption(), getDescription(), getDiscussion(), getDataLength(), getDateCreated(), + getDateUploaded(), getLicense(), getLicenseUrl(), getCreator(), getPageId(), + getCategories(), getDepictions(), isRequestedDeletion(), getCoordinates()); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.java b/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.java index 9d3bde848..59e33ea24 100644 --- a/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.java +++ b/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.java @@ -21,7 +21,8 @@ public final class OkHttpConnectionFactory { @NonNull private static final Cache NET_CACHE = new Cache(new File(CommonsApplication.getInstance().getCacheDir(), CACHE_DIR_NAME), NET_CACHE_SIZE); - @NonNull private static final OkHttpClient CLIENT = createClient(); + @NonNull + private static final OkHttpClient CLIENT = createClient(); @NonNull public static OkHttpClient getClient() { return CLIENT; @@ -40,7 +41,7 @@ public final class OkHttpConnectionFactory { private static HttpLoggingInterceptor getLoggingInterceptor() { final HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor() - .setLevel(Level.BASIC); + .setLevel(Level.BASIC); httpLoggingInterceptor.redactHeader("Authorization"); httpLoggingInterceptor.redactHeader("Cookie"); @@ -49,7 +50,10 @@ public final class OkHttpConnectionFactory { } private static class CommonHeaderRequestInterceptor implements Interceptor { - @Override @NonNull public Response intercept(@NonNull final Chain chain) throws IOException { + + @Override + @NonNull + public Response intercept(@NonNull final Chain chain) throws IOException { final Request request = chain.request().newBuilder() .header("User-Agent", CommonsApplication.getInstance().getUserAgent()) .build(); @@ -61,16 +65,18 @@ public final class OkHttpConnectionFactory { private static final String ERRORS_PREFIX = "{\"error"; - @Override @NonNull public Response intercept(@NonNull final Chain chain) throws IOException { + @Override + @NonNull + public Response intercept(@NonNull final Chain chain) throws IOException { final Response rsp = chain.proceed(chain.request()); if (rsp.isSuccessful()) { try (final ResponseBody responseBody = rsp.peekBody(ERRORS_PREFIX.length())) { - if (ERRORS_PREFIX.equals(responseBody.string())){ + if (ERRORS_PREFIX.equals(responseBody.string())) { try (final ResponseBody body = rsp.body()) { throw new IOException(body.string()); } } - }catch (final IOException e){ + } catch (final IOException e) { Timber.e(e); } return rsp; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java index 99e6c4b45..5461ccfca 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java @@ -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 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: "" - * key = "de" , value: "" + * 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: "" key = "de" , value: "" */ private Map 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 + *

* key of the HashMap is the language and value is the caption in the corresponding language - * + *

* returns list of captions stored in hashmap */ public Map getCaptions() { - return captions; + return captions; } public void setCaptions(Map 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()); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt new file mode 100644 index 000000000..ba5d1ed7f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt @@ -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() { + 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 -> + 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) { + compositeDisposable.add( + repository.save(contributions) + .subscribeOn(ioThreadScheduler) + .subscribe { longs: List? -> + repository["last_fetch_timestamp"] = System.currentTimeMillis() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java index 3c0ac925f..e288a84af 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java @@ -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> fetchContributions(); + @Query("SELECT * FROM contribution order by dateUploaded DESC") + abstract DataSource.Factory fetchContributions(); - @Insert(onConflict = OnConflictStrategy.REPLACE) - public abstract Single save(Contribution contribution); + @Insert(onConflict = OnConflictStrategy.REPLACE) + public abstract void saveSynchronous(Contribution contribution); - public Completable deleteAllAndSave(List contributions){ - return Completable.fromAction(() -> deleteAllAndSaveTransaction(contributions)); - } + public Completable save(final Contribution contribution) { + return Completable + .fromAction(() -> saveSynchronous(contribution)); + } - @Transaction - public void deleteAllAndSaveTransaction(List 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); + public Completable saveAndDelete(final Contribution oldContribution, + final Contribution newContribution) { + return Completable + .fromAction(() -> deleteAndSaveContribution(oldContribution, newContribution)); + } - @Delete - public abstract Single delete(Contribution contribution); + @Insert(onConflict = OnConflictStrategy.REPLACE) + public abstract Single> save(List contribution); - @Query("SELECT * from contribution WHERE filename=:fileName") - public abstract List getContributionWithTitle(String fileName); + @Delete + public abstract void deleteSynchronous(Contribution contribution); - @Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)") - public abstract Single 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 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 updateStates(int state, int[] toUpdateStates); - @Update - public abstract Single 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)); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java index 501f1fe58..a5de53bb1 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java @@ -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; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java index 8b0049004..d578d1185 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java @@ -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 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); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java index 9022bbfb9..8e3221ea9 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -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 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(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java index 82daf97f4..a0c8e1088 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java @@ -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 { +public class ContributionsListAdapter extends + PagedListAdapter { - private Callback callback; + private final Callback callback; private final MediaClient mediaClient; - private List 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 DIFF_CALLBACK = + new DiffUtil.ItemCallback() { + @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 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 { + + void deleteUpload(Contribution contribution); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index d7f813c6d..d707409e4 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -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 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 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); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java new file mode 100644 index 000000000..f09c5f205 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java @@ -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> 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()); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsLocalDataSource.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsLocalDataSource.java index c16862ea2..3b94d0fa7 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsLocalDataSource.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsLocalDataSource.java @@ -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 deleteContribution(Contribution contribution) { + public Completable deleteContribution(Contribution contribution) { return contributionDao.delete(contribution); } - public LiveData> getContributions() { + public Factory getContributions() { return contributionDao.fetchContributions(); } - public Completable saveContributions(List contributions) { - return contributionDao.deleteAllAndSave(contributions); + public Single> saveContributions(List contributions) { + return contributionDao.save(contributions); } public void set(String key, long value) { defaultKVStore.putLong(key,value); } - public Single updateContribution(Contribution contribution) { + public Completable updateContribution(Contribution contribution) { return contributionDao.update(contribution); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java index 310699cd1..b4725fed8 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java @@ -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 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> 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 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 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 diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRepository.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRepository.java index 500babaf5..17b004802 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRepository.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRepository.java @@ -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 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> fetchContributions() { + public Factory fetchContributions() { return localDataSource.getContributions(); } - public Completable save(List contributions) { + public Single> save(List 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 updateContribution(Contribution contribution) { + public Completable updateContribution(Contribution contribution) { return localDataSource.updateContribution(contribution); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index 648e21ef6..9606abee4 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -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) diff --git a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt index 01095dd7c..a45fd0c6e 100644 --- a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt +++ b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt @@ -10,7 +10,7 @@ import fr.free.nrw.commons.contributions.ContributionDao * The database for accessing the respective DAOs * */ -@Database(entities = [Contribution::class], version = 1, exportSchema = false) +@Database(entities = [Contribution::class], version = 2, exportSchema = false) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun contributionDao(): ContributionDao diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index f19ab4f3a..c76c54673 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -224,7 +224,9 @@ public class CommonsApplicationModule { @Provides @Singleton public AppDatabase provideAppDataBase() { - return Room.databaseBuilder(applicationContext, AppDatabase.class, "commons_room.db").build(); + return Room.databaseBuilder(applicationContext, AppDatabase.class, "commons_room.db") + .fallbackToDestructiveMigration() + .build(); } @Provides diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java index 3bbe842b5..0ddd9fc70 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java @@ -32,6 +32,7 @@ public class MediaClient { //OkHttpJsonApiClient used JsonKvStore for this. I don't know why. private Map> continuationStore; + private Map continuationExists; public static final String NO_CAPTION = "No caption"; private static final String NO_DEPICTION = "No depiction"; @@ -40,6 +41,7 @@ public class MediaClient { this.mediaInterface = mediaInterface; this.mediaDetailInterface = mediaDetailInterface; this.continuationStore = new HashMap<>(); + this.continuationExists = new HashMap<>(); } /** @@ -83,6 +85,36 @@ public class MediaClient { } + /** + * This method takes the userName as input and returns a list of Media objects filtered using + * allimages query It uses the allimages query API to get the images contributed by the userName, + * 10 at a time. + * + * @param userName the username + * @return + */ + public Single> getMediaListForUser(String userName) { + Map continuation = + continuationStore.containsKey("user_" + userName) + ? continuationStore.get("user_" + userName) + : Collections.emptyMap(); + return responseToMediaList(mediaInterface + .getMediaListForUser(userName, 10, continuation), "user_" + userName); + } + + /** + * Check if media for user has reached the end of the list. + * @param userName + * @return + */ + public boolean doesMediaListForUserHaveMorePages(String userName) { + final String key = "user_" + userName; + if(continuationExists.containsKey(key)) { + return continuationExists.get(key); + } + return true; + } + /** * This method takes a keyword as input and returns a list of Media objects filtered using image generator query * It uses the generator query API to get the images searched using a query, 10 at a time. @@ -106,7 +138,12 @@ public class MediaClient { || null == mwQueryResponse.query().pages()) { return Observable.empty(); } - continuationStore.put(key, mwQueryResponse.continuation()); + if(mwQueryResponse.continuation() != null) { + continuationStore.put(key, mwQueryResponse.continuation()); + continuationExists.put(key, true); + } else { + continuationExists.put(key, false); + } return Observable.fromIterable(mwQueryResponse.query().pages()); }) .map(Media::from) diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index 39c81386e..3e5f3f0fd 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -23,26 +23,21 @@ import butterknife.ButterKnife; import com.google.android.material.snackbar.Snackbar; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.bookmarks.Bookmark; import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider; import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao; import fr.free.nrw.commons.category.CategoryImagesCallback; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.utils.DownloadUtils; import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.ViewUtil; import javax.inject.Inject; -import javax.inject.Named; import timber.log.Timber; public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener { - @Inject SessionManager sessionManager; - @Inject @Named("default_preferences") JsonKvStore store; @Inject BookmarkPicturesDao bookmarkDao; @BindView(R.id.mediaDetailsPager) ViewPager pager; diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java index 10dd02a83..e102a8fcb 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java @@ -1,11 +1,9 @@ package fr.free.nrw.commons.media; -import org.wikipedia.dataclient.mwapi.MwQueryResponse; - -import java.util.Map; - import fr.free.nrw.commons.depictions.models.DepictionResponse; import io.reactivex.Observable; +import java.util.Map; +import org.wikipedia.dataclient.mwapi.MwQueryResponse; import retrofit2.http.GET; import retrofit2.http.Query; import retrofit2.http.QueryMap; @@ -17,6 +15,7 @@ public interface MediaInterface { String MEDIA_PARAMS="&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=640" + "&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal" + "|Artist|LicenseShortName|LicenseUrl"; + /** * Checks if a page exists or not. * @@ -48,6 +47,19 @@ public interface MediaInterface { MEDIA_PARAMS) Observable getMediaListFromCategory(@Query("gcmtitle") String category, @Query("gcmlimit") int itemLimit, @QueryMap Map continuation); + /** + * This method retrieves a list of Media objects for a given user name + * + * @param username user's Wikimedia Commons username. + * @param itemLimit how many images are returned + * @param continuation the continuation string from the previous query or empty map + * @return + */ + @GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters + "&generator=allimages&gaisort=timestamp&gaidir=older" + MEDIA_PARAMS) + Observable getMediaListForUser(@Query("gaiuser") String username, + @Query("gailimit") int itemLimit, @QueryMap(encoded = true) Map continuation); + /** * This method retrieves a list of Media objects filtered using image generator query * @@ -86,21 +98,15 @@ public interface MediaInterface { Observable getPageHtml(@Query("page") String title); /** - * Fetches caption using file name - * - * @param filename name of the file to be used for fetching captions - * */ - @GET("w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1") - Observable fetchCaptionByFilename(@Query("language") String language, @Query("titles") String filename); + * Fetches list of images from a depiction entity + * + * @param query depictionEntityId + * @param sroffset number od depictions already fetched, this is useful in implementing + * pagination + */ - /** - * Fetches list of images from a depiction entity - * - * @param query depictionEntityId - * @param sroffset number od depictions already fetched, this is useful in implementing pagination - */ - - @GET("w/api.php?action=query&list=search&format=json&srnamespace=6") - Observable fetchImagesForDepictedItem(@Query("srsearch") String query, @Query("sroffset") String sroffset); + @GET("w/api.php?action=query&list=search&format=json&srnamespace=6") + Observable fetchImagesForDepictedItem(@Query("srsearch") String query, + @Query("sroffset") String sroffset); } diff --git a/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java b/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java index 7ea86a019..8f625205c 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java @@ -6,7 +6,6 @@ public class Prefs { public static String TRACKING_ENABLED = "eventLogging"; public static final String DEFAULT_LICENSE = "defaultLicense"; public static final String UPLOADS_SHOWING = "uploadsshowing"; - public static final String IS_CONTRIBUTION_COUNT_CHANGED = "ccontributionCountChanged"; public static final String MANAGED_EXIF_TAGS = "managed_exif_tags"; public static final String KEY_LANGUAGE_VALUE = "languageDescription"; public static final String KEY_THEME_VALUE = "appThemePref"; diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 00ef4ed4b..53cf8d7fe 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -65,43 +65,6 @@ public class SettingsFragment extends PreferenceFragmentCompat { }); } - final EditTextPreference uploadLimit = findPreference("uploads"); - int currentUploadLimit = defaultKvStore.getInt(Prefs.UPLOADS_SHOWING, 100); - uploadLimit.setText(String.valueOf(currentUploadLimit)); - - uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> { - - if (newValue.toString().length() == 0) { - return false; - } - - int value = Integer.parseInt(newValue.toString()); - if (value > 500) { - Snackbar error = Snackbar.make(getView(), R.string.maximum_limit_alert, Snackbar.LENGTH_LONG); - error.show(); - return false; - } else if (value == 0) { - Snackbar error = Snackbar.make(getView(), R.string.cannot_be_zero, Snackbar.LENGTH_LONG); - error.show(); - return false; - } - return true; - }); - - uploadLimit.setOnBindEditTextListener(editText -> { - - editText.setInputType(InputType.TYPE_CLASS_NUMBER); - editText.selectAll(); - int maxLength = 3; // set maxLength to 3 - editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)}); - - int value = Integer.parseInt(editText.getText().toString()); - - defaultKvStore.putInt(Prefs.UPLOADS_SHOWING, value); - defaultKvStore.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, true); - uploadLimit.setText(Integer.toString(value)); - }); - langListPreference = findPreference("descriptionDefaultLanguagePref"); prepareLanguages(); Preference betaTesterPreference = findPreference("becomeBetaTester"); @@ -121,7 +84,6 @@ public class SettingsFragment extends PreferenceFragmentCompat { findPreference("displayNearbyCardView").setEnabled(false); findPreference("displayLocationPermissionForCardView").setEnabled(false); findPreference("displayCampaignsCardView").setEnabled(false); - uploadLimit.setEnabled(false); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 89f7fce12..0f88be382 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -23,9 +23,14 @@ import fr.free.nrw.commons.di.CommonsDaggerService; import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.utils.CommonsDateUtil; import fr.free.nrw.commons.wikidata.WikidataEditService; +import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Scheduler; +import io.reactivex.Single; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; import io.reactivex.processors.PublishProcessor; import io.reactivex.schedulers.Schedulers; import java.io.File; @@ -33,6 +38,7 @@ import java.io.IOException; import java.text.ParseException; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.Callable; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; @@ -106,10 +112,10 @@ public class UploadService extends CommonsDaggerService { notificationManager.notify(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build()); contribution.setTransferred(transferred); - compositeDisposable.add(contributionDao. - save(contribution).subscribeOn(ioThreadScheduler) - .observeOn(mainThreadScheduler) - .subscribe()); + + compositeDisposable.add(contributionDao.update(contribution) + .subscribeOn(ioThreadScheduler) + .subscribe()); } } @@ -156,14 +162,11 @@ public class UploadService extends CommonsDaggerService { Timber.d("%d uploads left", toUpload); notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build()); } + compositeDisposable.add(contributionDao .save(contribution) .subscribeOn(ioThreadScheduler) - .observeOn(mainThreadScheduler) - .subscribe(aLong->{ - contribution.set_id(aLong); - uploadContribution(contribution); - }, Throwable::printStackTrace)); + .subscribe(() -> uploadContribution(contribution))); } private boolean freshStart = true; @@ -269,7 +272,7 @@ public class UploadService extends CommonsDaggerService { } private void onUpload(Contribution contribution, String notificationTag, - UploadResult uploadResult) throws ParseException { + UploadResult uploadResult) { Timber.d("Stash upload response 2 is %s", uploadResult.toString()); notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS); @@ -282,8 +285,7 @@ public class UploadService extends CommonsDaggerService { } } - private void onSuccessfulUpload(Contribution contribution, UploadResult uploadResult) - throws ParseException { + private void onSuccessfulUpload(Contribution contribution, UploadResult uploadResult) { compositeDisposable .add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution)); WikidataPlace wikidataPlace = contribution.getWikidataPlace(); @@ -293,17 +295,11 @@ public class UploadService extends CommonsDaggerService { saveCompletedContribution(contribution, uploadResult); } - private void saveCompletedContribution(Contribution contribution, UploadResult uploadResult) throws ParseException { - contribution.setFilename(uploadResult.createCanonicalFileName()); - contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl()); - contribution.setState(Contribution.STATE_COMPLETED); - contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatTimestamp() - .parse(uploadResult.getImageinfo().getTimestamp())); - compositeDisposable.add(contributionDao - .save(contribution) - .subscribeOn(ioThreadScheduler) - .observeOn(mainThreadScheduler) - .subscribe()); + private void saveCompletedContribution(Contribution contribution, UploadResult uploadResult) { + compositeDisposable.add(mediaClient.getMedia("File:" + uploadResult.getFilename()) + .map(media -> new Contribution(media, Contribution.STATE_COMPLETED)) + .flatMapCompletable(newContribution -> contributionDao.saveAndDelete(contribution, newContribution)) + .subscribe()); } @SuppressLint("StringFormatInvalid") @@ -317,10 +313,11 @@ public class UploadService extends CommonsDaggerService { notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_FAILED, curNotification.build()); contribution.setState(Contribution.STATE_FAILED); - compositeDisposable.add(contributionDao.save(contribution) - .subscribeOn(ioThreadScheduler) - .observeOn(mainThreadScheduler) - .subscribe()); + + compositeDisposable.add(contributionDao + .update(contribution) + .subscribeOn(ioThreadScheduler) + .subscribe()); } private String findUniqueFilename(String fileName) throws IOException { diff --git a/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.java index d03dd8fe4..f831baa0a 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.java @@ -21,6 +21,16 @@ public class CommonsDateUtil { return simpleDateFormat; } + /** + * Gets SimpleDateFormat for date pattern returned by Media object + * @return simpledateformat + */ + public static SimpleDateFormat getMediaSimpleDateFormat() { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT); + simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return simpleDateFormat; + } + /** * Gets the timestamp pattern for a date * @return timestamp diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index e58dbefd6..634c147e0 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -26,13 +26,6 @@ android:summary="@string/use_external_storage_summary" android:title="@string/use_external_storage" /> - - ())) + .thenReturn(Single.just(listOf(1L, 2L))) + whenever(sessionManager.userName).thenReturn("Test") + whenever(mediaClient.getMediaListForUser(anyString())).thenReturn( + Single.just(listOf(mock(Media::class.java))) + ) + whenever(mediaClient.doesMediaListForUserHaveMorePages(anyString())) + .thenReturn(true) + contributionBoundaryCallback.onZeroItemsLoaded() + verify(repository).save(anyList()); + verify(mediaClient).getMediaListForUser(anyString()); + } + + @Test + fun testOnLastItemLoaded() { + whenever(repository.save(anyList())) + .thenReturn(Single.just(listOf(1L, 2L))) + whenever(sessionManager.userName).thenReturn("Test") + whenever(mediaClient.getMediaListForUser(anyString())).thenReturn( + Single.just(listOf(mock(Media::class.java))) + ) + whenever(mediaClient.doesMediaListForUserHaveMorePages(anyString())) + .thenReturn(true) + contributionBoundaryCallback.onItemAtEndLoaded(mock(Contribution::class.java)) + verify(repository).save(anyList()); + verify(mediaClient).getMediaListForUser(anyString()); + } + + @Test + fun testOnFrontItemLoaded() { + whenever(repository.save(anyList())) + .thenReturn(Single.just(listOf(1L, 2L))) + whenever(sessionManager.userName).thenReturn("Test") + whenever(mediaClient.getMediaListForUser(anyString())).thenReturn( + Single.just(listOf(mock(Media::class.java))) + ) + whenever(mediaClient.doesMediaListForUserHaveMorePages(anyString())) + .thenReturn(true) + contributionBoundaryCallback.onItemAtFrontLoaded(mock(Contribution::class.java)) + verify(repository).save(anyList()); + verify(mediaClient).getMediaListForUser(anyString()); + } + + @Test + fun testFetchContributions() { + whenever(repository.save(anyList())) + .thenReturn(Single.just(listOf(1L, 2L))) + whenever(sessionManager.userName).thenReturn("Test") + whenever(mediaClient.getMediaListForUser(anyString())).thenReturn( + Single.just(listOf(mock(Media::class.java))) + ) + whenever(mediaClient.doesMediaListForUserHaveMorePages(anyString())) + .thenReturn(true) + contributionBoundaryCallback.fetchContributions() + verify(repository).save(anyList()); + verify(mediaClient).getMediaListForUser(anyString()); + } + + @Test + fun testFetchContributionsForEndOfList() { + whenever(sessionManager.userName).thenReturn("Test") + whenever(mediaClient.doesMediaListForUserHaveMorePages(anyString())) + .thenReturn(false) + contributionBoundaryCallback.fetchContributions() + verify(mediaClient, times(0)).getMediaListForUser(anyString()) + verifyNoMoreInteractions(repository) + } + + @Test + fun testFetchContributionsFailed() { + whenever(sessionManager.userName).thenReturn("Test") + whenever(mediaClient.doesMediaListForUserHaveMorePages(anyString())) + .thenReturn(true) + whenever(mediaClient.getMediaListForUser(anyString())).thenReturn(Single.error(Exception("Error"))) + contributionBoundaryCallback.fetchContributions() + verifyZeroInteractions(repository); + verify(mediaClient).getMediaListForUser(anyString()); + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsListPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsListPresenterTest.kt new file mode 100644 index 000000000..7e9b723a7 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsListPresenterTest.kt @@ -0,0 +1,63 @@ +package fr.free.nrw.commons.contributions + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.verifyZeroInteractions +import com.nhaarman.mockitokotlin2.whenever +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.media.MediaClient +import io.reactivex.Completable +import io.reactivex.Scheduler +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.any +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations + +/** + * The unit test class for ContributionsListPresenterTest + */ +class ContributionsListPresenterTest { + @Mock + internal lateinit var contributionBoundaryCallback: ContributionBoundaryCallback + + @Mock + internal lateinit var repository: ContributionsRepository + + @Rule + @JvmField + var instantTaskExecutorRule = InstantTaskExecutorRule() + + lateinit var scheduler: Scheduler + + lateinit var contributionsListPresenter: ContributionsListPresenter + + /** + * initial setup + */ + @Before + @Throws(Exception::class) + fun setUp() { + MockitoAnnotations.initMocks(this) + scheduler = Schedulers.trampoline() + contributionsListPresenter = + ContributionsListPresenter(contributionBoundaryCallback, repository, scheduler); + } + + @Test + fun testDeleteUpload() { + whenever(repository.deleteContributionFromDB(any())) + .thenReturn(Completable.complete()) + contributionsListPresenter.deleteUpload(mock(Contribution::class.java)) + verify(repository, times(1)) + .deleteContributionFromDB(ArgumentMatchers.any(Contribution::class.java)); + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsPresenterTest.kt index e318b2ea7..05546504a 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsPresenterTest.kt @@ -7,9 +7,11 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.loader.content.CursorLoader import androidx.loader.content.Loader +import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever +import io.reactivex.Completable import io.reactivex.Scheduler import io.reactivex.Single import io.reactivex.schedulers.TestScheduler @@ -17,9 +19,11 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.* import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations +import java.util.concurrent.TimeUnit /** * The unit test class for ContributionsPresenter @@ -42,7 +46,7 @@ class ContributionsPresenterTest { @Rule @JvmField var instantTaskExecutorRule = InstantTaskExecutorRule() - lateinit var scheduler : Scheduler + lateinit var scheduler : TestScheduler /** * initial setup @@ -60,23 +64,13 @@ class ContributionsPresenterTest { liveData=MutableLiveData() } - /** - * Test fetch contributions - */ - @Test - fun testFetchContributions(){ - whenever(repository.getString(ArgumentMatchers.anyString())).thenReturn("10") - whenever(repository.fetchContributions()).thenReturn(liveData) - contributionsPresenter.fetchContributions() - verify(repository).fetchContributions() - } - /** * Test presenter actions onDeleteContribution */ @Test fun testDeleteContribution() { - whenever(repository.deleteContributionFromDB(ArgumentMatchers.any(Contribution::class.java))).thenReturn(Single.just(1)) + whenever(repository.deleteContributionFromDB(ArgumentMatchers.any())) + .thenReturn(Completable.complete()) contributionsPresenter.deleteUpload(contribution) verify(repository).deleteContributionFromDB(contribution) } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsRepositoryTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsRepositoryTest.kt new file mode 100644 index 000000000..0b11c4bb4 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsRepositoryTest.kt @@ -0,0 +1,55 @@ +package fr.free.nrw.commons.contributions + +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import fr.free.nrw.commons.utils.createMockDataSourceFactory +import io.reactivex.Scheduler +import io.reactivex.Single +import junit.framework.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.* +import org.mockito.Mockito.any +import org.mockito.Mockito.mock + +/** + * The unit test class for ContributionsRepositoryTest + */ +class ContributionsRepositoryTest { + @Mock + internal lateinit var localDataSource: ContributionsLocalDataSource + + @InjectMocks + private lateinit var contributionsRepository: ContributionsRepository + + lateinit var scheduler: Scheduler + + /** + * initial setup + */ + @Before + @Throws(Exception::class) + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun testFetchContributions() { + val contribution = mock(Contribution::class.java) + whenever(localDataSource.getContributions()) + .thenReturn(createMockDataSourceFactory(listOf(contribution))) + val contributionsFactory = contributionsRepository.fetchContributions() + verify(localDataSource, times(1)).getContributions(); + } + + @Test + fun testSaveContribution() { + val contributions = listOf(mock(Contribution::class.java)) + whenever(localDataSource.saveContributions(ArgumentMatchers.anyList())) + .thenReturn(Single.just(listOf(1L))) + val save = contributionsRepository.save(contributions).test().assertValueAt(0) { + it.size == 1 && it.get(0) == 1L + } + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaClientTest.kt index 5579ce72f..d594e722b 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaClientTest.kt @@ -1,5 +1,6 @@ package fr.free.nrw.commons.media +import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.Media import fr.free.nrw.commons.utils.CommonsDateUtil import io.reactivex.Observable @@ -7,18 +8,16 @@ import junit.framework.Assert.* import org.junit.Before import org.junit.Test import org.mockito.* -import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock import org.wikipedia.dataclient.mwapi.ImageDetails import org.wikipedia.dataclient.mwapi.MwQueryPage import org.wikipedia.dataclient.mwapi.MwQueryResponse import org.wikipedia.dataclient.mwapi.MwQueryResult import org.wikipedia.gallery.ImageInfo import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.* import java.util.* import org.mockito.Captor - - +import org.mockito.Mockito.* class MediaClientTest { @@ -46,9 +45,10 @@ class MediaClientTest { `when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mediaInterface!!.checkPageExistsUsingTitle(ArgumentMatchers.anyString())) - .thenReturn(Observable.just(mockResponse)) + .thenReturn(Observable.just(mockResponse)) - val checkPageExistsUsingTitle = mediaClient!!.checkPageExistsUsingTitle("File:Test.jpg").blockingGet() + val checkPageExistsUsingTitle = + mediaClient!!.checkPageExistsUsingTitle("File:Test.jpg").blockingGet() assertTrue(checkPageExistsUsingTitle) } @@ -63,9 +63,10 @@ class MediaClientTest { `when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mediaInterface!!.checkPageExistsUsingTitle(ArgumentMatchers.anyString())) - .thenReturn(Observable.just(mockResponse)) + .thenReturn(Observable.just(mockResponse)) - val checkPageExistsUsingTitle = mediaClient!!.checkPageExistsUsingTitle("File:Test.jpg").blockingGet() + val checkPageExistsUsingTitle = + mediaClient!!.checkPageExistsUsingTitle("File:Test.jpg").blockingGet() assertFalse(checkPageExistsUsingTitle) } @@ -80,7 +81,7 @@ class MediaClientTest { `when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mediaInterface!!.checkFileExistsUsingSha(ArgumentMatchers.anyString())) - .thenReturn(Observable.just(mockResponse)) + .thenReturn(Observable.just(mockResponse)) val checkFileExistsUsingSha = mediaClient!!.checkFileExistsUsingSha("abcde").blockingGet() assertTrue(checkFileExistsUsingSha) @@ -97,7 +98,7 @@ class MediaClientTest { `when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mediaInterface!!.checkFileExistsUsingSha(ArgumentMatchers.anyString())) - .thenReturn(Observable.just(mockResponse)) + .thenReturn(Observable.just(mockResponse)) val checkFileExistsUsingSha = mediaClient!!.checkFileExistsUsingSha("abcde").blockingGet() assertFalse(checkFileExistsUsingSha) @@ -117,7 +118,7 @@ class MediaClientTest { `when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString())) - .thenReturn(Observable.just(mockResponse)) + .thenReturn(Observable.just(mockResponse)) assertEquals("Test", mediaClient!!.getMedia("abcde").blockingGet().filename) } @@ -136,10 +137,11 @@ class MediaClientTest { `when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString())) - .thenReturn(Observable.just(mockResponse)) + .thenReturn(Observable.just(mockResponse)) assertEquals(Media.EMPTY, mediaClient!!.getMedia("abcde").blockingGet()) } + @Captor private val filenameCaptor: ArgumentCaptor? = null @@ -159,18 +161,18 @@ class MediaClientTest { `when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mediaInterface!!.getMediaWithGenerator(filenameCaptor!!.capture())) - .thenReturn(Observable.just(mockResponse)) + .thenReturn(Observable.just(mockResponse)) assertEquals("Test", mediaClient!!.getPictureOfTheDay().blockingGet().filename) assertEquals(template, filenameCaptor.value); } - + @Captor private val continuationCaptor: ArgumentCaptor>? = null @Test fun getMediaListFromCategoryTwice() { - val mockContinuation= mapOf(Pair("gcmcontinue", "test")) + val mockContinuation = mapOf(Pair("gcmcontinue", "test")) val imageInfo = ImageInfo() val mwQueryPage = mock(MwQueryPage::class.java) @@ -184,9 +186,13 @@ class MediaClientTest { `when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mockResponse.continuation()).thenReturn(mockContinuation) - `when`(mediaInterface!!.getMediaListFromCategory(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(), - continuationCaptor!!.capture())) - .thenReturn(Observable.just(mockResponse)) + `when`( + mediaInterface!!.getMediaListFromCategory( + ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(), + continuationCaptor!!.capture() + ) + ) + .thenReturn(Observable.just(mockResponse)) val media1 = mediaClient!!.getMediaListFromCategory("abcde").blockingGet().get(0) val media2 = mediaClient!!.getMediaListFromCategory("abcde").blockingGet().get(0) @@ -197,6 +203,38 @@ class MediaClientTest { assertEquals(media2.filename, "Test") } + @Test + fun getMediaListForUser() { + val mockContinuation = mapOf("gcmcontinue" to "test") + val imageInfo = ImageInfo() + + val mwQueryPage = mock(MwQueryPage::class.java) + whenever(mwQueryPage.title()).thenReturn("Test") + whenever(mwQueryPage.imageInfo()).thenReturn(imageInfo) + + val mwQueryResult = mock(MwQueryResult::class.java) + whenever(mwQueryResult.pages()).thenReturn(listOf(mwQueryPage)) + + val mockResponse = mock(MwQueryResponse::class.java) + whenever(mockResponse.query()).thenReturn(mwQueryResult) + whenever(mockResponse.continuation()).thenReturn(mockContinuation) + + whenever( + mediaInterface!!.getMediaListForUser( + ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(), + continuationCaptor!!.capture() + ) + ) + .thenReturn(Observable.just(mockResponse)) + val media1 = mediaClient!!.getMediaListForUser("Test").blockingGet().get(0) + val media2 = mediaClient!!.getMediaListForUser("Test").blockingGet().get(0) + + verify(mediaInterface, times(2))?.getMediaListForUser( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyInt(), ArgumentMatchers.anyMap() + ) + } + @Test fun getPageHtmlTest() { val mwParseResult = mock(MwParseResult::class.java) @@ -207,7 +245,7 @@ class MediaClientTest { mockResponse.setParse(mwParseResult) `when`(mediaInterface!!.getPageHtml(ArgumentMatchers.anyString())) - .thenReturn(Observable.just(mockResponse)) + .thenReturn(Observable.just(mockResponse)) assertEquals("Test", mediaClient!!.getPageHtml("abcde").blockingGet()) } @@ -218,7 +256,7 @@ class MediaClientTest { mockResponse.setParse(null) `when`(mediaInterface!!.getPageHtml(ArgumentMatchers.anyString())) - .thenReturn(Observable.just(mockResponse)) + .thenReturn(Observable.just(mockResponse)) assertEquals("", mediaClient!!.getPageHtml("abcde").blockingGet()) } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/utils/NetworkUtilsTest.java b/app/src/test/kotlin/fr/free/nrw/commons/utils/NetworkUtilsTest.java index 80622f5cb..32a8791ab 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/utils/NetworkUtilsTest.java +++ b/app/src/test/kotlin/fr/free/nrw/commons/utils/NetworkUtilsTest.java @@ -6,6 +6,7 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.telephony.TelephonyManager; +import org.jetbrains.annotations.NotNull; import org.junit.Before; import org.junit.Test; @@ -28,34 +29,30 @@ public class NetworkUtilsTest { @Test public void testInternetConnectionEstablished() { - Context mockContext = mock(Context.class); - Application mockApplication = mock(Application.class); - ConnectivityManager mockConnectivityManager = mock(ConnectivityManager.class); - NetworkInfo mockNetworkInfo = mock(NetworkInfo.class); - when(mockNetworkInfo.isConnectedOrConnecting()) - .thenReturn(true); - when(mockConnectivityManager.getActiveNetworkInfo()) - .thenReturn(mockNetworkInfo); - when(mockApplication.getSystemService(Context.CONNECTIVITY_SERVICE)) - .thenReturn(mockConnectivityManager); - when(mockContext.getApplicationContext()).thenReturn(mockApplication); + Context mockContext = getContext(true); boolean internetConnectionEstablished = NetworkUtils.isInternetConnectionEstablished(mockContext); assertTrue(internetConnectionEstablished); } - @Test - public void testInternetConnectionNotEstablished() { + @NotNull + public static Context getContext(boolean connectionEstablished) { Context mockContext = mock(Context.class); Application mockApplication = mock(Application.class); ConnectivityManager mockConnectivityManager = mock(ConnectivityManager.class); NetworkInfo mockNetworkInfo = mock(NetworkInfo.class); when(mockNetworkInfo.isConnectedOrConnecting()) - .thenReturn(false); + .thenReturn(connectionEstablished); when(mockConnectivityManager.getActiveNetworkInfo()) - .thenReturn(mockNetworkInfo); + .thenReturn(mockNetworkInfo); when(mockApplication.getSystemService(Context.CONNECTIVITY_SERVICE)) - .thenReturn(mockConnectivityManager); + .thenReturn(mockConnectivityManager); when(mockContext.getApplicationContext()).thenReturn(mockApplication); + return mockContext; + } + + @Test + public void testInternetConnectionNotEstablished() { + Context mockContext = getContext(false); boolean internetConnectionEstablished = NetworkUtils.isInternetConnectionEstablished(mockContext); assertFalse(internetConnectionEstablished); } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/utils/PagedListMock.kt b/app/src/test/kotlin/fr/free/nrw/commons/utils/PagedListMock.kt new file mode 100644 index 000000000..f4a42fa91 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/utils/PagedListMock.kt @@ -0,0 +1,74 @@ +package fr.free.nrw.commons.utils + + +import android.database.Cursor +import androidx.lifecycle.LiveData +import androidx.paging.DataSource +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList +import androidx.room.InvalidationTracker +import androidx.room.RoomDatabase +import androidx.room.RoomSQLiteQuery +import androidx.room.paging.LimitOffsetDataSource +import com.nhaarman.mockitokotlin2.whenever +import org.mockito.Mockito.mock + +fun List.asPagedList(config: PagedList.Config? = null): LiveData> { + val defaultConfig = PagedList.Config.Builder() + .setEnablePlaceholders(false) + .setPageSize(size) + .setMaxSize(size + 2) + .setPrefetchDistance(1) + .build() + return LivePagedListBuilder( + createMockDataSourceFactory(this), + config ?: defaultConfig + ).build() +} + +/** + * Provides a mocked instance of the data source factory + */ +fun createMockDataSourceFactory(itemList: List): DataSource.Factory = + object : DataSource.Factory() { + override fun create(): DataSource = MockLimitDataSource(itemList) + } + +/** + * Provides a mocked Room SQL query + */ +private fun mockQuery(): RoomSQLiteQuery? { + val query = mock(RoomSQLiteQuery::class.java); + whenever(query.sql).thenReturn(""); + return query; +} + +/** + * Provides a mocked Room DB + */ +private fun mockDb(): RoomDatabase? { + val roomDatabase = mock(RoomDatabase::class.java); + val invalidationTracker = mock(InvalidationTracker::class.java) + whenever(roomDatabase.invalidationTracker).thenReturn(invalidationTracker); + return roomDatabase; +} + +/** + * Class that defines the mocked data source + */ +class MockLimitDataSource(private val itemList: List) : + LimitOffsetDataSource(mockDb(), mockQuery(), false, null) { + override fun convertRows(cursor: Cursor?): MutableList = itemList.toMutableList() + override fun countItems(): Int = itemList.count() + override fun isInvalid(): Boolean = false + override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) { + } + + override fun loadRange(startPosition: Int, loadCount: Int): MutableList { + return itemList.subList(startPosition, startPosition + loadCount).toMutableList() + } + + override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { + callback.onResult(itemList, 0) + } +} \ No newline at end of file