mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-30 22:34:02 +01:00 
			
		
		
		
	With lazy loading of contributions (#3566)
This commit is contained in:
		
							parent
							
								
									c216fdf0d4
								
							
						
					
					
						commit
						d863a404f1
					
				
					 34 changed files with 1397 additions and 928 deletions
				
			
		|  | @ -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()); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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()); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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() | ||||
|                 } | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -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)); | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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); | ||||
|   } | ||||
| } | ||||
|  | @ -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); | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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()); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -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); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -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"; | ||||
|  |  | |||
|  | @ -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); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Vivek Maskara
						Vivek Maskara