With lazy loading of contributions (#3566)

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

View file

@ -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"

View file

@ -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<String> 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<Media> CREATOR = new Creator<Media>() {
@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<String> 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<String> 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<String> 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<String> 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.
* </p>
@ -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<String> categories) {
public void setCategories(final List<String> 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<String> 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<Media> CREATOR = new Creator<Media>() {
@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());
}
}

View file

@ -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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

@ -32,6 +32,7 @@ public class MediaClient {
//OkHttpJsonApiClient used JsonKvStore for this. I don't know why.
private Map<String, Map<String, String>> continuationStore;
private Map<String, Boolean> 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<List<Media>> getMediaListForUser(String userName) {
Map<String, String> 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)

View file

@ -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;

View file

@ -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<MwQueryResponse> getMediaListFromCategory(@Query("gcmtitle") String category, @Query("gcmlimit") int itemLimit, @QueryMap Map<String, String> 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<MwQueryResponse> getMediaListForUser(@Query("gaiuser") String username,
@Query("gailimit") int itemLimit, @QueryMap(encoded = true) Map<String, String> continuation);
/**
* This method retrieves a list of Media objects filtered using image generator query
*
@ -86,21 +98,15 @@ public interface MediaInterface {
Observable<MwParseResponse> 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<MwQueryResponse> 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<DepictionResponse> fetchImagesForDepictedItem(@Query("srsearch") String query, @Query("sroffset") String sroffset);
@GET("w/api.php?action=query&list=search&format=json&srnamespace=6")
Observable<DepictionResponse> fetchImagesForDepictedItem(@Query("srsearch") String query,
@Query("sroffset") String sroffset);
}

View file

@ -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";

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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

View file

@ -26,13 +26,6 @@
android:summary="@string/use_external_storage_summary"
android:title="@string/use_external_storage" />
<EditTextPreference
android:defaultValue="100"
android:key="uploads"
app:useSimpleSummaryProvider="true"
app:singleLineTitle="false"
android:title="@string/set_limit" />
<ListPreference
android:key="descriptionDefaultLanguagePref"
app:useSimpleSummaryProvider="true"

View file

@ -0,0 +1,139 @@
package fr.free.nrw.commons.contributions
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.utils.NetworkUtilsTest
import fr.free.nrw.commons.utils.createMockDataSourceFactory
import io.reactivex.Scheduler
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import io.reactivex.schedulers.TestScheduler
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers.*
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import java.lang.RuntimeException
import java.util.*
/**
* The unit test class for ContributionBoundaryCallbackTest
*/
class ContributionBoundaryCallbackTest {
@Mock
internal lateinit var repository: ContributionsRepository
@Mock
internal lateinit var sessionManager: SessionManager
@Mock
internal lateinit var mediaClient: MediaClient
private lateinit var contributionBoundaryCallback: ContributionBoundaryCallback
@Rule
@JvmField
var instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var scheduler: Scheduler
/**
* initial setup
*/
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
scheduler = Schedulers.trampoline()
contributionBoundaryCallback =
ContributionBoundaryCallback(repository, sessionManager, mediaClient, scheduler);
}
@Test
fun testOnZeroItemsLoaded() {
whenever(repository.save(anyList<Contribution>()))
.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<Contribution>());
verify(mediaClient).getMediaListForUser(anyString());
}
@Test
fun testOnLastItemLoaded() {
whenever(repository.save(anyList<Contribution>()))
.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<Contribution>()))
.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<Contribution>()))
.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());
}
}

View file

@ -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<Contribution>()))
.thenReturn(Completable.complete())
contributionsListPresenter.deleteUpload(mock(Contribution::class.java))
verify(repository, times(1))
.deleteContributionFromDB(ArgumentMatchers.any(Contribution::class.java));
}
}

View file

@ -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<Contribution>()))
.thenReturn(Completable.complete())
contributionsPresenter.deleteUpload(contribution)
verify(repository).deleteContributionFromDB(contribution)
}

View file

@ -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
}
}
}

View file

@ -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<String>? = 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<Map<String, String>>? = 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<String, String>()
)
}
@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())
}

View file

@ -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);
}

View file

@ -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 <T> List<T>.asPagedList(config: PagedList.Config? = null): LiveData<PagedList<T>> {
val defaultConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(size)
.setMaxSize(size + 2)
.setPrefetchDistance(1)
.build()
return LivePagedListBuilder<Int, T>(
createMockDataSourceFactory(this),
config ?: defaultConfig
).build()
}
/**
* Provides a mocked instance of the data source factory
*/
fun <T> createMockDataSourceFactory(itemList: List<T>): DataSource.Factory<Int, T> =
object : DataSource.Factory<Int, T>() {
override fun create(): DataSource<Int, T> = 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<T>(private val itemList: List<T>) :
LimitOffsetDataSource<T>(mockDb(), mockQuery(), false, null) {
override fun convertRows(cursor: Cursor?): MutableList<T> = itemList.toMutableList()
override fun countItems(): Int = itemList.count()
override fun isInvalid(): Boolean = false
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) {
}
override fun loadRange(startPosition: Int, loadCount: Int): MutableList<T> {
return itemList.subList(startPosition, startPosition + loadCount).toMutableList()
}
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>) {
callback.onResult(itemList, 0)
}
}