diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index d18f5be8cfe..936d6e5a6e5 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -7,6 +7,7 @@
+
diff --git a/app/build.gradle b/app/build.gradle
index 31dafb6d1f2..596560b4410 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -19,10 +19,9 @@ dependencies {
implementation project(':wikimedia-data-client')
// Utils
- implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
implementation 'in.yuvi:http.fluent:1.3'
implementation 'com.google.code.gson:gson:2.8.5'
- implementation 'com.squareup.okhttp3:okhttp:4.2.0'
+ implementation 'com.squareup.okhttp3:okhttp:4.5.0'
implementation 'com.squareup.okio:okio:2.2.2'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
@@ -44,6 +43,7 @@ dependencies {
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
implementation 'com.karumi:dexter:5.0.0'
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
+
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
// Logging
@@ -53,7 +53,7 @@ dependencies {
api('com.github.tony19:logback-android-classic:1.1.1-6') {
exclude group: 'com.google.android', module: 'android'
}
- implementation "com.squareup.okhttp3:logging-interceptor:4.2.0"
+ implementation "com.squareup.okhttp3:logging-interceptor:4.5.0"
// Dependency injector
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
@@ -65,7 +65,7 @@ dependencies {
//Mocking
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
- testImplementation 'org.mockito:mockito-inline:2.8.47'
+ testImplementation 'org.mockito:mockito-inline:2.13.0'
testImplementation 'org.mockito:mockito-core:2.23.0'
testImplementation "org.powermock:powermock-module-junit4:2.0.0-beta.5"
testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5"
@@ -108,9 +108,10 @@ dependencies {
//Room
implementation "androidx.room:room-runtime:$ROOM_VERSION"
- kapt "androidx.room:room-compiler:$ROOM_VERSION" // For Kotlin use kapt instead of annotationProcessor
- implementation 'com.squareup.retrofit2:retrofit:2.7.1'
+ implementation "androidx.room:room-ktx:$ROOM_VERSION"
implementation "androidx.room:room-rxjava2:$ROOM_VERSION"
+ kapt "androidx.room:room-compiler:$ROOM_VERSION" // For Kotlin use kapt instead of annotationProcessor
+ implementation 'com.squareup.retrofit2:retrofit:2.8.1'
testImplementation "androidx.arch.core:core-testing:2.1.0"
// Pref
@@ -208,6 +209,7 @@ android {
buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
+ buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json\""
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
@@ -229,6 +231,7 @@ android {
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
buildConfigField "String", "TEST_USERNAME", "\"" + System.getenv("test_user_name") + "\""
buildConfigField "String", "TEST_PASSWORD", "\"" + System.getenv("test_user_password") + "\""
+ buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\""
dimension 'tier'
}
@@ -240,6 +243,7 @@ android {
buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\""
buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
+ buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns_beta_active.json\""
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
@@ -261,6 +265,7 @@ android {
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
buildConfigField "String", "TEST_USERNAME", "\"" + System.getenv("test_user_name") + "\""
buildConfigField "String", "TEST_PASSWORD", "\"" + System.getenv("test_user_password") + "\""
+ buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\""
dimension 'tier'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e0d15b7323c..e0cf4d03c92 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -15,7 +15,9 @@
+
+
@@ -118,12 +120,12 @@
android:parentActivityName=".contributions.MainActivity" />
@@ -137,9 +139,8 @@
/>
+ android:name=".achievements.AchievementsActivity"
+ android:label="@string/Achievements" />
categories; // as loaded at runtime?
+ /**
+ * Depicts is a feature part of Structured data. Multiple Depictions can be added for an image just like categories.
+ * However unlike categories depictions is multi-lingual
+ */
+ private Depictions depictions;
+ private boolean requestedDeletion;
+ @Nullable private LatLng coordinates;
+
+ /**
+ * Provides local constructor
+ */
+ public Media() {
+ }
+
+ /**
+ * Provides a minimal constructor
+ *
+ * @param filename Media filename
+ */
+ public Media(String filename) {
+ this.filename = filename;
+ }
+
+ /**
+ * Provide Media constructor
+ * @param localUri Media URI
+ * @param imageUrl Media image URL
+ * @param filename Media filename
+ * @param description Media description
+ * @param dataLength Media date length
+ * @param dateCreated Media creation date
+ * @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) {
+ this.localUri = localUri;
+ this.thumbUrl = imageUrl;
+ this.imageUrl = imageUrl;
+ this.filename = filename;
+ this.description = description;
+ this.dataLength = dataLength;
+ this.dateCreated = dateCreated;
+ this.dateUploaded = dateUploaded;
+ this.creator = creator;
+ }
+
+ public Media(Uri localUri, String filename,
+ String description, String creator, List categories) {
+ this(localUri,null, filename,
+ description, -1, null, new Date(), creator);
+ this.categories = categories;
+ }
+
+ public Media(String title, Date date, String user) {
+ this(null, null, title, "", -1, date, date, user);
+ }
+
+ /**
+ * Creating Media object from MWQueryPage.
+ * Earlier only basic details were set for the media object but going forward,
+ * a full media object(with categories, descriptions, coordinates etc) can be constructed using this method
+ *
+ * @param page response from the API
+ * @return Media object
+ */
+ @Nullable
+ public static Media from(MwQueryPage page) {
+ ImageInfo imageInfo = page.imageInfo();
+ if (imageInfo == null) {
+ return new Media(); // null is not allowed
+ }
+ ExtMetadata metadata = imageInfo.getMetadata();
+ if (metadata == null) {
+ Media media = new Media(null, imageInfo.getOriginalUrl(),
+ page.title(), "", 0, null, null, null);
+ if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
+ media.setThumbUrl(imageInfo.getThumbUrl());
+ }
+ return media;
+ }
+
+ Media media = new Media(null,
+ imageInfo.getOriginalUrl(),
+ page.title(),
+ "",
+ 0,
+ safeParseDate(metadata.dateTime()),
+ safeParseDate(metadata.dateTime()),
+ getArtist(metadata)
+ );
+
+ if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
+ media.setThumbUrl(imageInfo.getThumbUrl());
+ }
+
+ media.setPageId(String.valueOf(page.pageId()));
+
+ String language = Locale.getDefault().getLanguage();
+ if (StringUtils.isBlank(language)) {
+ language = "default";
+ }
+
+ media.setDescription(metadata.imageDescription());
+ media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories()));
+ String latitude = metadata.getGpsLatitude();
+ String longitude = metadata.getGpsLongitude();
+
+ if (!StringUtils.isBlank(latitude) && !StringUtils.isBlank(longitude)) {
+ LatLng latLng = new LatLng(Double.parseDouble(latitude), Double.parseDouble(longitude), 0);
+ media.setCoordinates(latLng);
+ }
+
+ media.setLicenseInformation(metadata.licenseShortName(), metadata.licenseUrl());
+ return media;
+ }
+
+ /**
+ * This method extracts the Commons Username from the artist HTML information
+ * @param metadata
+ * @return
+ */
+ private static String getArtist(ExtMetadata metadata) {
+ try {
+ String artistHtml = metadata.artist();
+ return artistHtml.substring(artistHtml.indexOf("title=\""), artistHtml.indexOf("\">"))
+ .replace("title=\"User:", "");
+ } catch (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;
+ }
+
+ public String getThumbUrl() {
+ return thumbUrl;
+ }
+
+ /**
+ * Gets media display title
+ * @return Media title
+ */
+ @NonNull public String getDisplayTitle() {
+ 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;
+ }
+
+ /**
+ * @return title to be shown on image thumbnail
+ * If caption is available for the image then it returns caption else filename
+ */
+ public String getThumbnailTitle() {
+ return thumbnailTitle != null? thumbnailTitle : getDisplayTitle();
+ }
+
+ /**
+ * Gets file page title
+ * @return New media page title
+ */
+ @NonNull public PageTitle getPageTitle() {
+ return Utils.getPageTitle(getFilename());
+ }
+
+ /**
+ * Gets local URI
+ * @return Media local URI
+ */
+ public Uri getLocalUri() {
+ return localUri;
+ }
+
+ /**
+ * Gets image URL
+ * can be null.
+ * @return Image URL
+ */
+ @Nullable
+ public String getImageUrl() {
+ return imageUrl;
+ }
+
+ /**
+ * Gets the name of the file.
+ * @return file name as a string
+ */
+ public String getFilename() {
+ return filename;
+ }
+
+ /**
+ * Sets the name of the file.
+ * @param filename the new name of the file
+ */
+ public void setFilename(String filename) {
+ this.filename = filename;
+ }
+
+ /**
+ * Sets the discussion of the file.
+ * @param discussion
+ */
+ public void setDiscussion(String discussion) {
+ this.discussion = discussion;
+ }
+
+ /**
+ * Gets the file discussion as a string.
+ * @return file discussion as a string
+ */
+ public String getDiscussion() {
+ return discussion;
+ }
+
+ /**
+ * Gets the file description.
+ * @return file description as a string
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * 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
+ *
+ * @return caption
+ */
+ public String getCaption() {
+ return caption;
+ }
+
+ /**
+ * @return depictions associated with the current media
+ */
+ public Depictions getDepiction() {
+ return depictions;
+ }
+
+ /**
+ * Sets the file description.
+ * @param description the new description of the file
+ */
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ /**
+ * Gets the dataLength of the file.
+ * @return file dataLength as a long
+ */
+ public long getDataLength() {
+ return dataLength;
+ }
+
+ /**
+ * Sets the dataLength of the file.
+ * @param dataLength as a long
+ */
+ public void setDataLength(long dataLength) {
+ this.dataLength = dataLength;
+ }
+
+ /**
+ * Gets the creation date of the file.
+ * @return creation date as a Date
+ */
+ public Date getDateCreated() {
+ return dateCreated;
+ }
+
+ /**
+ * Sets the creation date of the file.
+ * @param date creation date as a Date
+ */
+ public void setDateCreated(Date date) {
+ this.dateCreated = date;
+ }
+
+ /**
+ * Gets the upload date of the file.
+ * Can be null.
+ * @return upload date as a Date
+ */
+ public @Nullable
+ Date getDateUploaded() {
+ return dateUploaded;
+ }
+
+ /**
+ * Gets the name of the creator of the file.
+ * @return creator name as a String
+ */
+ public String getCreator() {
+ return creator;
+ }
+
+ /**
+ * Sets the creator name of the file.
+ * @param creator creator name as a string
+ */
+ public void setCreator(String creator) {
+ this.creator = creator;
+ }
+
+ /**
+ * Gets the license name of the file.
+ * @return license as a String
+ */
+ public String getLicense() {
+ return license;
+ }
+
+ public void setThumbUrl(String thumbUrl) {
+ this.thumbUrl = thumbUrl;
+ }
+
+ public String getLicenseUrl() {
+ return licenseUrl;
+ }
+
+ /**
+ * Sets the license name of the file.
+ * @param license license 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;
+ }
+
+ /**
+ * Gets the coordinates of where the file was created.
+ * @return file coordinates as a LatLng
+ */
+ public @Nullable
+ LatLng getCoordinates() {
+ 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;
+ }
+
+ /**
+ * Gets the categories the file falls under.
+ * @return file categories as an ArrayList of Strings
+ */
+ @SuppressWarnings("unchecked")
+ public List getCategories() {
+ return categories;
+ }
+
+ /**
+ * Sets the categories the file falls under.
+ *
+ * Does not append: i.e. will clear the current categories
+ * and then add the specified ones.
+ * @param categories file categories as a list of Strings
+ */
+ public void setCategories(List categories) {
+ this.categories = categories;
+ }
+
+ @Nullable private static Date safeParseDate(String dateStr) {
+ try {
+ return CommonsDateUtil.getIso8601DateFormatShort().parse(dateStr);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Set requested deletion to true
+ * @param requestedDeletion
+ */
+ public void setRequestedDeletion(boolean requestedDeletion){
+ this.requestedDeletion = requestedDeletion;
+ }
+
+ /**
+ * Get the value of requested deletion
+ * @return boolean requestedDeletion
+ */
+ public boolean isRequestedDeletion(){
+ return requestedDeletion;
+ }
+
+ /**
+ * Sets the license name of the file.
+ *
+ * @param license license name as a String
+ */
+ public void setLicense(String license) {
+ this.license = license;
+ }
+
+ /**
+ * 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
+ *
+ * This function sets captions
+ * @param caption
+ */
+ public void setCaption(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;
+ }
+
+ public void setImageUrl(final String imageUrl) {
+ this.imageUrl = imageUrl;
+ }
+
+ public void setDateUploaded(@Nullable final Date dateUploaded) {
+ this.dateUploaded = dateUploaded;
+ }
+
+ public void setLicenseUrl(final String licenseUrl) {
+ this.licenseUrl = licenseUrl;
+ }
+
+ public Depictions getDepictions() {
+ return depictions;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Creates a way to transfer information between two or more
+ * activities.
+ * @param dest Instance of Parcel
+ * @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);
+ }
+
+ protected Media(Parcel in) {
+ this.localUri = in.readParcelable(Uri.class.getClassLoader());
+ this.thumbUrl = in.readString();
+ this.imageUrl = in.readString();
+ this.filename = in.readString();
+ this.thumbnailTitle = in.readString();
+ this.caption = in.readString();
+ this.description = in.readString();
+ this.discussion = in.readString();
+ this.dataLength = in.readLong();
+ long tmpDateCreated = in.readLong();
+ this.dateCreated = tmpDateCreated == -1 ? null : new Date(tmpDateCreated);
+ long tmpDateUploaded = in.readLong();
+ this.dateUploaded = tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded);
+ this.license = in.readString();
+ this.licenseUrl = in.readString();
+ this.creator = in.readString();
+ this.pageId = in.readString();
+ final ArrayList list = new ArrayList<>();
+ in.readStringList(list);
+ this.categories=list;
+ in.readParcelable(Depictions.class.getClassLoader());
+ this.requestedDeletion = in.readByte() != 0;
+ this.coordinates = in.readParcelable(LatLng.class.getClassLoader());
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public Media createFromParcel(Parcel source) {
+ return new Media(source);
+ }
+
+ @Override
+ public Media[] newArray(int size) {
+ return new Media[size];
+ }
+ };
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java
new file mode 100644
index 00000000000..58b6be2c9e4
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java
@@ -0,0 +1,101 @@
+package fr.free.nrw.commons;
+
+import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
+
+import androidx.core.text.HtmlCompat;
+import fr.free.nrw.commons.media.Depictions;
+import fr.free.nrw.commons.media.MediaClient;
+import io.reactivex.Single;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.jetbrains.annotations.NotNull;
+import timber.log.Timber;
+
+/**
+ * Fetch additional media data from the network that we don't store locally.
+ *
+ * This includes things like category lists and multilingual descriptions,
+ * which are not intrinsic to the media and may change due to editing.
+ */
+@Singleton
+public class MediaDataExtractor {
+
+ private final MediaClient mediaClient;
+
+ @Inject
+ public MediaDataExtractor(final MediaClient mediaClient) {
+ this.mediaClient = mediaClient;
+ }
+
+ /**
+ * Simplified method to extract all details required to show media details.
+ * It fetches media object, deletion status, talk page and captions for the filename
+ * @param filename for which the details are to be fetched
+ * @return full Media object with all details including deletion status and talk page
+ */
+ public Single fetchMediaDetails(final String filename, final String pageId) {
+ return Single.zip(getMediaFromFileName(filename),
+ mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + filename),
+ getDiscussion(filename),
+ pageId != null ? getCaption(PAGE_ID_PREFIX + pageId)
+ : Single.just(MediaClient.NO_CAPTION),
+ getDepictions(filename),
+ this::combineToMedia);
+ }
+
+ @NotNull
+ private Media combineToMedia(final Media media, final Boolean deletionStatus, final String discussion,
+ final String caption, final Depictions depictions) {
+ media.setDiscussion(discussion);
+ media.setCaption(caption);
+ media.setDepictions(depictions);
+ if (deletionStatus) {
+ media.setRequestedDeletion(true);
+ }
+ return media;
+ }
+
+ /**
+ * Obtains captions using filename
+ * @param wikibaseIdentifier
+ *
+ * @return caption for the image in user's locale
+ * Ex: "a nice painting" (english locale) and "No Caption" in case the caption is not available for the image
+ */
+ private Single getCaption(final String wikibaseIdentifier) {
+ return mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier);
+ }
+
+ /**
+ * Fetch depictions from the MediaWiki API
+ * @param filename the filename we will return the caption for
+ * @return Depictions
+ */
+ private Single getDepictions(final String filename) {
+ return mediaClient.getDepictions(filename)
+ .doOnError(throwable -> Timber.e(throwable, "error while fetching depictions"));
+ }
+
+ /**
+ * Method can be used to fetch media for a given filename
+ * @param filename Eg. File:Test.jpg
+ * @return return data rich Media object
+ */
+ public Single getMediaFromFileName(final String filename) {
+ return mediaClient.getMedia(filename);
+ }
+
+ /**
+ * Fetch talk page from the MediaWiki API
+ * @param filename
+ * @return
+ */
+ private Single getDiscussion(final String filename) {
+ return mediaClient.getPageHtml(filename.replace("File", "File talk"))
+ .map(discussion -> HtmlCompat.fromHtml(discussion, HtmlCompat.FROM_HTML_MODE_LEGACY).toString())
+ .onErrorReturn(throwable -> {
+ Timber.e(throwable, "Error occurred while fetching discussion");
+ return "";
+ });
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.java b/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.java
index 9d3bde848ec..b9c10496ccb 100644
--- a/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.java
+++ b/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.java
@@ -1,6 +1,11 @@
package fr.free.nrw.commons;
import androidx.annotation.NonNull;
+
+import okhttp3.logging.HttpLoggingInterceptor.Level;
+import org.wikipedia.dataclient.SharedPreferenceCookieManager;
+import org.wikipedia.dataclient.okhttp.HttpStatusException;
+
import java.io.File;
import java.io.IOException;
import okhttp3.Cache;
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java
index 7070fa40b60..e83edfac40b 100644
--- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java
@@ -1,65 +1,71 @@
-package fr.free.nrw.commons.bookmarks.pictures;
+package fr.free.nrw.commons.category;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
+import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ListAdapter;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import butterknife.BindView;
import butterknife.ButterKnife;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.bookmarks.BookmarksActivity;
-import fr.free.nrw.commons.category.GridViewAdapter;
+import fr.free.nrw.commons.kvstore.JsonKvStore;
+import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
+import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
+import javax.inject.Named;
import timber.log.Timber;
-public class BookmarkPicturesFragment extends DaggerFragment {
+/**
+ * Displays images for a particular category with load more on scrolling incorporated
+ */
+public class CategoryImagesListFragment extends DaggerFragment {
+
+ private static int TIMEOUT_SECONDS = 15;
+ /**
+ * counts the total number of items loaded from the API
+ */
+ private int mediaSize = 0;
private GridViewAdapter gridAdapter;
- private CompositeDisposable compositeDisposable = new CompositeDisposable();
- @BindView(R.id.statusMessage) TextView statusTextView;
+ @BindView(R.id.statusMessage)
+ TextView statusTextView;
@BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar;
- @BindView(R.id.bookmarkedPicturesList) GridView gridView;
+ @BindView(R.id.categoryImagesList) GridView gridView;
@BindView(R.id.parentLayout) RelativeLayout parentLayout;
+ private CompositeDisposable compositeDisposable = new CompositeDisposable();
+ private boolean hasMoreImages = true;
+ private boolean isLoading = true;
+ private String categoryName = null;
+ @Inject MediaClient mediaClient;
@Inject
- BookmarkPicturesController controller;
-
- /**
- * Create an instance of the fragment with the right bundle parameters
- * @return an instance of the fragment
- */
- public static BookmarkPicturesFragment newInstance() {
- return new BookmarkPicturesFragment();
- }
+ @Named("default_preferences")
+ JsonKvStore categoryKvStore;
@Override
- public View onCreateView(
- @NonNull LayoutInflater inflater,
- ViewGroup container,
- Bundle savedInstanceState
- ) {
- View v = inflater.inflate(R.layout.fragment_bookmarks_pictures, container, false);
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.fragment_category_images, container, false);
ButterKnife.bind(this, v);
return v;
}
@@ -68,13 +74,7 @@ public View onCreateView(
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
- initList();
- }
-
- @Override
- public void onStop() {
- super.onStop();
- controller.stop();
+ initViews();
}
@Override
@@ -83,22 +83,31 @@ public void onDestroy() {
compositeDisposable.clear();
}
- @Override
- public void onResume() {
- super.onResume();
- if (controller.needRefreshBookmarkedPictures()) {
- gridView.setVisibility(GONE);
- if (gridAdapter != null) {
- gridAdapter.clear();
- ((BookmarksActivity) getContext()).viewPagerNotifyDataSetChanged();
- }
+ /**
+ * Initializes the UI elements for the fragment
+ * Setup the grid view to and scroll listener for it
+ */
+ private void initViews() {
+ String categoryName = getArguments().getString("categoryName");
+ if (getArguments() != null && categoryName != null) {
+ this.categoryName = categoryName;
+ resetQueryContinueValues(categoryName);
initList();
+ setScrollListener();
}
}
/**
- * Checks for internet connection and then initializes
- * the recycler view with bookmarked pictures
+ * Query continue values determine the last page that was loaded for the particular keyword
+ * This method resets those values, so that the results can be queried from the first page itself
+ * @param keyword
+ */
+ private void resetQueryContinueValues(String keyword) {
+ categoryKvStore.remove("query_continue_" + keyword);
+ }
+
+ /**
+ * Checks for internet connection and then initializes the grid view with first 10 images of that category
*/
@SuppressLint("CheckResult")
private void initList() {
@@ -107,12 +116,12 @@ private void initList() {
return;
}
+ isLoading = true;
progressBar.setVisibility(VISIBLE);
- statusTextView.setVisibility(GONE);
-
- compositeDisposable.add(controller.loadBookmarkedPictures()
+ compositeDisposable.add(mediaClient.getMediaListFromCategory(categoryName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(this::handleSuccess, this::handleError));
}
@@ -141,6 +150,7 @@ private void handleError(Throwable throwable) {
}catch (Exception e){
e.printStackTrace();
}
+
}
/**
@@ -157,16 +167,68 @@ private void initErrorView() {
}
/**
- * Handles the UI updates when there is no bookmarks
+ * Initializes the adapter with a list of Media objects
+ * @param mediaList List of new Media to be displayed
*/
- private void initEmptyBookmarkListView() {
- progressBar.setVisibility(GONE);
- if (gridAdapter == null || gridAdapter.isEmpty()) {
- statusTextView.setVisibility(VISIBLE);
- statusTextView.setText(getString(R.string.bookmark_empty));
- } else {
- statusTextView.setVisibility(GONE);
+ private void setAdapter(List mediaList) {
+ gridAdapter = new GridViewAdapter(this.getContext(), R.layout.layout_category_images, mediaList);
+ gridView.setAdapter(gridAdapter);
+ }
+
+ /**
+ * Sets the scroll listener for the grid view so that more images are fetched when the user scrolls down
+ * Checks if the category has more images before loading
+ * Also checks whether images are currently being fetched before triggering another request
+ */
+ private void setScrollListener() {
+ gridView.setOnScrollListener(new AbsListView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ if (hasMoreImages && !isLoading && (firstVisibleItem + visibleItemCount + 1 >= totalItemCount)) {
+ isLoading = true;
+ fetchMoreImages();
+ }
+ if (!hasMoreImages){
+ progressBar.setVisibility(GONE);
+ }
+ }
+ });
+ }
+
+ /**
+ * This method is called when viewPager has reached its end.
+ * Fetches more images for the category and adds it to the grid view and viewpager adapter
+ */
+ public void fetchMoreImagesViewPager(){
+ if (hasMoreImages && !isLoading) {
+ isLoading = true;
+ fetchMoreImages();
}
+ if (!hasMoreImages){
+ progressBar.setVisibility(GONE);
+ }
+ }
+
+ /**
+ * Fetches more images for the category and adds it to the grid view adapter
+ */
+ @SuppressLint("CheckResult")
+ private void fetchMoreImages() {
+ if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
+ handleNoInternet();
+ return;
+ }
+
+ progressBar.setVisibility(VISIBLE);
+ compositeDisposable.add(mediaClient.getMediaListFromCategory(categoryName)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .subscribe(this::handleSuccess, this::handleError));
}
/**
@@ -175,12 +237,9 @@ private void initEmptyBookmarkListView() {
* @param collection List of new Media to be displayed
*/
private void handleSuccess(List collection) {
- if (collection == null) {
+ if (collection == null || collection.isEmpty()) {
initErrorView();
- return;
- }
- if (collection.isEmpty()) {
- initEmptyBookmarkListView();
+ hasMoreImages = false;
return;
}
@@ -188,27 +247,48 @@ private void handleSuccess(List collection) {
setAdapter(collection);
} else {
if (gridAdapter.containsAll(collection)) {
+ hasMoreImages = false;
return;
}
gridAdapter.addItems(collection);
- ((BookmarksActivity) getContext()).viewPagerNotifyDataSetChanged();
+ ((CategoryImagesCallback) getContext()).viewPagerNotifyDataSetChanged();
}
+
progressBar.setVisibility(GONE);
+ isLoading = false;
statusTextView.setVisibility(GONE);
- gridView.setVisibility(VISIBLE);
+ for (Media m : collection) {
+ final String pageId = m.getPageId();
+ if (pageId != null) {
+ replaceTitlesWithCaptions(PAGE_ID_PREFIX + pageId, mediaSize++);
+ }
+ }
}
/**
- * Initializes the adapter with a list of Media objects
- * @param mediaList List of new Media to be displayed
+ * fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available)
+ * else show filename
*/
- private void setAdapter(List mediaList) {
- gridAdapter = new GridViewAdapter(
- this.getContext(),
- R.layout.layout_category_images,
- mediaList
- );
- gridView.setAdapter(gridAdapter);
+ public void replaceTitlesWithCaptions(String wikibaseIdentifier, int i) {
+ compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .subscribe(subscriber -> {
+ handleLabelforImage(subscriber, i);
+ }));
+
+ }
+
+ /**
+ * If caption is available for the image, then modify grid adapter
+ * to show captions
+ */
+ private void handleLabelforImage(String s, int position) {
+ if (!s.trim().equals(getString(R.string.detail_caption_empty))) {
+ gridAdapter.getItem(position).setThumbnailTitle(s);
+ gridAdapter.notifyDataSetChanged();
+ }
}
/**
@@ -217,6 +297,7 @@ private void setAdapter(List mediaList) {
* @return GridView Adapter
*/
public ListAdapter getAdapter() {
- return gridView.getAdapter();
+ return gridAdapter;
}
+
}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java
new file mode 100644
index 00000000000..d8f9eb45e7d
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java
@@ -0,0 +1,242 @@
+package fr.free.nrw.commons.category;
+
+import android.text.TextUtils;
+import fr.free.nrw.commons.kvstore.JsonKvStore;
+import fr.free.nrw.commons.upload.GpsCategoryModel;
+import fr.free.nrw.commons.utils.StringSortingUtils;
+import io.reactivex.Observable;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import javax.inject.Inject;
+import javax.inject.Named;
+import timber.log.Timber;
+
+/**
+ * The model class for categories in upload
+ */
+public class CategoriesModel{
+ private static final int SEARCH_CATS_LIMIT = 25;
+
+ private final CategoryClient categoryClient;
+ private final CategoryDao categoryDao;
+ private final JsonKvStore directKvStore;
+ private final GpsCategoryModel gpsCategoryModel;
+
+ private List selectedCategories;
+
+ @Inject
+ public CategoriesModel(CategoryClient categoryClient,
+ CategoryDao categoryDao,
+ @Named("default_preferences") JsonKvStore directKvStore,
+ final GpsCategoryModel gpsCategoryModel) {
+ this.categoryClient = categoryClient;
+ this.categoryDao = categoryDao;
+ this.directKvStore = directKvStore;
+ this.gpsCategoryModel = gpsCategoryModel;
+ this.selectedCategories = new ArrayList<>();
+ }
+
+ /**
+ * Sorts CategoryItem by similarity
+ * @param filter
+ * @return
+ */
+ public Comparator sortBySimilarity(final String filter) {
+ Comparator stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
+ return (firstItem, secondItem) -> stringSimilarityComparator
+ .compare(firstItem.getName(), secondItem.getName());
+ }
+
+ /**
+ * Returns if the item contains an year
+ * @param item
+ * @return
+ */
+ public boolean containsYear(String item) {
+ //Check for current and previous year to exclude these categories from removal
+ Calendar now = Calendar.getInstance();
+ int year = now.get(Calendar.YEAR);
+ String yearInString = String.valueOf(year);
+
+ int prevYear = year - 1;
+ String prevYearInString = String.valueOf(prevYear);
+ Timber.d("Previous year: %s", prevYearInString);
+
+ //Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
+ //And that item does not equal the current year or previous year
+ //And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
+ //Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
+ return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
+ || item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")
+ || (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
+ }
+
+ /**
+ * Updates category count in category dao
+ * @param item
+ */
+ public void updateCategoryCount(CategoryItem item) {
+ Category category = categoryDao.find(item.getName());
+
+ // Newly used category...
+ if (category == null) {
+ category = new Category(null, item.getName(), new Date(), 0);
+ }
+
+ category.incTimesUsed();
+ categoryDao.save(category);
+ }
+
+
+ /**
+ * Regional category search
+ * @param term
+ * @param imageTitleList
+ * @return
+ */
+ public Observable searchAll(String term, List imageTitleList) {
+ //If query text is empty, show him category based on gps and title and recent searches
+ if (TextUtils.isEmpty(term)) {
+ Observable categoryItemObservable =
+ Observable.concat(gpsCategories(), titleCategories(imageTitleList));
+ if (hasDirectCategories()) {
+ return Observable.concat(
+ categoryItemObservable,
+ directCategories(),
+ recentCategories()
+ );
+ }
+ return categoryItemObservable;
+ }
+
+ //otherwise, search API for matching categories
+ //term passed as lower case to make search case-insensitive(taking only lower case for everything)
+ return categoryClient
+ .searchCategoriesForPrefix(term.toLowerCase(), SEARCH_CATS_LIMIT)
+ .map(name -> new CategoryItem(name, false));
+ }
+
+
+ /**
+ * Returns if we have a category in DirectKV Store
+ * @return
+ */
+ private boolean hasDirectCategories() {
+ return !directKvStore.getString("Category", "").equals("");
+ }
+
+ /**
+ * Returns categories in DirectKVStore
+ * @return
+ */
+ private Observable directCategories() {
+ String directCategory = directKvStore.getString("Category", "");
+ List categoryList = new ArrayList<>();
+ Timber.d("Direct category found: " + directCategory);
+
+ if (!directCategory.equals("")) {
+ categoryList.add(directCategory);
+ Timber.d("DirectCat does not equal emptyString. Direct Cat list has " + categoryList);
+ }
+ return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false));
+ }
+
+ /**
+ * Returns GPS categories
+ * @return
+ */
+ Observable gpsCategories() {
+ return Observable.fromIterable(gpsCategoryModel.getCategoryList())
+ .map(name -> new CategoryItem(name, false));
+ }
+
+ /**
+ * Returns title based categories
+ * @param titleList
+ * @return
+ */
+ private Observable titleCategories(List titleList) {
+ return Observable.fromIterable(titleList)
+ .concatMap(this::getTitleCategories);
+ }
+
+ /**
+ * Return category for single title
+ * title is converted to lower case to make search case-insensitive
+ * @param title
+ * @return
+ */
+ private Observable getTitleCategories(String title) {
+ return categoryClient.searchCategories(title.toLowerCase(), SEARCH_CATS_LIMIT)
+ .map(name -> new CategoryItem(name, false));
+ }
+
+ /**
+ * Returns recent categories
+ * @return
+ */
+ private Observable recentCategories() {
+ return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
+ .map(s -> new CategoryItem(s, false));
+ }
+
+ /**
+ * Handles category item selection
+ * @param item
+ */
+ public void onCategoryItemClicked(CategoryItem item) {
+ if (item.isSelected()) {
+ selectCategory(item);
+ updateCategoryCount(item);
+ } else {
+ unselectCategory(item);
+ }
+ }
+
+ /**
+ * Select's category
+ * @param item
+ */
+ public void selectCategory(CategoryItem item) {
+ selectedCategories.add(item);
+ }
+
+ /**
+ * Unselect Category
+ * @param item
+ */
+ public void unselectCategory(CategoryItem item) {
+ selectedCategories.remove(item);
+ }
+
+
+ /**
+ * Get Selected Categories
+ * @return
+ */
+ public List getSelectedCategories() {
+ return selectedCategories;
+ }
+
+ /**
+ * Get Categories String List
+ * @return
+ */
+ public List getCategoryStringList() {
+ List output = new ArrayList<>();
+ for (CategoryItem item : selectedCategories) {
+ output.add(item.getName());
+ }
+ return output;
+ }
+
+ /**
+ * Cleanup the existing in memory cache's
+ */
+ public void cleanUp() {
+ this.selectedCategories.clear();
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java
index e8aab51bc3e..c983af03afb 100644
--- a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java
+++ b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java
@@ -88,7 +88,7 @@ public View getView(int position, View convertView, ViewGroup parent) {
SimpleDraweeView imageView = convertView.findViewById(R.id.categoryImageView);
TextView fileName = convertView.findViewById(R.id.categoryImageTitle);
TextView author = convertView.findViewById(R.id.categoryImageAuthor);
- fileName.setText(item.getMostRelevantCaption());
+ fileName.setText(item.getThumbnailTitle());
setAuthorView(item, author);
imageView.setImageURI(item.getThumbUrl());
return convertView;
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java
index c67070acbb5..80c378137a1 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java
@@ -1,36 +1,22 @@
package fr.free.nrw.commons.contributions;
-import android.content.Context;
-import android.net.Uri;
import android.os.Parcel;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.StringDef;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.lang.annotation.Retention;
-import java.util.Date;
-import java.util.Locale;
-
-import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
-import fr.free.nrw.commons.filepicker.UploadableFile;
-import fr.free.nrw.commons.settings.Prefs;
-import fr.free.nrw.commons.utils.ConfigUtils;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
+import fr.free.nrw.commons.auth.SessionManager;
+import fr.free.nrw.commons.upload.UploadMediaDetail;
+import fr.free.nrw.commons.upload.UploadModel.UploadItem;
+import fr.free.nrw.commons.upload.WikidataPlace;
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.wikipedia.dataclient.mwapi.MwQueryLogEvent;
@Entity(tableName = "contribution")
-public class Contribution extends Media {
-
- //{{According to Exif data|2009-01-09}}
- private static final String TEMPLATE_DATE_ACC_TO_EXIF = "{{According to Exif data|%s}}";
-
- //2009-01-09 → 9 January 2009
- private static final String TEMPLATE_DATA_OTHER_SOURCE = "%s";
+public class Contribution extends Media {
// No need to be bitwise - they're mutually exclusive
public static final int STATE_COMPLETED = -1;
@@ -38,219 +24,133 @@ public class Contribution extends Media {
public static final int STATE_QUEUED = 2;
public static final int STATE_IN_PROGRESS = 3;
- @Retention(SOURCE)
- @StringDef({SOURCE_CAMERA, SOURCE_GALLERY, SOURCE_EXTERNAL})
- public @interface FileSource {}
-
- public static final String SOURCE_CAMERA = "camera";
- public static final String SOURCE_GALLERY = "gallery";
- public static final String SOURCE_EXTERNAL = "external";
@PrimaryKey (autoGenerate = true)
- @NonNull
- public long _id;
- public Uri contentUri;
- public String source;
- public String editSummary;
- public int state;
- public long transferred;
- public String decimalCoords;
- public boolean isMultiple;
- public String wikiDataEntityId;
- public String wikiItemName;
- private String p18Value;
- public Uri contentProviderUri;
- public String dateCreatedSource;
+ 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
+ */
+ private List depictedItems = new ArrayList<>();
+ private String mimeType;
+ /**
+ * This hasmap stores the list of multilingual captions, where
+ * key of the HashMap is the language and value is the caption in the corresponding language
+ * Ex: key = "en", value: ""
+ * key = "de" , value: ""
+ */
+ private Map captions = new HashMap<>();
- public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength,
- Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) {
- super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
- this.decimalCoords = decimalCoords;
- this.editSummary = editSummary;
- this.dateCreatedSource = "";
+ public Contribution() {
}
- public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength,
- Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords, int state) {
- super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
- this.decimalCoords = decimalCoords;
- this.editSummary = editSummary;
- this.dateCreatedSource = "";
- this.state=state;
+ public Contribution(final UploadItem item, final SessionManager sessionManager,
+ final List depictedItems, final List categories) {
+ super(item.getMediaUri(),
+ item.getFileName(),
+ UploadMediaDetail.formatList(item.getUploadMediaDetails()),
+ sessionManager.getAuthorName(),
+ categories);
+ captions = UploadMediaDetail.formatCaptions(item.getUploadMediaDetails());
+ decimalCoords = item.getGpsCoords().getDecimalCoords();
+ dateCreatedSource = "";
+ this.depictedItems = depictedItems;
+ wikidataPlace = WikidataPlace.from(item.getPlace());
}
-
-
- public void setDateCreatedSource(String dateCreatedSource) {
- this.dateCreatedSource = dateCreatedSource;
+ public Contribution(final MwQueryLogEvent queryLogEvent, final String user) {
+ super(queryLogEvent.title(),queryLogEvent.date(), user);
+ decimalCoords = "";
+ dateCreatedSource = "";
+ state = STATE_COMPLETED;
}
- public boolean getMultiple() {
- return isMultiple;
+ public void setDateCreatedSource(final String dateCreatedSource) {
+ this.dateCreatedSource = dateCreatedSource;
}
- public void setMultiple(boolean multiple) {
- isMultiple = multiple;
+ public String getDateCreatedSource() {
+ return dateCreatedSource;
}
public long getTransferred() {
return transferred;
}
- public void setTransferred(long transferred) {
+ public void setTransferred(final long transferred) {
this.transferred = transferred;
}
- public String getEditSummary() {
- return editSummary != null ? editSummary : CommonsApplication.DEFAULT_EDIT_SUMMARY;
- }
-
- public Uri getContentUri() {
- return contentUri;
- }
-
- public void setContentUri(Uri contentUri) {
- this.contentUri = contentUri;
- }
-
public int getState() {
return state;
}
- public void setState(int state) {
+ public void setState(final int state) {
this.state = state;
}
- public void setDateUploaded(Date date) {
- this.dateUploaded = date;
- }
-
- public String getPageContents(Context applicationContext) {
- StringBuilder buffer = new StringBuilder();
- buffer
- .append("== {{int:filedesc}} ==\n")
- .append("{{Information\n")
- .append("|description=").append(getDescription()).append("\n")
- .append("|source=").append("{{own}}\n")
- .append("|author=[[User:").append(creator).append("|").append(creator).append("]]\n");
-
- String templatizedCreatedDate = getTemplatizedCreatedDate();
- if (!StringUtils.isBlank(templatizedCreatedDate)) {
- buffer.append("|date=").append(templatizedCreatedDate);
- }
-
- buffer.append("}}").append("\n");
-
- //Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null
- if (decimalCoords != null) {
- buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n");
- }
-
- buffer.append("== {{int:license-header}} ==\n")
- .append(licenseTemplateFor(getLicense())).append("\n\n")
- .append("{{Uploaded from Mobile|platform=Android|version=")
- .append(ConfigUtils.getVersionNameWithSha(applicationContext)).append("}}\n");
- if(categories!=null&&categories.size()!=0) {
- for (int i = 0; i < categories.size(); i++) {
- String category = categories.get(i);
- buffer.append("\n[[Category:").append(category).append("]]");
- }
- }
- else
- buffer.append("{{subst:unc}}");
- return buffer.toString();
- }
-
/**
- * Returns upload date in either TEMPLATE_DATE_ACC_TO_EXIF or TEMPLATE_DATA_OTHER_SOURCE
- * @return
+ * @return array list of entityids for the depictions
*/
- private String getTemplatizedCreatedDate() {
- if (dateCreated != null) {
- java.text.SimpleDateFormat dateFormat = new java.text.SimpleDateFormat("yyyy-MM-dd");
- if (UploadableFile.DateTimeWithSource.EXIF_SOURCE.equals(dateCreatedSource)) {
- return String.format(Locale.ENGLISH, TEMPLATE_DATE_ACC_TO_EXIF, dateFormat.format(dateCreated)) + "\n";
- } else {
- return String.format(Locale.ENGLISH, TEMPLATE_DATA_OTHER_SOURCE, dateFormat.format(dateCreated)) + "\n";
- }
- }
- return "";
+ public List getDepictedItems() {
+ return depictedItems;
}
- @Override
- public void setFilename(String filename) {
- this.filename = filename;
- }
-
- public void setImageUrl(String imageUrl) {
- this.imageUrl = imageUrl;
- }
-
- public Contribution() {
-
+ public void setWikidataPlace(final WikidataPlace wikidataPlace) {
+ this.wikidataPlace = wikidataPlace;
}
- public String getSource() {
- return source;
+ public WikidataPlace getWikidataPlace() {
+ return wikidataPlace;
}
- public void setSource(String source) {
- this.source = source;
+ public long get_id() {
+ return _id;
}
- @NonNull
- private String licenseTemplateFor(String license) {
- switch (license) {
- case Prefs.Licenses.CC_BY_3:
- return "{{self|cc-by-3.0}}";
- case Prefs.Licenses.CC_BY_4:
- return "{{self|cc-by-4.0}}";
- case Prefs.Licenses.CC_BY_SA_3:
- return "{{self|cc-by-sa-3.0}}";
- case Prefs.Licenses.CC_BY_SA_4:
- return "{{self|cc-by-sa-4.0}}";
- case Prefs.Licenses.CC0:
- return "{{self|cc-zero}}";
- }
-
- throw new RuntimeException("Unrecognized license value: " + license);
+ public void set_id(final long _id) {
+ this._id = _id;
}
- public String getWikiDataEntityId() {
- return wikiDataEntityId;
+ public String getDecimalCoords() {
+ return decimalCoords;
}
- public String getWikiItemName() {
- return wikiItemName;
+ public void setDecimalCoords(final String decimalCoords) {
+ this.decimalCoords = decimalCoords;
}
- /**
- * When the corresponding wikidata entity is known as in case of nearby uploads, it can be set
- * using the setter method
- * @param wikiDataEntityId wikiDataEntityId
- */
- public void setWikiDataEntityId(String wikiDataEntityId) {
- this.wikiDataEntityId = wikiDataEntityId;
+ public void setDepictedItems(final List depictedItems) {
+ this.depictedItems = depictedItems;
}
- public void setWikiItemName(String wikiItemName) {
- this.wikiItemName = wikiItemName;
+ public void setMimeType(String mimeType) {
+ this.mimeType = mimeType;
}
- public String getP18Value() {
- return p18Value;
+ public String getMimeType() {
+ return mimeType;
}
/**
- * When the corresponding image property of wiki entity is known as in case of nearby uploads,
- * it can be set using the setter method
- * @param p18Value p18 value, image property of the wikidata item
+ * Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
+ * This is a replacement of the previously used titles for images (titles were not multilingual)
+ * Also now captions replace the previous convention of using title for filename
+ *
+ * key of the HashMap is the language and value is the caption in the corresponding language
+ *
+ * returns list of captions stored in hashmap
*/
- public void setP18Value(String p18Value) {
- this.p18Value = p18Value;
+ public Map getCaptions() {
+ return captions;
}
- public void setContentProviderUri(Uri contentProviderUri) {
- this.contentProviderUri = contentProviderUri;
+ public void setCaptions(Map captions) {
+ this.captions = captions;
}
@Override
@@ -259,48 +159,34 @@ public int describeContents() {
}
@Override
- public void writeToParcel(Parcel dest, int flags) {
+ public void writeToParcel(final Parcel dest, final int flags) {
super.writeToParcel(dest, flags);
- dest.writeLong(this._id);
- dest.writeParcelable(this.contentUri, flags);
- dest.writeString(this.source);
- dest.writeString(this.editSummary);
- dest.writeInt(this.state);
- dest.writeLong(this.transferred);
- dest.writeString(this.decimalCoords);
- dest.writeByte(this.isMultiple ? (byte) 1 : (byte) 0);
- dest.writeString(this.wikiDataEntityId);
- dest.writeString(this.wikiItemName);
- dest.writeString(this.p18Value);
- dest.writeParcelable(this.contentProviderUri, flags);
- dest.writeString(this.dateCreatedSource);
+ dest.writeLong(_id);
+ dest.writeInt(state);
+ dest.writeLong(transferred);
+ dest.writeString(decimalCoords);
+ dest.writeString(dateCreatedSource);
+ dest.writeSerializable((HashMap) captions);
}
- protected Contribution(Parcel in) {
+ protected Contribution(final Parcel in) {
super(in);
- this._id = in.readLong();
- this.contentUri = in.readParcelable(Uri.class.getClassLoader());
- this.source = in.readString();
- this.editSummary = in.readString();
- this.state = in.readInt();
- this.transferred = in.readLong();
- this.decimalCoords = in.readString();
- this.isMultiple = in.readByte() != 0;
- this.wikiDataEntityId = in.readString();
- this.wikiItemName = in.readString();
- this.p18Value = in.readString();
- this.contentProviderUri = in.readParcelable(Uri.class.getClassLoader());
- this.dateCreatedSource = in.readString();
+ _id = in.readLong();
+ state = in.readInt();
+ transferred = in.readLong();
+ decimalCoords = in.readString();
+ dateCreatedSource = in.readString();
+ captions = (HashMap) in.readSerializable();
}
public static final Creator CREATOR = new Creator() {
@Override
- public Contribution createFromParcel(Parcel source) {
+ public Contribution createFromParcel(final Parcel source) {
return new Contribution(source);
}
@Override
- public Contribution[] newArray(int size) {
+ public Contribution[] newArray(final int size) {
return new Contribution[size];
}
};
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java
index 510d63949a5..3c0ac925f8a 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java
@@ -1,6 +1,6 @@
package fr.free.nrw.commons.contributions;
-import androidx.paging.DataSource;
+import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
@@ -10,74 +10,45 @@
import androidx.room.Update;
import io.reactivex.Completable;
import io.reactivex.Single;
-import java.util.Calendar;
-import java.util.Date;
import java.util.List;
@Dao
public abstract class ContributionDao {
- @Query("SELECT * FROM contribution order by media_dateUploaded DESC")
- abstract DataSource.Factory fetchContributions();
+ @Query("SELECT * FROM contribution order by dateUploaded DESC")
+ abstract LiveData> fetchContributions();
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- public abstract void saveSynchronous(Contribution contribution);
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ public abstract Single save(Contribution contribution);
- public Completable save(final Contribution contribution) {
- return Completable
- .fromAction(() -> {
- contribution.setDateModified(Calendar.getInstance().getTime());
- saveSynchronous(contribution);
- });
- }
+ public Completable deleteAllAndSave(List contributions){
+ return Completable.fromAction(() -> deleteAllAndSaveTransaction(contributions));
+ }
- @Transaction
- public void deleteAndSaveContribution(final Contribution oldContribution,
- final Contribution newContribution) {
- deleteSynchronous(oldContribution);
- saveSynchronous(newContribution);
- }
+ @Transaction
+ public void deleteAllAndSaveTransaction(List contributions){
+ deleteAll(Contribution.STATE_COMPLETED);
+ save(contributions);
+ }
- public Completable saveAndDelete(final Contribution oldContribution,
- final Contribution newContribution) {
- return Completable
- .fromAction(() -> deleteAndSaveContribution(oldContribution, newContribution));
- }
+ @Insert
+ public abstract void save(List contribution);
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- public abstract Single> save(List contribution);
+ @Delete
+ public abstract Single delete(Contribution contribution);
- @Delete
- public abstract void deleteSynchronous(Contribution contribution);
+ @Query("SELECT * from contribution WHERE filename=:fileName")
+ public abstract List getContributionWithTitle(String fileName);
- public Completable delete(final Contribution contribution) {
- return Completable
- .fromAction(() -> deleteSynchronous(contribution));
- }
+ @Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)")
+ public abstract Single updateStates(int state, int[] toUpdateStates);
- @Query("SELECT * from contribution WHERE media_filename=:fileName")
- public abstract List getContributionWithTitle(String fileName);
+ @Query("Delete FROM contribution")
+ public abstract void deleteAll();
- @Query("SELECT * from contribution WHERE pageId=:pageId")
- public abstract Contribution getContribution(String pageId);
+ @Query("Delete FROM contribution WHERE state = :state")
+ public abstract void deleteAll(int state);
- @Query("SELECT * from contribution WHERE state=:state")
- public abstract Single> getContribution(int state);
-
- @Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)")
- public abstract Single updateStates(int state, int[] toUpdateStates);
-
- @Query("Delete FROM contribution")
- public abstract void deleteAll();
-
- @Update
- public abstract void updateSynchronous(Contribution contribution);
-
- public Completable update(final Contribution contribution) {
- return Completable
- .fromAction(() -> {
- contribution.setDateModified(Calendar.getInstance().getTime());
- updateSynchronous(contribution);
- });
- }
+ @Update
+ public abstract Single update(Contribution contribution);
}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java
index 3c21c67f189..824953650ff 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java
@@ -1,11 +1,12 @@
package fr.free.nrw.commons.contributions;
+import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
+
import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
-import android.widget.ImageButton;
+import android.widget.LinearLayout;
import android.widget.ProgressBar;
-import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
@@ -21,227 +22,152 @@
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import timber.log.Timber;
public class ContributionViewHolder extends RecyclerView.ViewHolder {
- private final Callback callback;
- @BindView(R.id.contributionImage)
- SimpleDraweeView imageView;
- @BindView(R.id.contributionTitle)
- TextView titleView;
- @BindView(R.id.contributionState)
- TextView stateView;
- @BindView(R.id.contributionSequenceNumber)
- TextView seqNumView;
- @BindView(R.id.contributionProgress)
- ProgressBar progressView;
- @BindView(R.id.image_options)
- RelativeLayout imageOptions;
- @BindView(R.id.wikipediaButton)
- ImageButton addToWikipediaButton;
- @BindView(R.id.retryButton)
- ImageButton retryButton;
- @BindView(R.id.cancelButton)
- ImageButton cancelButton;
- @BindView(R.id.pauseResumeButton)
- ImageButton pauseResumeButton;
-
-
- private int position;
- private Contribution contribution;
- private final CompositeDisposable compositeDisposable = new CompositeDisposable();
- private final MediaClient mediaClient;
- private boolean isWikipediaButtonDisplayed;
-
- 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(final int position, final Contribution contribution) {
- this.contribution = contribution;
- this.position = position;
- titleView.setText(contribution.getMedia().getMostRelevantCaption());
-
- imageView.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
- imageView.getHierarchy().setFailureImage(R.drawable.image_placeholder);
+ private static final long TIMEOUT_SECONDS = 15;
+ private final Callback callback;
+ @BindView(R.id.contributionImage)
+ SimpleDraweeView imageView;
+ @BindView(R.id.contributionTitle) TextView titleView;
+ @BindView(R.id.contributionState) TextView stateView;
+ @BindView(R.id.contributionSequenceNumber) TextView seqNumView;
+ @BindView(R.id.contributionProgress) ProgressBar progressView;
+ @BindView(R.id.failed_image_options) LinearLayout failedImageOptions;
+
+
+ private int position;
+ private Contribution contribution;
+ private Random random = new Random();
+ private CompositeDisposable compositeDisposable = new CompositeDisposable();
+ private final MediaClient mediaClient;
+
+ ContributionViewHolder(View parent, Callback callback,
+ MediaClient mediaClient) {
+ super(parent);
+ this.mediaClient = mediaClient;
+ ButterKnife.bind(this, parent);
+ this.callback=callback;
+ }
+ public void init(int position, Contribution contribution) {
+ this.contribution = contribution;
+ fetchAndDisplayCaption(contribution);
+ this.position = position;
+ String imageSource = chooseImageSource(contribution.getThumbUrl(), contribution.getLocalUri());
+ if (!TextUtils.isEmpty(imageSource)) {
+ final ImageRequest imageRequest =
+ ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
+ .setProgressiveRenderingEnabled(true)
+ .build();
+ imageView.setImageRequest(imageRequest);
+ }
- final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(),
- contribution.getLocalUri());
- if (!TextUtils.isEmpty(imageSource)) {
- final ImageRequest imageRequest =
- ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
- .setProgressiveRenderingEnabled(true)
- .build();
- imageView.setImageRequest(imageRequest);
+ seqNumView.setText(String.valueOf(position + 1));
+ seqNumView.setVisibility(View.VISIBLE);
+
+ switch (contribution.getState()) {
+ case Contribution.STATE_COMPLETED:
+ stateView.setVisibility(View.GONE);
+ progressView.setVisibility(View.GONE);
+ failedImageOptions.setVisibility(View.GONE);
+ stateView.setText("");
+ break;
+ case Contribution.STATE_QUEUED:
+ stateView.setVisibility(View.VISIBLE);
+ progressView.setVisibility(View.GONE);
+ stateView.setText(R.string.contribution_state_queued);
+ failedImageOptions.setVisibility(View.GONE);
+ break;
+ case Contribution.STATE_IN_PROGRESS:
+ stateView.setVisibility(View.GONE);
+ progressView.setVisibility(View.VISIBLE);
+ failedImageOptions.setVisibility(View.GONE);
+ long total = contribution.getDataLength();
+ long transferred = contribution.getTransferred();
+ if (transferred == 0 || transferred >= total) {
+ progressView.setIndeterminate(true);
+ } else {
+ progressView.setProgress((int)(((double)transferred / (double)total) * 100));
+ }
+ break;
+ case Contribution.STATE_FAILED:
+ stateView.setVisibility(View.VISIBLE);
+ stateView.setText(R.string.contribution_state_failed);
+ progressView.setVisibility(View.GONE);
+ failedImageOptions.setVisibility(View.VISIBLE);
+ break;
+ }
}
- seqNumView.setText(String.valueOf(position + 1));
- seqNumView.setVisibility(View.VISIBLE);
-
- addToWikipediaButton.setVisibility(View.GONE);
- switch (contribution.getState()) {
- case Contribution.STATE_COMPLETED:
- stateView.setVisibility(View.GONE);
- progressView.setVisibility(View.GONE);
- imageOptions.setVisibility(View.GONE);
- stateView.setText("");
- checkIfMediaExistsOnWikipediaPage(contribution);
- break;
- case Contribution.STATE_QUEUED:
- case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE:
- stateView.setVisibility(View.VISIBLE);
- progressView.setVisibility(View.GONE);
- stateView.setText(R.string.contribution_state_queued);
- imageOptions.setVisibility(View.GONE);
- break;
- case Contribution.STATE_IN_PROGRESS:
- stateView.setVisibility(View.GONE);
- progressView.setVisibility(View.VISIBLE);
- addToWikipediaButton.setVisibility(View.GONE);
- pauseResumeButton.setVisibility(View.VISIBLE);
- cancelButton.setVisibility(View.GONE);
- retryButton.setVisibility(View.GONE);
- imageOptions.setVisibility(View.VISIBLE);
- final long total = contribution.getDataLength();
- final long transferred = contribution.getTransferred();
- if (transferred == 0 || transferred >= total) {
- progressView.setIndeterminate(true);
+ /**
+ * In contributions first we show the title for the image stored in cache,
+ * then we fetch captions associated with the image and replace title on the thumbnail with caption
+ *
+ * @param contribution
+ */
+ private void fetchAndDisplayCaption(Contribution contribution) {
+ if ((contribution.getState() != Contribution.STATE_COMPLETED)) {
+ titleView.setText(contribution.getDisplayTitle());
} else {
- progressView.setProgress((int) (((double) transferred / (double) total) * 100));
+ final String pageId = contribution.getPageId();
+ if (pageId != null) {
+ Timber.d("Fetching caption for %s", contribution.getFilename());
+ 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())
+ .observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .subscribe(subscriber -> {
+ if (!subscriber.trim().equals(MediaClient.NO_CAPTION)) {
+ titleView.setText(subscriber);
+ } else {
+ titleView.setText(contribution.getDisplayTitle());
+ }
+ }));
+ } else {
+ titleView.setText(contribution.getDisplayTitle());
+ }
}
- break;
- case Contribution.STATE_PAUSED:
- stateView.setVisibility(View.VISIBLE);
- stateView.setText(R.string.paused);
- setResume();
- progressView.setVisibility(View.GONE);
- cancelButton.setVisibility(View.GONE);
- retryButton.setVisibility(View.GONE);
- pauseResumeButton.setVisibility(View.VISIBLE);
- imageOptions.setVisibility(View.VISIBLE);
- break;
- case Contribution.STATE_FAILED:
- stateView.setVisibility(View.VISIBLE);
- stateView.setText(R.string.contribution_state_failed);
- progressView.setVisibility(View.GONE);
- cancelButton.setVisibility(View.VISIBLE);
- retryButton.setVisibility(View.VISIBLE);
- pauseResumeButton.setVisibility(View.GONE);
- imageOptions.setVisibility(View.VISIBLE);
- break;
}
- }
- /**
- * Checks if a media exists on the corresponding Wikipedia article Currently the check is made for
- * the device's current language Wikipedia
- *
- * @param contribution
- */
- private void checkIfMediaExistsOnWikipediaPage(final Contribution contribution) {
- if (contribution.getWikidataPlace() == null
- || contribution.getWikidataPlace().getWikipediaArticle() == null) {
- return;
+ /**
+ * Returns the image source for the image view, first preference is given to thumbUrl if that is
+ * null, moves to local uri and if both are null return null
+ *
+ * @param thumbUrl
+ * @param localUri
+ * @return
+ */
+ @Nullable
+ private String chooseImageSource(String thumbUrl, Uri localUri) {
+ return !TextUtils.isEmpty(thumbUrl) ? thumbUrl :
+ localUri != null ? localUri.toString() :
+ null;
}
- final String wikipediaArticle = contribution.getWikidataPlace().getWikipediaPageTitle();
- compositeDisposable.add(mediaClient.doesPageContainMedia(wikipediaArticle)
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(mediaExists -> {
- displayWikipediaButton(mediaExists);
- }));
- }
- /**
- * Handle action buttons visibility if the corresponding wikipedia page doesn't contain any media.
- * This method needs to control the state of just the scenario where media does not exists as
- * other scenarios are already handled in the init method.
- *
- * @param mediaExists
- */
- private void displayWikipediaButton(Boolean mediaExists) {
- if (!mediaExists) {
- addToWikipediaButton.setVisibility(View.VISIBLE);
- isWikipediaButtonDisplayed = true;
- cancelButton.setVisibility(View.GONE);
- retryButton.setVisibility(View.GONE);
- imageOptions.setVisibility(View.VISIBLE);
+ /**
+ * Retry upload when it is failed
+ */
+ @OnClick(R.id.retryButton)
+ public void retryUpload() {
+ callback.retryUpload(contribution);
}
- }
-
- /**
- * Returns the image source for the image view, first preference is given to thumbUrl if that is
- * null, moves to local uri and if both are null return null
- *
- * @param thumbUrl
- * @param localUri
- * @return
- */
- @Nullable
- private String chooseImageSource(final String thumbUrl, final Uri localUri) {
- return !TextUtils.isEmpty(thumbUrl) ? thumbUrl :
- localUri != null ? localUri.toString() :
- null;
- }
-
- /**
- * Retry upload when it is failed
- */
- @OnClick(R.id.retryButton)
- public void retryUpload() {
- callback.retryUpload(contribution);
- }
-
- /**
- * Delete a failed upload attempt
- */
- @OnClick(R.id.cancelButton)
- public void deleteUpload() {
- callback.deleteUpload(contribution);
- }
- @OnClick(R.id.contributionImage)
- public void imageClicked() {
- callback.openMediaDetail(position, isWikipediaButtonDisplayed);
- }
-
- @OnClick(R.id.wikipediaButton)
- public void wikipediaButtonClicked() {
- callback.addImageToWikipedia(contribution);
- }
-
- /**
- * Triggers a callback for pause/resume
- */
- @OnClick(R.id.pauseResumeButton)
- public void onPauseResumeButtonClicked() {
- if (pauseResumeButton.getTag().toString().equals("pause")) {
- callback.pauseUpload(contribution);
- setResume();
- } else {
- callback.resumeUpload(contribution);
- setPaused();
+ /**
+ * Delete a failed upload attempt
+ */
+ @OnClick(R.id.cancelButton)
+ public void deleteUpload() {
+ callback.deleteUpload(contribution);
}
- }
- /**
- * Update pause/resume button to show pause state
- */
- private void setPaused() {
- pauseResumeButton.setImageResource(R.drawable.pause_icon);
- pauseResumeButton.setTag(R.string.pause);
- }
-
- /**
- * Update pause/resume button to show resume state
- */
- private void setResume() {
- pauseResumeButton.setImageResource(R.drawable.play_icon);
- pauseResumeButton.setTag(R.string.resume);
- }
+ @OnClick(R.id.contributionImage)
+ public void imageClicked(){
+ callback.openMediaDetail(position);
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java
index bb93b5777e6..64b836c39d4 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java
@@ -1,7 +1,6 @@
package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
-import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED;
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
@@ -20,26 +19,20 @@
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 fr.free.nrw.commons.MediaDataExtractor;
-import fr.free.nrw.commons.auth.SessionManager;
-import io.reactivex.disposables.Disposable;
-import java.util.List;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
import butterknife.BindView;
import butterknife.ButterKnife;
+import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
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.ContributionsListFragment.Callback;
+import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
+import fr.free.nrw.commons.contributions.ContributionsListFragment.SourceRefresher;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
@@ -51,6 +44,7 @@
import fr.free.nrw.commons.nearby.NearbyController;
import fr.free.nrw.commons.nearby.NearbyNotificationCardView;
import fr.free.nrw.commons.nearby.Place;
+import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.UploadService;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.DialogUtil;
@@ -61,6 +55,7 @@
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;
@@ -68,10 +63,11 @@
public class ContributionsFragment
extends CommonsDaggerSupportFragment
implements
+ MediaDetailProvider,
OnBackStackChangedListener,
+ SourceRefresher,
LocationUpdateListener,
- MediaDetailProvider,
- ICampaignsView, ContributionsContract.View, Callback {
+ ICampaignsView, ContributionsContract.View {
@Inject @Named("default_preferences") JsonKvStore store;
@Inject NearbyController nearbyController;
@Inject OkHttpJsonApiClient okHttpJsonApiClient;
@@ -83,8 +79,8 @@ public class ContributionsFragment
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private ContributionsListFragment contributionsListFragment;
- private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
private MediaDetailPagerFragment mediaDetailPagerFragment;
+ private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
@BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView;
@@ -92,9 +88,6 @@ public class ContributionsFragment
@Inject ContributionsPresenter contributionsPresenter;
- @Inject
- SessionManager sessionManager;
-
private LatLng curLatLng;
private boolean firstLocationUpdate = true;
@@ -109,7 +102,7 @@ public class ContributionsFragment
private ServiceConnection uploadServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder binder) {
- uploadService = (UploadService) ((UploadService.UploadServiceLocalBinder) binder)
+ uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder)
.getService();
isUploadServiceConnected = true;
}
@@ -121,6 +114,7 @@ public void onServiceDisconnected(ComponentName componentName) {
}
};
private boolean shouldShowMediaDetailsFragment;
+ private int numberOfContributions;
private boolean isAuthCookieAcquired;
@Override
@@ -135,6 +129,7 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
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);
@@ -147,9 +142,9 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
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");
}
@@ -161,14 +156,89 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
showContributionsListFragment();
}
- if (!ConfigUtils.isBetaFlavour() && sessionManager.isUserLoggedIn()
- && sessionManager.getCurrentAccount() != null) {
+ 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);
@@ -196,7 +266,7 @@ private void showContributionsListFragment() {
if (nearbyNotificationCardView != null) {
if (store.getBoolean("displayNearbyCardView", true)) {
if (nearbyNotificationCardView.cardViewVisibilityState
- == NearbyNotificationCardView.CardViewVisibilityState.READY) {
+ == NearbyNotificationCardView.CardViewVisibilityState.READY) {
nearbyNotificationCardView.setVisibility(View.VISIBLE);
}
} else {
@@ -206,20 +276,18 @@ private void showContributionsListFragment() {
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
@@ -240,42 +308,43 @@ void onAuthCookieAcquired() {
}
- private void initFragments() {
- if (null == contributionsListFragment) {
- contributionsListFragment = new ContributionsListFragment();
- }
+ public Intent getUploadServiceIntent(){
+ Intent intent = new Intent(getActivity(), UploadService.class);
+ intent.setAction(UploadService.ACTION_START_SERVICE);
+ return intent;
+ }
- if (shouldShowMediaDetailsFragment) {
+ /**
+ * 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();
- } else {
- showContributionsListFragment();
}
+ mediaDetailPagerFragment.showImage(i);
+ }
- showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG);
+ @Override
+ public void refreshSource() {
+ contributionsPresenter.fetchContributions();
}
- /**
- * 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 Media getMediaAtPosition(int i) {
+ return contributionsPresenter.getItemAtPosition(i);
}
- public Intent getUploadServiceIntent(){
- Intent intent = new Intent(getActivity(), UploadService.class);
- intent.setAction(UploadService.ACTION_START_SERVICE);
- return intent;
+ @Override
+ public int getTotalMediaCount() {
+ return numberOfContributions;
}
@SuppressWarnings("ConstantConditions")
private void setUploadCount() {
+
compositeDisposable.add(okHttpJsonApiClient
.getUploadCount(((MainActivity)getActivity()).sessionManager.getCurrentAccount().name)
.subscribeOn(Schedulers.io())
@@ -305,6 +374,8 @@ public void onPause() {
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
+ boolean mediaDetailsVisible = mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible();
+ outState.putBoolean("mediaDetailsVisible", mediaDetailsVisible);
}
@Override
@@ -314,6 +385,13 @@ public void onResume() {
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) {
@@ -326,6 +404,10 @@ public void onResume() {
}
fetchCampaigns();
+ if(isAuthCookieAcquired){
+ contributionsPresenter.fetchContributions();
+ }
+
}
private void checkPermissionsAndShowNearbyCardView() {
@@ -382,11 +464,17 @@ private void updateClosestNearbyCardViewInfo() {
}
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);
@@ -466,62 +554,48 @@ private void fetchCampaigns() {
presenter.onDetachView();
}
- /**
- * Retry upload when it is failed
- *
- * @param contribution contribution to be retried
- */
@Override
- public void retryUpload(Contribution contribution) {
- if (NetworkUtils.isInternetConnectionEstablished(getContext())) {
- if (contribution.getState() == STATE_FAILED || contribution.getState() == STATE_PAUSED || contribution.getState()==Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE && null != uploadService) {
- uploadService.queue(contribution);
- Timber.d("Restarting for %s", contribution.toString());
- } else {
- Timber.d("Skipping re-upload for non-failed %s", contribution.toString());
- }
- } else {
- ViewUtil.showLongToast(getContext(), R.string.this_function_needs_network_connection);
- }
-
+ public void showWelcomeTip(boolean shouldShow) {
+ contributionsListFragment.showWelcomeTip(shouldShow);
}
- /**
- * Pauses the upload
- * @param contribution
- */
@Override
- public void pauseUpload(Contribution contribution) {
- uploadService.pauseUpload(contribution);
+ public void showProgress(boolean shouldShow) {
+ contributionsListFragment.showProgress(shouldShow);
}
- /**
- * 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, boolean isWikipediaButtonDisplayed) {
- if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
- mediaDetailPagerFragment = new MediaDetailPagerFragment();
- showMediaDetailPagerFragment();
- }
- mediaDetailPagerFragment.showImage(position, isWikipediaButtonDisplayed);
+ public void showNoContributionsUI(boolean shouldShow) {
+ contributionsListFragment.showNoContributionsUI(shouldShow);
}
@Override
- public Media getMediaAtPosition(int i) {
- return contributionsListFragment.getMediaAtPosition(i);
+ public void setUploadCount(int count) {
+ this.numberOfContributions=count;
}
@Override
- public int getTotalMediaCount() {
- return contributionsListFragment.getTotalMediaCount();
+ public void showContributions(List contributionList) {
+ contributionsListFragment.setContributions(contributionList);
}
- @Override
- public Integer getContributionStateAt(int position) {
- return contributionsListFragment.getContributionStateAt(position);
+ /**
+ * Retry upload when it is failed
+ *
+ * @param contribution contribution to be retried
+ */
+ private void retryUpload(Contribution contribution) {
+ if (NetworkUtils.isInternetConnectionEstablished(getContext())) {
+ if (contribution.getState() == STATE_FAILED && null != uploadService) {
+ uploadService.queue(UploadService.ACTION_UPLOAD_FILE, contribution);
+ Timber.d("Restarting for %s", contribution.toString());
+ } else {
+ Timber.d("Skipping re-upload for non-failed %s", contribution.toString());
+ }
+ } else {
+ ViewUtil.showLongToast(getContext(), R.string.this_function_needs_network_connection);
+ }
+
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java
index e6db7e3de28..82daf97f4d1 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java
@@ -1,71 +1,68 @@
- 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.paging.PagedListAdapter;
-import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.RecyclerView;
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
- PagedListAdapter {
+public class ContributionsListAdapter extends RecyclerView.Adapter {
- private final Callback callback;
+ private Callback callback;
private final MediaClient mediaClient;
+ private List contributions;
- ContributionsListAdapter(final Callback callback,
- final MediaClient mediaClient) {
- super(DIFF_CALLBACK);
+ public ContributionsListAdapter(Callback callback,
+ MediaClient mediaClient) {
this.callback = callback;
this.mediaClient = mediaClient;
+ contributions = new ArrayList<>();
}
/**
- * 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
+ * Creates the new View Holder which will be used to display items(contributions)
+ * using the onBindViewHolder(viewHolder,position)
*/
- private static final DiffUtil.ItemCallback DIFF_CALLBACK =
- new DiffUtil.ItemCallback() {
- @Override
- public boolean areItemsTheSame(final Contribution oldContribution, final Contribution newContribution) {
- return oldContribution.getPageId().equals(newContribution.getPageId());
- }
-
- @Override
- public boolean areContentsTheSame(final Contribution oldContribution, final Contribution newContribution) {
- return oldContribution.equals(newContribution);
- }
- };
+ @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;
+ }
- /**
- * Initializes the view holder with contribution data
- */
@Override
public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) {
- holder.init(position, getItem(position));
+ final Contribution contribution = contributions.get(position);
+ if (TextUtils.isEmpty(contribution.getThumbUrl())
+ && contribution.getState() == Contribution.STATE_COMPLETED) {
+ callback.fetchMediaUriFor(contribution);
+ }
+
+ holder.init(position, contribution);
}
- Contribution getContributionForPosition(final int position) {
- return getItem(position);
+ @Override
+ public int getItemCount() {
+ return contributions.size();
+ }
+
+ public void setContributions(@NonNull List contributionList) {
+ contributions = contributionList;
+ notifyDataSetChanged();
}
- /**
- * Creates the new View Holder which will be used to display items(contributions) using the
- * onBindViewHolder(viewHolder,position)
- */
- @NonNull
@Override
- 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 long getItemId(int position) {
+ return contributions.get(position).get_id();
}
public interface Callback {
@@ -74,12 +71,10 @@ public interface Callback {
void deleteUpload(Contribution contribution);
- void openMediaDetail(int contribution, boolean isWikipediaPageExists);
-
- void addImageToWikipedia(Contribution contribution);
+ void openMediaDetail(int contribution);
- void pauseUpload(Contribution contribution);
+ Contribution getContributionForPosition(int position);
- void resumeUpload(Contribution contribution);
+ void fetchMediaUriFor(Contribution contribution);
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java
index 4a435b28f34..d7f813c6db3 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java
@@ -2,13 +2,9 @@
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
-import static fr.free.nrw.commons.di.NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE;
-import android.content.Context;
import android.content.res.Configuration;
-import android.net.Uri;
import android.os.Bundle;
-import android.os.Parcelable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -19,322 +15,219 @@
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.fragment.app.FragmentManager;
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.Utils;
+import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
-import fr.free.nrw.commons.utils.DialogUtil;
+import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaClient;
-import java.util.Locale;
+import java.util.ArrayList;
+import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
-import org.wikipedia.dataclient.WikiSite;
-import timber.log.Timber;
/**
* Created by root on 01.06.2018.
*/
-public class ContributionsListFragment extends CommonsDaggerSupportFragment implements
- ContributionsListContract.View, ContributionsListAdapter.Callback,
- WikipediaInstructionsDialogFragment.Callback {
-
- private static final String RV_STATE = "rv_scroll_state";
-
- @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;
-
- @Inject
- ContributionController controller;
- @Inject
- MediaClient mediaClient;
-
- @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
- @Inject
- WikiSite languageWikipediaSite;
-
- @Inject
- ContributionsListPresenter contributionsListPresenter;
-
- private Animation fab_close;
- private Animation fab_open;
- private Animation rotate_forward;
- private Animation rotate_backward;
-
-
- private boolean isFabOpen;
-
- private ContributionsListAdapter adapter;
-
- private Callback callback;
-
- private final int SPAN_COUNT_LANDSCAPE = 3;
- private final int SPAN_COUNT_PORTRAIT = 1;
-
-
- @Override
- 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;
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- if (getParentFragment() != null && getParentFragment() instanceof ContributionsFragment) {
- callback = ((ContributionsFragment) getParentFragment());
+public class ContributionsListFragment extends CommonsDaggerSupportFragment {
+
+ 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;
+
+ @Inject @Named("default_preferences") JsonKvStore kvStore;
+ @Inject ContributionController controller;
+ @Inject MediaClient mediaClient;
+
+ private Animation fab_close;
+ private Animation fab_open;
+ private Animation rotate_forward;
+ private Animation rotate_backward;
+
+
+ private boolean isFabOpen = false;
+
+ private ContributionsListAdapter adapter;
+
+ private Callback callback;
+ private String lastVisibleItemID;
+
+ private int SPAN_COUNT=3;
+ private List contributions=new ArrayList<>();
+
+ 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;
}
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- callback = null;//To avoid possible memory leak
- }
-
- 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.getViewLifecycleOwner(), 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;
+ }
+
+ private void initAdapter() {
+ adapter = new ContributionsListAdapter(callback, mediaClient);
+ adapter.setHasStableIds(true);
}
- }
-
- /**
- * Shows welcome message if user has no contributions yet i.e. new user.
- */
- @Override
- 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.
- */
- @Override
- public void showProgress(final boolean shouldShow) {
- progressBar.setVisibility(shouldShow ? VISIBLE : GONE);
- }
-
- @Override
- 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);
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ initRecyclerView();
+ initializeAnimations();
+ setListeners();
}
- }
- @Override
- public void retryUpload(final Contribution contribution) {
- if (null != callback) {//Just being safe, ideally they won't be called when detached
- callback.retryUpload(contribution);
+ private void initRecyclerView() {
+ if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ rvContributionsList.setLayoutManager(new GridLayoutManager(getContext(),SPAN_COUNT));
+ } else {
+ rvContributionsList.setLayoutManager(new LinearLayoutManager(getContext()));
+ }
+
+ rvContributionsList.setAdapter(adapter);
+ adapter.setContributions(contributions);
}
- }
- @Override
- public void deleteUpload(final Contribution contribution) {
- contributionsListPresenter.deleteUpload(contribution);
- }
+ @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()));
+ }
+ }
- @Override
- public void openMediaDetail(final int position, boolean isWikipediaButtonDisplayed) {
- if (null != callback) {//Just being safe, ideally they won't be called when detached
- callback.showDetail(position, isWikipediaButtonDisplayed);
+ 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);
}
- }
-
- /**
- * Handle callback for wikipedia icon clicked
- *
- * @param contribution
- */
- @Override
- public void addImageToWikipedia(Contribution contribution) {
- DialogUtil.showAlertDialog(getActivity(),
- getString(R.string.add_picture_to_wikipedia_article_title),
- String.format(getString(R.string.add_picture_to_wikipedia_article_desc),
- Locale.getDefault().getDisplayLanguage()),
- () -> {
- showAddImageToWikipediaInstructions(contribution);
- }, () -> {
- // do nothing
+
+ 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);
});
- }
-
- /**
- * Pauses the current upload
- * @param contribution
- */
- @Override
- public void pauseUpload(Contribution contribution) {
- callback.pauseUpload(contribution);
- }
-
- /**
- * Resumes the current upload
- * @param contribution
- */
- @Override
- public void resumeUpload(Contribution contribution) {
- callback.retryUpload(contribution);
- }
-
- /**
- * Display confirmation dialog with instructions when the user tries to add image to wikipedia
- *
- * @param contribution
- */
- private void showAddImageToWikipediaInstructions(Contribution contribution) {
- FragmentManager fragmentManager = getFragmentManager();
- WikipediaInstructionsDialogFragment fragment = WikipediaInstructionsDialogFragment
- .newInstance(contribution);
- fragment.setCallback(this::onConfirmClicked);
- fragment.show(fragmentManager, "WikimediaFragment");
- }
-
-
- public Media getMediaAtPosition(final int i) {
- return adapter.getContributionForPosition(i).getMedia();
- }
-
- public int getTotalMediaCount() {
- return adapter.getItemCount();
- }
-
- /**
- * Open the editor for the language Wikipedia
- *
- * @param contribution
- */
- @Override
- public void onConfirmClicked(@Nullable Contribution contribution, boolean copyWikicode) {
- if (copyWikicode) {
- String wikicode = contribution.getMedia().getWikiCode();
- Utils.copy("wikicode", wikicode, getContext());
}
- final String url =
- languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace()
- .getWikipediaPageTitle();
- Utils.handleWebUrl(getContext(), Uri.parse(url));
- }
+ 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 Integer getContributionStateAt(int position) {
- return adapter.getContributionForPosition(position).getState();
- }
+ public void showNoContributionsUI(boolean shouldShow) {
+ noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
+ }
- public interface Callback {
+ public void setContributions(List contributionList) {
+ this.contributions.clear();
+ this.contributions.addAll(contributionList);
+ adapter.setContributions(contributions);
+ }
- void retryUpload(Contribution contribution);
+ public interface SourceRefresher {
+ void refreshSource();
+ }
- void showDetail(int position, boolean isWikipediaButtonDisplayed);
+ @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 pauseUpload(Contribution contribution);
- }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java
index 2cae4f04c5f..310699cd158 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java
@@ -1,12 +1,29 @@
package fr.free.nrw.commons.contributions;
+import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+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.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;
import io.reactivex.Scheduler;
+import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.schedulers.Schedulers;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
+import timber.log.Timber;
/**
* The presenter class for Contributions
@@ -18,10 +35,25 @@ public class ContributionsPresenter implements UserActionListener {
private final Scheduler ioThreadScheduler;
private CompositeDisposable compositeDisposable;
private ContributionsContract.View view;
+ private List contributionList=new ArrayList<>();
+
+ @Inject
+ Context context;
+
+ @Inject
+ UserClient userClient;
+
+ @Inject
+ AppDatabase appDatabase;
+
+ @Inject
+ SessionManager sessionManager;
@Inject
MediaDataExtractor mediaDataExtractor;
+ private LifecycleOwner lifeCycleOwner;
+
@Inject
ContributionsPresenter(ContributionsRepository repository, @Named(CommonsApplicationModule.MAIN_THREAD) Scheduler mainThreadScheduler,@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) {
this.repository = repository;
@@ -29,12 +61,74 @@ public class ContributionsPresenter implements UserActionListener {
this.ioThreadScheduler=ioThreadScheduler;
}
+ private String user;
+
@Override
public void onAttachView(ContributionsContract.View view) {
this.view = view;
compositeDisposable=new CompositeDisposable();
}
+ public void setLifeCycleOwner(LifecycleOwner lifeCycleOwner){
+ this.lifeCycleOwner=lifeCycleOwner;
+ }
+
+ public void fetchContributions() {
+ Timber.d("fetch Contributions");
+ LiveData> liveDataContributions = repository.fetchContributions();
+ if(null!=lifeCycleOwner) {
+ liveDataContributions.observe(lifeCycleOwner, this::showContributions);
+ }
+
+ if (NetworkUtils.isInternetConnectionEstablished(CommonsApplication.getInstance()) && shouldFetchContributions()) {
+ Timber.d("fetching contributions: ");
+ view.showProgress(true);
+ this.user = sessionManager.getUserName();
+ view.showContributions(Collections.emptyList());
+ compositeDisposable.add(userClient.logEvents(user)
+ .subscribeOn(ioThreadScheduler)
+ .observeOn(mainThreadScheduler)
+ .doOnNext(mwQueryLogEvent -> Timber.d("Received image %s", mwQueryLogEvent.title()))
+ .filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted()).doOnNext(mwQueryLogEvent -> Timber.d("Image %s passed filters", mwQueryLogEvent.title()))
+ .map(image -> new Contribution(image, user))
+ .toList()
+ .subscribe(this::saveContributionsToDB, error -> {
+ Timber.e("Failed to fetch contributions: %s", error.getMessage());
+ }));
+ }
+ }
+
+ private void showContributions(@NonNull List contributions) {
+ view.showProgress(false);
+ if (contributions.isEmpty()) {
+ view.showWelcomeTip(true);
+ view.showNoContributionsUI(true);
+ } else {
+ view.showWelcomeTip(false);
+ view.showNoContributionsUI(false);
+ view.setUploadCount(contributions.size());
+ view.showContributions(contributions);
+ this.contributionList.clear();
+ this.contributionList.addAll(contributions);
+ }
+ }
+
+ private void saveContributionsToDB(List contributions) {
+ Timber.e("Fetched: "+contributions.size()+" contributions "+" saving to db");
+ repository.save(contributions).subscribeOn(ioThreadScheduler).subscribe();
+ repository.set("last_fetch_timestamp",System.currentTimeMillis());
+ }
+
+ private boolean shouldFetchContributions() {
+ long lastFetchTimestamp = repository.getLong("last_fetch_timestamp");
+ Timber.d("last fetch timestamp: %s", lastFetchTimestamp);
+ if(lastFetchTimestamp!=0){
+ return System.currentTimeMillis()-lastFetchTimestamp>15*60*100;
+ }
+ Timber.d("should fetch contributions: %s", true);
+ return true;
+ }
+
@Override
public void onDetachView() {
this.view = null;
@@ -52,9 +146,43 @@ public Contribution getContributionsWithTitle(String title) {
*/
@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);
+ }
+
+ @Override
+ public void updateContribution(Contribution contribution) {
compositeDisposable.add(repository
- .deleteContributionFromDB(contribution)
+ .updateContribution(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
+
+ @Override
+ public void fetchMediaDetails(Contribution contribution) {
+ compositeDisposable.add(mediaDataExtractor
+ .getMediaFromFileName(contribution.getFilename())
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(media -> {
+ contribution.setThumbUrl(media.getThumbUrl());
+ updateContribution(contribution);
+ }));
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt
index 02975f3e175..01095dd7ceb 100644
--- a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt
+++ b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt
@@ -10,7 +10,7 @@ import fr.free.nrw.commons.contributions.ContributionDao
* The database for accessing the respective DAOs
*
*/
-@Database(entities = [Contribution::class], version = 5, exportSchema = false)
+@Database(entities = [Contribution::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun contributionDao(): ContributionDao
diff --git a/app/src/main/java/fr/free/nrw/commons/db/Converters.java b/app/src/main/java/fr/free/nrw/commons/db/Converters.java
index 3156f5e2dca..94311c67c57 100644
--- a/app/src/main/java/fr/free/nrw/commons/db/Converters.java
+++ b/app/src/main/java/fr/free/nrw/commons/db/Converters.java
@@ -5,9 +5,9 @@
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import fr.free.nrw.commons.CommonsApplication;
-import fr.free.nrw.commons.contributions.ChunkInfo;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.location.LatLng;
+import fr.free.nrw.commons.media.Depictions;
import fr.free.nrw.commons.upload.WikidataPlace;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.Date;
@@ -84,23 +84,23 @@ public static WikidataPlace stringToWikidataPlace(String wikidataPlace) {
}
@TypeConverter
- public static String chunkInfoToString(ChunkInfo chunkInfo) {
- return writeObjectToString(chunkInfo);
+ public static String depictionListToString(List depictedItems) {
+ return writeObjectToString(depictedItems);
}
@TypeConverter
- public static ChunkInfo stringToChunkInfo(String chunkInfo) {
- return readObjectFromString(chunkInfo, ChunkInfo.class);
+ public static List stringToList(String depictedItems) {
+ return readObjectWithTypeToken(depictedItems, new TypeToken>() {});
}
@TypeConverter
- public static String depictionListToString(List depictedItems) {
+ public static String depictionsToString(Depictions depictedItems) {
return writeObjectToString(depictedItems);
}
@TypeConverter
- public static List stringToList(String depictedItems) {
- return readObjectWithTypeToken(depictedItems, new TypeToken>() {});
+ public static Depictions stringToDepictions(String depictedItems) {
+ return readObjectFromString(depictedItems, Depictions.class);
}
private static String writeObjectToString(Object object) {
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/DepictionModule.java b/app/src/main/java/fr/free/nrw/commons/depictions/DepictionModule.java
new file mode 100644
index 00000000000..a64d829670d
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/DepictionModule.java
@@ -0,0 +1,27 @@
+package fr.free.nrw.commons.depictions;
+
+import dagger.Binds;
+import dagger.Module;
+import fr.free.nrw.commons.depictions.Media.DepictedImagesContract;
+import fr.free.nrw.commons.depictions.Media.DepictedImagesPresenter;
+import fr.free.nrw.commons.depictions.subClass.SubDepictionListContract;
+import fr.free.nrw.commons.depictions.subClass.SubDepictionListPresenter;
+
+/**
+ * The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
+ */
+@Module
+public abstract class DepictionModule {
+
+ @Binds
+ public abstract DepictedImagesContract.UserActionListener bindsDepictedImagesPresenter(
+ DepictedImagesPresenter
+ presenter
+ );
+
+ @Binds
+ public abstract SubDepictionListContract.UserActionListener bindsSubDepictionListPresenter(
+ SubDepictionListPresenter
+ presenter
+ );
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/GridViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/depictions/GridViewAdapter.java
new file mode 100644
index 00000000000..56cb7372860
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/GridViewAdapter.java
@@ -0,0 +1,119 @@
+package fr.free.nrw.commons.depictions;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import com.facebook.drawee.view.SimpleDraweeView;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.R;
+
+/**
+ * Adapter for Items in DepictionDetailsActivity
+ */
+public class GridViewAdapter extends ArrayAdapter {
+
+ private List data;
+
+ public GridViewAdapter(Context context, int layoutResourceId, List data) {
+ super(context, layoutResourceId, data);
+ this.data = data;
+ }
+
+ /**
+ * Adds more item to the list
+ * Its triggered on scrolling down in the list
+ * @param images
+ */
+ public void addItems(List images) {
+ if (data == null) {
+ data = new ArrayList<>();
+ }
+ data.addAll(images);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Check the first item in the new list with old list and returns true if they are same
+ * Its triggered on successful response of the fetch images API.
+ * @param images
+ */
+ public boolean containsAll(List images){
+ if (images == null || images.isEmpty()) {
+ return false;
+ }
+ if (data == null) {
+ data = new ArrayList<>();
+ return false;
+ }
+ if (data.size() == 0) {
+ return false;
+ }
+ String fileName = data.get(0).getFilename();
+ String imageName = images.get(0).getFilename();
+ return imageName.equals(fileName);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return data == null || data.isEmpty();
+ }
+
+ /**
+ * Sets up the UI for the depicted image item
+ * @param position
+ * @param convertView
+ * @param parent
+ * @return
+ */
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(R.layout.layout_depict_image, null);
+ }
+
+ Media item = data.get(position);
+ SimpleDraweeView imageView = convertView.findViewById(R.id.depict_image_view);
+ TextView fileName = convertView.findViewById(R.id.depict_image_title);
+ TextView author = convertView.findViewById(R.id.depict_image_author);
+ fileName.setText(item.getThumbnailTitle());
+ setAuthorView(item, author);
+ imageView.setImageURI(item.getThumbUrl());
+ return convertView;
+ }
+
+ @Nullable
+ @Override
+ public Media getItem(int position) {
+ return data.get(position);
+ }
+
+ /**
+ * Shows author information if its present
+ * @param item
+ * @param author
+ */
+ private void setAuthorView(Media item, TextView author) {
+ if (!TextUtils.isEmpty(item.getCreator())) {
+ String uploadedByTemplate = getContext().getString(R.string.image_uploaded_by);
+
+ String uploadedBy = String.format(Locale.getDefault(), uploadedByTemplate, item.getCreator());
+ author.setText(uploadedBy);
+ } else {
+ author.setVisibility(View.GONE);
+ }
+ }
+
+ }
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/Media/DepictedImagesContract.java b/app/src/main/java/fr/free/nrw/commons/depictions/Media/DepictedImagesContract.java
new file mode 100644
index 00000000000..67e59390c86
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/Media/DepictedImagesContract.java
@@ -0,0 +1,107 @@
+package fr.free.nrw.commons.depictions.Media;
+
+import android.widget.ListAdapter;
+
+import java.util.List;
+
+import fr.free.nrw.commons.BasePresenter;
+import fr.free.nrw.commons.Media;
+
+/**
+ * Contract with which DepictedImagesFragment and its presenter will talk to each other
+ */
+public interface DepictedImagesContract {
+
+ interface View {
+
+ /**
+ * Handles the UI updates for no internet scenario
+ */
+ void handleNoInternet();
+
+ /**
+ * Handles the UI updates for a error scenario
+ */
+ void initErrorView();
+
+ /**
+ * Initializes the adapter with a list of Media objects
+ *
+ * @param mediaList List of new Media to be displayed
+ */
+ void setAdapter(List mediaList);
+
+ /**
+ * Seat caption to the image at the given position
+ */
+ void handleLabelforImage(String caption, int position);
+
+ /**
+ * Display snackbar
+ */
+ void showSnackBar();
+
+ /**
+ * Inform the view that there are no more items to be loaded for this search query
+ * or reset the isLastPage for the current query
+ * @param isLastPage
+ */
+ void setIsLastPage(boolean isLastPage);
+
+ /**
+ * Set visibility of progressbar depending on the boolean value
+ */
+ void progressBarVisible(Boolean value);
+
+ /**
+ * It return an instance of gridView adapter which helps in extracting media details
+ * used by the gridView
+ *
+ * @return GridView Adapter
+ */
+ ListAdapter getAdapter();
+
+ /**
+ * adds list to adapter
+ */
+ void addItemsToAdapter(List media);
+
+ /**
+ * Sets loading status depending on the boolean value
+ */
+ void setLoadingStatus(Boolean value);
+
+ /**
+ * Handles the success scenario
+ * On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
+ *
+ * @param collection List of new Media to be displayed
+ */
+ void handleSuccess(List collection);
+
+ }
+
+ interface UserActionListener extends BasePresenter {
+
+ /**
+ * Checks for internet connection and then initializes the grid view with first 10 images of that depiction
+ */
+ void initList(String entityId);
+
+ /**
+ * Fetches more images for the item and adds it to the grid view adapter
+ */
+ void fetchMoreImages();
+
+ /**
+ * fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available)
+ * else show filename
+ */
+ void replaceTitlesWithCaptions(String title, int position);
+
+ /**
+ * add items to query list
+ */
+ void addItemsToQueryList(List collection);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/Media/DepictedImagesFragment.java b/app/src/main/java/fr/free/nrw/commons/depictions/Media/DepictedImagesFragment.java
new file mode 100644
index 00000000000..1c1aeaef333
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/Media/DepictedImagesFragment.java
@@ -0,0 +1,267 @@
+package fr.free.nrw.commons.depictions.Media;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.GridView;
+import android.widget.ListAdapter;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import dagger.android.support.DaggerFragment;
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.depictions.GridViewAdapter;
+import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
+import fr.free.nrw.commons.utils.NetworkUtils;
+import fr.free.nrw.commons.utils.ViewUtil;
+import java.util.List;
+import javax.inject.Inject;
+import timber.log.Timber;
+
+/**
+ * Fragment for showing image list after selected an item from SearchActivity In Explore
+ */
+public class DepictedImagesFragment extends DaggerFragment implements DepictedImagesContract.View {
+
+
+ public static final String PAGE_ID_PREFIX = "M";
+ @BindView(R.id.statusMessage)
+ TextView statusTextView;
+ @BindView(R.id.loadingImagesProgressBar)
+ ProgressBar progressBar;
+ @BindView(R.id.depicts_image_list)
+ GridView gridView;
+ @BindView(R.id.parentLayout)
+ RelativeLayout parentLayout;
+ @Inject
+ DepictedImagesPresenter presenter;
+ private GridViewAdapter gridAdapter;
+ private String entityId = null;
+ private boolean isLastPage;
+ private boolean isLoading = true;
+ private int mediaSize = 0;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.fragment_depict_image, container, false);
+ ButterKnife.bind(this, v);
+ presenter.onAttachView(this);
+ return v;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
+ initViews();
+ }
+
+ /**
+ * Initializes the UI elements for the fragment
+ * Setup the grid view to and scroll listener for it
+ */
+ private void initViews() {
+ String depictsName = getArguments().getString("wikidataItemName");
+ entityId = getArguments().getString("entityId");
+ if (getArguments() != null && depictsName != null) {
+ initList();
+ setScrollListener();
+ }
+ }
+
+ private void initList() {
+ presenter.initList(entityId);
+ if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
+ handleNoInternet();
+ } else {
+ presenter.initList(entityId);
+ }
+ }
+
+ /**
+ * Handles the UI updates for no internet scenario
+ */
+ @Override
+ public void handleNoInternet() {
+ progressBar.setVisibility(GONE);
+ if (gridAdapter == null || gridAdapter.isEmpty()) {
+ statusTextView.setVisibility(VISIBLE);
+ statusTextView.setText(getString(R.string.no_internet));
+ } else {
+ ViewUtil.showShortSnackbar(parentLayout, R.string.no_internet);
+ }
+ }
+
+ /**
+ * Handles the UI updates for a error scenario
+ */
+ @Override
+ public void initErrorView() {
+ progressBar.setVisibility(GONE);
+ if (gridAdapter == null || gridAdapter.isEmpty()) {
+ statusTextView.setVisibility(VISIBLE);
+ statusTextView.setText(getString(R.string.no_images_found));
+ } else {
+ statusTextView.setVisibility(GONE);
+ }
+ }
+
+ /**
+ * Sets the scroll listener for the grid view so that more images are fetched when the user scrolls down
+ * Checks if the item has more images before loading
+ * Also checks whether images are currently being fetched before triggering another request
+ */
+ private void setScrollListener() {
+ gridView.setOnScrollListener(new AbsListView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ if (!isLastPage && !isLoading && (firstVisibleItem + visibleItemCount >= totalItemCount)) {
+ isLoading = true;
+ if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
+ handleNoInternet();
+ } else {
+ presenter.fetchMoreImages();
+ }
+ }
+ if (isLastPage) {
+ progressBar.setVisibility(GONE);
+ }
+ }
+ });
+ }
+
+ /**
+ * Seat caption to the image at the given position
+ */
+ @Override
+ public void handleLabelforImage(String caption, int position) {
+ if (!caption.trim().equals(getString(R.string.detail_caption_empty))) {
+ gridAdapter.getItem(position).setThumbnailTitle(caption);
+ gridAdapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Display snackbar
+ */
+ @Override
+ public void showSnackBar() {
+ ViewUtil.showShortSnackbar(parentLayout, R.string.error_loading_images);
+ }
+
+ /**
+ * Set visibility of progressbar depending on the boolean value
+ */
+ @Override
+ public void progressBarVisible(Boolean value) {
+ if (value) {
+ progressBar.setVisibility(VISIBLE);
+ } else {
+ progressBar.setVisibility(GONE);
+ }
+ }
+
+ /**
+ * It return an instance of gridView adapter which helps in extracting media details
+ * used by the gridView
+ *
+ * @return GridView Adapter
+ */
+ @Override
+ public ListAdapter getAdapter() {
+ return gridAdapter;
+ }
+
+ /**
+ * Initializes the adapter with a list of Media objects
+ *
+ * @param mediaList List of new Media to be displayed
+ */
+ @Override
+ public void setAdapter(List mediaList) {
+ gridAdapter = new fr.free.nrw.commons.depictions.GridViewAdapter(getContext(), R.layout.layout_depict_image, mediaList);
+ gridView.setAdapter(gridAdapter);
+ }
+
+ /**
+ * adds list to adapter
+ */
+ @Override
+ public void addItemsToAdapter(List media) {
+ gridAdapter.addAll(media);
+ gridAdapter.notifyDataSetChanged();
+ }
+
+ /**
+ * Sets loading status depending on the boolean value
+ */
+ @Override
+ public void setLoadingStatus(Boolean value) {
+ if (!value) {
+ statusTextView.setVisibility(GONE);
+ }
+ isLoading = value;
+ }
+
+ /**
+ * Inform the view that there are no more items to be loaded for this search query
+ * or reset the isLastPage for the current query
+ * @param isLastPage
+ */
+ @Override
+ public void setIsLastPage(boolean isLastPage) {
+ this.isLastPage=isLastPage;
+ progressBar.setVisibility(GONE);
+ }
+
+
+ /**
+ * Handles the success scenario
+ * On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
+ *
+ * @param collection List of new Media to be displayed
+ */
+ @Override
+ public void handleSuccess(List collection) {
+ presenter.addItemsToQueryList(collection);
+ if (gridAdapter == null) {
+ setAdapter(collection);
+ } else {
+ if (gridAdapter.containsAll(collection)) {
+ return;
+ }
+ gridAdapter.addItems(collection);
+
+ try {
+ ((WikidataItemDetailsActivity) getContext()).viewPagerNotifyDataSetChanged();
+ } catch (RuntimeException e) {
+ Timber.e(e);
+ }
+ }
+ progressBar.setVisibility(GONE);
+ isLoading = false;
+ statusTextView.setVisibility(GONE);
+ for (Media media : collection) {
+ final String pageId = media.getPageId();
+ if (pageId != null) {
+ presenter.replaceTitlesWithCaptions(PAGE_ID_PREFIX + pageId, mediaSize++);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/Media/DepictedImagesPresenter.java b/app/src/main/java/fr/free/nrw/commons/depictions/Media/DepictedImagesPresenter.java
new file mode 100644
index 00000000000..372aa60a435
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/Media/DepictedImagesPresenter.java
@@ -0,0 +1,159 @@
+package fr.free.nrw.commons.depictions.Media;
+
+import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
+import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
+
+import android.annotation.SuppressLint;
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.explore.depictions.DepictsClient;
+import fr.free.nrw.commons.kvstore.JsonKvStore;
+import fr.free.nrw.commons.media.MediaClient;
+import io.reactivex.Scheduler;
+import io.reactivex.disposables.CompositeDisposable;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.List;
+import javax.inject.Inject;
+import javax.inject.Named;
+import timber.log.Timber;
+
+/**
+ * Presenter for DepictedImagesFragment
+ */
+public class DepictedImagesPresenter implements DepictedImagesContract.UserActionListener {
+
+ private static final DepictedImagesContract.View DUMMY = (DepictedImagesContract.View) Proxy
+ .newProxyInstance(
+ DepictedImagesContract.View.class.getClassLoader(),
+ new Class[]{DepictedImagesContract.View.class},
+ (proxy, method, methodArgs) -> null);
+ DepictsClient depictsClient;
+ MediaClient mediaClient;
+ @Named("default_preferences")
+ JsonKvStore depictionKvStore;
+ private final Scheduler ioScheduler;
+ private final Scheduler mainThreadScheduler;
+ private DepictedImagesContract.View view = DUMMY;
+ private CompositeDisposable compositeDisposable = new CompositeDisposable();
+ /**
+ * Wikibase enitityId for the depicted Item
+ * Ex: Q9394
+ */
+ private String entityId = null;
+ private List queryList = new ArrayList<>();
+
+ @Inject
+ public DepictedImagesPresenter(@Named("default_preferences") JsonKvStore depictionKvStore, DepictsClient depictsClient, MediaClient mediaClient, @Named(IO_THREAD) Scheduler ioScheduler,
+ @Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
+ this.depictionKvStore = depictionKvStore;
+ this.depictsClient = depictsClient;
+ this.ioScheduler = ioScheduler;
+ this.mainThreadScheduler = mainThreadScheduler;
+ this.mediaClient = mediaClient;
+ }
+
+ @Override
+ public void onAttachView(DepictedImagesContract.View view) {
+ this.view = view;
+ }
+
+ @Override
+ public void onDetachView() {
+ this.view = DUMMY;
+ }
+
+ /**
+ * Checks for internet connection and then initializes the grid view with first 10 images of that depiction
+ */
+ @SuppressLint("CheckResult")
+ @Override
+ public void initList(String entityId) {
+ view.setLoadingStatus(true);
+ view.progressBarVisible(true);
+ view.setIsLastPage(false);
+ compositeDisposable.add(depictsClient.fetchImagesForDepictedItem(entityId, 0)
+ .subscribeOn(ioScheduler)
+ .observeOn(mainThreadScheduler)
+ .subscribe(this::handleSuccess, this::handleError));
+ }
+
+ /**
+ * Fetches more images for the item and adds it to the grid view adapter
+ */
+ @SuppressLint("CheckResult")
+ @Override
+ public void fetchMoreImages() {
+ view.progressBarVisible(true);
+ compositeDisposable.add(depictsClient.fetchImagesForDepictedItem(entityId, queryList.size())
+ .subscribeOn(ioScheduler)
+ .observeOn(mainThreadScheduler)
+ .subscribe(this::handlePaginationSuccess, this::handleError));
+ }
+
+ /**
+ * Handles the success scenario
+ * it initializes the recycler view by adding items to the adapter
+ */
+ private void handlePaginationSuccess(List media) {
+ queryList.addAll(media);
+ view.progressBarVisible(false);
+ view.addItemsToAdapter(media);
+ }
+
+ /**
+ * Logs and handles API error scenario
+ *
+ * @param throwable
+ */
+ public void handleError(Throwable throwable) {
+ Timber.e(throwable, "Error occurred while loading images inside items");
+ try {
+ view.initErrorView();
+ view.showSnackBar();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ /**
+ * Handles the success scenario
+ * On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
+ * @param collection List of new Media to be displayed
+ */
+ public void handleSuccess(List collection) {
+ if (collection == null || collection.isEmpty()) {
+ if (queryList.isEmpty()) {
+ view.initErrorView();
+ } else {
+ view.setIsLastPage(true);
+ }
+ } else {
+ this.queryList.addAll(collection);
+ view.handleSuccess(collection);
+ }
+ }
+
+ /**
+ * fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available)
+ * else show filename
+ */
+ @Override
+ public void replaceTitlesWithCaptions(String wikibaseIdentifier, int position) {
+ compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier)
+ .subscribeOn(ioScheduler)
+ .observeOn(mainThreadScheduler)
+ .subscribe(caption -> {
+ view.handleLabelforImage(caption, position);
+ }));
+
+ }
+
+ /**
+ * add items to query list
+ */
+ @Override
+ public void addItemsToQueryList(List collection) {
+ queryList.addAll(collection);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/WikidataItemDetailsActivity.java b/app/src/main/java/fr/free/nrw/commons/depictions/WikidataItemDetailsActivity.java
new file mode 100644
index 00000000000..40ea07995f6
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/WikidataItemDetailsActivity.java
@@ -0,0 +1,204 @@
+package fr.free.nrw.commons.depictions;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.FrameLayout;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.viewpager.widget.ViewPager;
+
+import com.google.android.material.tabs.TabLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment;
+import fr.free.nrw.commons.depictions.subClass.SubDepictionListFragment;
+import fr.free.nrw.commons.explore.ViewPagerAdapter;
+import fr.free.nrw.commons.media.MediaDetailPagerFragment;
+import fr.free.nrw.commons.theme.NavigationBaseActivity;
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
+
+/**
+ * Activity to show depiction media, parent classes and child classes of depicted items in Explore
+ */
+public class WikidataItemDetailsActivity extends NavigationBaseActivity implements MediaDetailPagerFragment.MediaDetailProvider, AdapterView.OnItemClickListener {
+ private FragmentManager supportFragmentManager;
+ private DepictedImagesFragment depictionImagesListFragment;
+ private MediaDetailPagerFragment mediaDetailPagerFragment;
+ /**
+ * Name of the depicted item
+ * Ex: Rabbit
+ */
+ private String wikidataItemName;
+ @BindView(R.id.mediaContainer)
+ FrameLayout mediaContainer;
+ @BindView(R.id.tab_layout)
+ TabLayout tabLayout;
+ @BindView(R.id.viewPager)
+ ViewPager viewPager;
+
+ ViewPagerAdapter viewPagerAdapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_wikidata_item_details);
+ ButterKnife.bind(this);
+ supportFragmentManager = getSupportFragmentManager();
+ viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
+ viewPager.setAdapter(viewPagerAdapter);
+ viewPager.setOffscreenPageLimit(2);
+ tabLayout.setupWithViewPager(viewPager);
+ setTabs();
+ setPageTitle();
+ initDrawer();
+ forceInitBackButton();
+ }
+
+ /**
+ * Gets the passed wikidataItemName from the intents and displays it as the page title
+ */
+ private void setPageTitle() {
+ if (getIntent() != null && getIntent().getStringExtra("wikidataItemName") != null) {
+ setTitle(getIntent().getStringExtra("wikidataItemName"));
+ }
+ }
+
+ /**
+ * This method is called on success of API call for featured Images.
+ * The viewpager will notified that number of items have changed.
+ */
+ public void viewPagerNotifyDataSetChanged() {
+ if (mediaDetailPagerFragment !=null){
+ mediaDetailPagerFragment.notifyDataSetChanged();
+ }
+ }
+
+
+ /**
+ * This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab,
+ * Set the fragments according to the tab selected in the viewPager.
+ */
+ private void setTabs() {
+ List fragmentList = new ArrayList<>();
+ List titleList = new ArrayList<>();
+ depictionImagesListFragment = new DepictedImagesFragment();
+ SubDepictionListFragment subDepictionListFragment = new SubDepictionListFragment();
+ SubDepictionListFragment parentDepictionListFragment = new SubDepictionListFragment();
+ wikidataItemName = getIntent().getStringExtra("wikidataItemName");
+ String entityId = getIntent().getStringExtra("entityId");
+ if (getIntent() != null && wikidataItemName != null) {
+ Bundle arguments = new Bundle();
+ arguments.putString("wikidataItemName", wikidataItemName);
+ arguments.putString("entityId", entityId);
+ arguments.putBoolean("isParentClass", false);
+ depictionImagesListFragment.setArguments(arguments);
+ subDepictionListFragment.setArguments(arguments);
+ Bundle parentClassArguments = new Bundle();
+ parentClassArguments.putString("wikidataItemName", wikidataItemName);
+ parentClassArguments.putString("entityId", entityId);
+ parentClassArguments.putBoolean("isParentClass", true);
+ parentDepictionListFragment.setArguments(parentClassArguments);
+ }
+ fragmentList.add(depictionImagesListFragment);
+ titleList.add(getResources().getString(R.string.title_for_media));
+ fragmentList.add(subDepictionListFragment);
+ titleList.add(getResources().getString(R.string.title_for_child_classes));
+ fragmentList.add(parentDepictionListFragment);
+ titleList.add(getResources().getString(R.string.title_for_parent_classes));
+ viewPagerAdapter.setTabData(fragmentList, titleList);
+ viewPager.setOffscreenPageLimit(2);
+ viewPagerAdapter.notifyDataSetChanged();
+
+ }
+
+ /**
+ * Shows media detail fragment when user clicks on any image in the list
+ */
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ tabLayout.setVisibility(View.GONE);
+ viewPager.setVisibility(View.GONE);
+ mediaContainer.setVisibility(View.VISIBLE);
+ if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
+ // set isFeaturedImage true for featured images, to include author field on media detail
+ mediaDetailPagerFragment = new MediaDetailPagerFragment(false, true);
+ FragmentManager supportFragmentManager = getSupportFragmentManager();
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.mediaContainer, mediaDetailPagerFragment)
+ .addToBackStack(null)
+ .commit();
+ supportFragmentManager.executePendingTransactions();
+ }
+ mediaDetailPagerFragment.showImage(position);
+ forceInitBackButton();
+ }
+
+ /**
+ * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
+ * @param i It is the index of which media object is to be returned which is same as
+ * current index of viewPager.
+ * @return Media Object
+ */
+ @Override
+ public Media getMediaAtPosition(int i) {
+ if (depictionImagesListFragment.getAdapter() == null) {
+ // not yet ready to return data
+ return null;
+ } else {
+ return (Media) depictionImagesListFragment.getAdapter().getItem(i);
+ }
+ }
+
+ /**
+ * This method is called on backPressed of anyFragment in the activity.
+ * If condition is called when mediaDetailFragment is opened.
+ */
+ @Override
+ public void onBackPressed() {
+ if (supportFragmentManager.getBackStackEntryCount() == 1){
+ // back to search so show search toolbar and hide navigation toolbar
+ tabLayout.setVisibility(View.VISIBLE);
+ viewPager.setVisibility(View.VISIBLE);
+ mediaContainer.setVisibility(View.GONE);
+ }
+ super.onBackPressed();
+ }
+
+ /**
+ * This method is called on from getCount of MediaDetailPagerFragment
+ * The viewpager will contain same number of media items as that of media elements in adapter.
+ * @return Total Media count in the adapter
+ */
+ @Override
+ public int getTotalMediaCount() {
+ if (depictionImagesListFragment.getAdapter() == null) {
+ return 0;
+ }
+ return depictionImagesListFragment.getAdapter().getCount();
+ }
+
+ /**
+ * Consumers should be simply using this method to use this activity.
+ *
+ * @param context A Context of the application package implementing this class.
+ * @param depictedItem Name of the depicts for displaying its details
+ */
+ public static void startYourself(Context context, DepictedItem depictedItem) {
+ Intent intent = new Intent(context, WikidataItemDetailsActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ intent.putExtra("wikidataItemName", depictedItem.getName());
+ intent.putExtra("entityId", depictedItem.getId());
+ context.startActivity(intent);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/models/Continue.java b/app/src/main/java/fr/free/nrw/commons/depictions/models/Continue.java
new file mode 100644
index 00000000000..2365391a567
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/models/Continue.java
@@ -0,0 +1,57 @@
+package fr.free.nrw.commons.depictions.models;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Model class for object obtained while parsing depiction response
+ */
+public class Continue {
+
+ @SerializedName("sroffset")
+ @Expose
+ private Integer sroffset;
+ @SerializedName("continue")
+ @Expose
+ private String _continue;
+
+ /**
+ * No args constructor for use in serialization
+ *
+ */
+ public Continue() {
+ }
+
+ /**
+ *
+ * @param sroffset
+ * @param _continue
+ */
+ public Continue(Integer sroffset, String _continue) {
+ super();
+ this.sroffset = sroffset;
+ this._continue = _continue;
+ }
+
+ /**
+ * gets sroffset from Continue object
+ */
+ public Integer getSroffset() {
+ return sroffset;
+ }
+
+ public void setSroffset(Integer sroffset) {
+ this.sroffset = sroffset;
+ }
+
+ /**
+ * gets continue string from Continue object
+ */
+ public String getContinue() {
+ return _continue;
+ }
+
+ public void setContinue(String _continue) {
+ this._continue = _continue;
+ }
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/models/DepictionResponse.java b/app/src/main/java/fr/free/nrw/commons/depictions/models/DepictionResponse.java
new file mode 100644
index 00000000000..15ea61d50dd
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/models/DepictionResponse.java
@@ -0,0 +1,73 @@
+package fr.free.nrw.commons.depictions.models;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Model class for list of depicted images obtained by fetching using depiction entity
+ */
+public class DepictionResponse {
+
+ @SerializedName("batchcomplete")
+ @Expose
+ private String batchcomplete;
+ @SerializedName("continue")
+ @Expose
+ private Continue _continue;
+ @SerializedName("query")
+ @Expose
+ private Query query;
+
+ /**
+ * No args constructor for use in serialization
+ *
+ */
+ public DepictionResponse() {
+ }
+
+ /**
+ *
+ * @param query
+ * @param batchcomplete
+ * @param _continue
+ */
+ public DepictionResponse(String batchcomplete, Continue _continue, Query query) {
+ super();
+ this.batchcomplete = batchcomplete;
+ this._continue = _continue;
+ this.query = query;
+ }
+
+ /**
+ * returns batchcomplete string from DepictionResponse object
+ */
+ public String getBatchcomplete() {
+ return batchcomplete;
+ }
+
+ public void setBatchcomplete(String batchcomplete) {
+ this.batchcomplete = batchcomplete;
+ }
+
+ /**
+ * returns continue object from DepictionResponse object
+ */
+ public Continue getContinue() {
+ return _continue;
+ }
+
+ public void setContinue(Continue _continue) {
+ this._continue = _continue;
+ }
+
+ /**
+ * returns query object from DepictionResponse object
+ */
+ public Query getQuery() {
+ return query;
+ }
+
+ public void setQuery(Query query) {
+ this.query = query;
+ }
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/models/Query.java b/app/src/main/java/fr/free/nrw/commons/depictions/models/Query.java
new file mode 100644
index 00000000000..358527fe762
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/models/Query.java
@@ -0,0 +1,60 @@
+package fr.free.nrw.commons.depictions.models;
+import java.util.List;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Model class for object obtained while parsing depiction response
+ *
+ * the getSearch() function is used to parse media
+ */
+public class Query {
+
+ @SerializedName("searchinfo")
+ @Expose
+ private Searchinfo searchinfo;
+ @SerializedName("search")
+ @Expose
+ private List search = null;
+
+ /**
+ * No args constructor for use in serialization
+ *
+ */
+ public Query() {
+ }
+
+ /**
+ *
+ * @param search
+ * @param searchinfo
+ */
+ public Query(Searchinfo searchinfo, List search) {
+ super();
+ this.searchinfo = searchinfo;
+ this.search = search;
+ }
+
+ /**
+ * return searchInfo
+ */
+ public Searchinfo getSearchinfo() {
+ return searchinfo;
+ }
+
+ public void setSearchinfo(Searchinfo searchinfo) {
+ this.searchinfo = searchinfo;
+ }
+
+ /**
+ * the getSearch() function is used to parse media
+ */
+ public List getSearch() {
+ return search;
+ }
+
+ public void setSearch(List search) {
+ this.search = search;
+ }
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/models/Search.java b/app/src/main/java/fr/free/nrw/commons/depictions/models/Search.java
new file mode 100644
index 00000000000..fbff3616ae7
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/models/Search.java
@@ -0,0 +1,140 @@
+package fr.free.nrw.commons.depictions.models;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Model class for object obtained while parsing depiction response
+ * this class contains all the details of for the media object
+ */
+
+public class Search {
+
+ @SerializedName("ns")
+ @Expose
+ private Integer ns;
+ @SerializedName("title")
+ @Expose
+ private String title;
+ @SerializedName("pageid")
+ @Expose
+ private Integer pageid;
+ @SerializedName("size")
+ @Expose
+ private Integer size;
+ @SerializedName("wordcount")
+ @Expose
+ private Integer wordcount;
+ @SerializedName("snippet")
+ @Expose
+ private String snippet;
+ @SerializedName("timestamp")
+ @Expose
+ private String timestamp;
+
+ /**
+ * No args constructor for use in serialization
+ *
+ */
+ public Search() {
+ }
+
+ /**
+ *
+ * @param timestamp
+ * @param title
+ * @param ns
+ * @param snippet
+ * @param wordcount
+ * @param size
+ * @param pageid
+ */
+ public Search(Integer ns, String title, Integer pageid, Integer size, Integer wordcount, String snippet, String timestamp) {
+ super();
+ this.ns = ns;
+ this.title = title;
+ this.pageid = pageid;
+ this.size = size;
+ this.wordcount = wordcount;
+ this.snippet = snippet;
+ this.timestamp = timestamp;
+ }
+
+ /**
+ * returns ns int from Search object
+ */
+ public Integer getNs() {
+ return ns;
+ }
+
+ public void setNs(Integer ns) {
+ this.ns = ns;
+ }
+
+ /**
+ * returns title string from Search object
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ /**
+ * returns pageid int from Search object
+ */
+ public Integer getPageid() {
+ return pageid;
+ }
+
+ public void setPageid(Integer pageid) {
+ this.pageid = pageid;
+ }
+
+ /**
+ * returns size int from Search object
+ */
+ public Integer getSize() {
+ return size;
+ }
+
+ public void setSize(Integer size) {
+ this.size = size;
+ }
+
+ /**
+ * returns wordcount int from Search object
+ */
+ public Integer getWordcount() {
+ return wordcount;
+ }
+
+ public void setWordcount(Integer wordcount) {
+ this.wordcount = wordcount;
+ }
+
+ /**
+ * returns snippet String from Search object
+ */
+ public String getSnippet() {
+ return snippet;
+ }
+
+ public void setSnippet(String snippet) {
+ this.snippet = snippet;
+ }
+
+ /**
+ * returns ns int from Search object
+ */
+ public String getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(String timestamp) {
+ this.timestamp = timestamp;
+ }
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/models/Searchinfo.java b/app/src/main/java/fr/free/nrw/commons/depictions/models/Searchinfo.java
new file mode 100644
index 00000000000..f04f62d0eb1
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/models/Searchinfo.java
@@ -0,0 +1,42 @@
+package fr.free.nrw.commons.depictions.models;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Model class for object obtained while parsing query object
+ */
+
+public class Searchinfo {
+
+ @SerializedName("totalhits")
+ @Expose
+ private Integer totalhits;
+
+ /**
+ * No args constructor for use in serialization
+ *
+ */
+ public Searchinfo() {
+ }
+
+ /**
+ *
+ * @param totalhits
+ */
+ public Searchinfo(Integer totalhits) {
+ super();
+ this.totalhits = totalhits;
+ }
+
+ /**
+ * returns "totalhint" integer in SearchInfo object
+ */
+ public Integer getTotalhits() {
+ return totalhits;
+ }
+
+ public void setTotalhits(Integer totalhits) {
+ this.totalhits = totalhits;
+ }
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListContract.java b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListContract.java
new file mode 100644
index 00000000000..0973149cc02
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListContract.java
@@ -0,0 +1,39 @@
+package fr.free.nrw.commons.depictions.subClass;
+
+import java.io.IOException;
+import java.util.List;
+
+import fr.free.nrw.commons.BasePresenter;
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
+
+/**
+ * The contract with which SubDepictionListFragment and its presenter would talk to each other
+ */
+public interface SubDepictionListContract {
+
+ interface View {
+
+ void onImageUrlFetched(String response, int position);
+
+ void onSuccess(List mediaList);
+
+ void initErrorView();
+
+ void showSnackbar();
+
+ void setIsLastPage(boolean b);
+
+ boolean isParentClass();
+ }
+
+ interface UserActionListener extends BasePresenter {
+
+ void saveQuery();
+
+ void fetchThumbnailForEntityId(String entityId, int position);
+
+ void initSubDepictionList(String qid, Boolean isParentClass) throws IOException;
+
+ String getQuery();
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListFragment.java b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListFragment.java
new file mode 100644
index 00000000000..5c8a3c045d4
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListFragment.java
@@ -0,0 +1,190 @@
+package fr.free.nrw.commons.depictions.subClass;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.pedrogomez.renderers.RVRendererAdapter;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+import javax.inject.Inject;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import dagger.android.support.DaggerFragment;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
+import fr.free.nrw.commons.explore.depictions.SearchDepictionsAdapterFactory;
+import fr.free.nrw.commons.explore.depictions.SearchDepictionsRenderer;
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
+import fr.free.nrw.commons.utils.NetworkUtils;
+import fr.free.nrw.commons.utils.ViewUtil;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+/**
+ * Fragment for parent classes and child classes of Depicted items in Explore
+ */
+public class SubDepictionListFragment extends DaggerFragment implements SubDepictionListContract.View {
+
+ @BindView(R.id.imagesListBox)
+ RecyclerView depictionsRecyclerView;
+ @BindView(R.id.imageSearchInProgress)
+ ProgressBar progressBar;
+ @BindView(R.id.imagesNotFound)
+ TextView depictionNotFound;
+ @BindView(R.id.bottomProgressBar)
+ ProgressBar bottomProgressBar;
+ /**
+ * Keeps a record of whether current instance of the fragment if of SubClass or ParentClass
+ */
+ private boolean isParentClass = false;
+ private RVRendererAdapter depictionsAdapter;
+ /**
+ * Used by scroll state listener, when hasMoreImages is false scrolling does not fetches any more images
+ */
+ private boolean hasMoreImages = true;
+ RecyclerView.LayoutManager layoutManager;
+ /**
+ * Stores entityId for the depiction
+ */
+ private String entityId;
+ /**
+ * Stores name of the depiction searched
+ */
+ private String depictsName;
+
+ @Inject SubDepictionListPresenter presenter;
+
+ private final SearchDepictionsAdapterFactory adapterFactory = new SearchDepictionsAdapterFactory(new SearchDepictionsRenderer.DepictCallback() {
+ @Override
+ public void depictsClicked(DepictedItem item) {
+ // Open SubDepiction Details page
+ getActivity().finish();
+ WikidataItemDetailsActivity.startYourself(getContext(), item);
+ }
+
+ @Override
+ public void fetchThumbnailUrlForEntity(String entityId, int position) {
+ presenter.fetchThumbnailForEntityId(entityId, position);
+ }
+
+ });
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ }
+
+ private void initViews() {
+ if (getArguments() != null) {
+ depictsName = getArguments().getString("wikidataItemName");
+ entityId = getArguments().getString("entityId");
+ isParentClass = getArguments().getBoolean("isParentClass");
+ if (entityId != null) {
+ initList(entityId, isParentClass);
+ }
+ }
+ }
+
+ private void initList(String qid, Boolean isParentClass) {
+ if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
+ handleNoInternet();
+ } else {
+ progressBar.setVisibility(View.VISIBLE);
+ try {
+ presenter.initSubDepictionList(qid, isParentClass);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.fragment_browse_image, container, false);
+ ButterKnife.bind(this, v);
+ presenter.onAttachView(this);
+ isParentClass = false;
+ depictionNotFound.setVisibility(GONE);
+ if (getActivity().getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_PORTRAIT) {
+ layoutManager = new LinearLayoutManager(getContext());
+ } else {
+ layoutManager = new GridLayoutManager(getContext(), 2);
+ }
+ initViews();
+ depictionsRecyclerView.setLayoutManager(layoutManager);
+ depictionsAdapter = adapterFactory.create();
+ depictionsRecyclerView.setAdapter(depictionsAdapter);
+ return v;
+ }
+
+ private void handleNoInternet() {
+ progressBar.setVisibility(GONE);
+ ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.no_internet);
+ }
+
+ @Override
+ public void onImageUrlFetched(String response, int position) {
+ depictionsAdapter.getItem(position).setImageUrl(response);
+ depictionsAdapter.notifyItemChanged(position);
+ }
+
+ @Override
+ public void onSuccess(List mediaList) {
+ hasMoreImages = false;
+ progressBar.setVisibility(View.GONE);
+ depictionNotFound.setVisibility(GONE);
+ bottomProgressBar.setVisibility(GONE);
+ int itemCount=layoutManager.getItemCount();
+ depictionsAdapter.addAll(mediaList);
+ depictionsRecyclerView.getRecycledViewPool().clear();
+ if(itemCount!=0) {
+ depictionsAdapter.notifyItemRangeInserted(itemCount, mediaList.size()-1);
+ }else{
+ depictionsAdapter.notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void initErrorView() {
+ hasMoreImages = false;
+ progressBar.setVisibility(GONE);
+ bottomProgressBar.setVisibility(GONE);
+ depictionNotFound.setVisibility(VISIBLE);
+ String no_depiction = getString(isParentClass? R.string.no_parent_classes: R.string.no_child_classes);
+ depictionNotFound.setText(String.format(Locale.getDefault(), no_depiction, depictsName));
+
+ }
+
+ @Override
+ public void showSnackbar() {
+ ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.error_loading_depictions);
+ }
+
+ @Override
+ public void setIsLastPage(boolean b) {
+ hasMoreImages = !b;
+ }
+
+ @Override
+ public boolean isParentClass() {
+ return isParentClass;
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListPresenter.java b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListPresenter.java
new file mode 100644
index 00000000000..1f9f4fc7656
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/SubDepictionListPresenter.java
@@ -0,0 +1,158 @@
+package fr.free.nrw.commons.depictions.subClass;
+
+import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
+import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
+
+import fr.free.nrw.commons.explore.depictions.DepictsClient;
+import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
+import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
+import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
+import io.reactivex.Scheduler;
+import io.reactivex.disposables.CompositeDisposable;
+import java.io.IOException;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
+import javax.inject.Named;
+import timber.log.Timber;
+
+/**
+* Presenter for parent classes and child classes of Depicted items in Explore
+ */
+public class SubDepictionListPresenter implements SubDepictionListContract.UserActionListener {
+
+ /**
+ * This creates a dynamic proxy instance of the class,
+ * proxy is to control access to the target object
+ * here our target object is the view.
+ * Thus we when onDettach method of fragment is called we replace the binding of view to our object with the proxy instance
+ */
+ private static final SubDepictionListContract.View DUMMY = (SubDepictionListContract.View) Proxy
+ .newProxyInstance(
+ SubDepictionListContract.View.class.getClassLoader(),
+ new Class[]{SubDepictionListContract.View.class},
+ (proxy, method, methodArgs) -> null);
+
+ private final Scheduler ioScheduler;
+ private final Scheduler mainThreadScheduler;
+ private SubDepictionListContract.View view = DUMMY;
+ RecentSearchesDao recentSearchesDao;
+ /**
+ * Value of the search query
+ */
+ public String query;
+ protected CompositeDisposable compositeDisposable = new CompositeDisposable();
+ DepictsClient depictsClient;
+ private static int TIMEOUT_SECONDS = 15;
+ private List queryList = new ArrayList<>();
+ OkHttpJsonApiClient okHttpJsonApiClient;
+ /**
+ * variable used to record the number of API calls already made for fetching Thumbnails
+ */
+ private int size = 0;
+
+ @Inject
+ public SubDepictionListPresenter(RecentSearchesDao recentSearchesDao, DepictsClient depictsClient, OkHttpJsonApiClient okHttpJsonApiClient, @Named(IO_THREAD) Scheduler ioScheduler,
+ @Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
+ this.recentSearchesDao = recentSearchesDao;
+ this.ioScheduler = ioScheduler;
+ this.mainThreadScheduler = mainThreadScheduler;
+ this.depictsClient = depictsClient;
+ this.okHttpJsonApiClient = okHttpJsonApiClient;
+ }
+ @Override
+ public void onAttachView(SubDepictionListContract.View view) {
+ this.view = view;
+ }
+
+ @Override
+ public void onDetachView() {
+ this.view = DUMMY;
+ }
+
+ /**
+ * Store the current query in Recent searches
+ */
+ @Override
+ public void saveQuery() {
+ RecentSearch recentSearch = recentSearchesDao.find(query);
+
+ // Newly searched query...
+ if (recentSearch == null) {
+ recentSearch = new RecentSearch(null, query, new Date());
+ } else {
+ recentSearch.setLastSearched(new Date());
+ }
+ recentSearchesDao.save(recentSearch);
+ }
+
+ /**
+ * Calls Wikibase APIs to fetch Thumbnail image for a given wikidata item
+ */
+ @Override
+ public void fetchThumbnailForEntityId(String entityId, int position) {
+ compositeDisposable.add(depictsClient.getP18ForItem(entityId)
+ .subscribeOn(ioScheduler)
+ .observeOn(mainThreadScheduler)
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .subscribe(response -> {
+ view.onImageUrlFetched(response,position);
+ }));
+ }
+
+ @Override
+ public void initSubDepictionList(String qid, Boolean isParentClass) throws IOException {
+ size = 0;
+ if (isParentClass) {
+ compositeDisposable.add(okHttpJsonApiClient.getParentQIDs(qid)
+ .subscribeOn(ioScheduler)
+ .observeOn(mainThreadScheduler)
+ .subscribe(this::handleSuccess, this::handleError));
+ } else {
+ compositeDisposable.add(okHttpJsonApiClient.getChildQIDs(qid)
+ .subscribeOn(ioScheduler)
+ .observeOn(mainThreadScheduler)
+ .subscribe(this::handleSuccess, this::handleError));
+ }
+
+ }
+
+ @Override
+ public String getQuery() {
+ return query;
+ }
+
+ /**
+ * Handles the success scenario
+ * it initializes the recycler view by adding items to the adapter
+ */
+ public void handleSuccess(List mediaList) {
+ if (mediaList == null || mediaList.isEmpty()) {
+ if(queryList.isEmpty()){
+ view.initErrorView();
+ }else{
+ view.setIsLastPage(true);
+ }
+ } else {
+ this.queryList.addAll(mediaList);
+ view.onSuccess(mediaList);
+ for (DepictedItem m : mediaList) {
+ fetchThumbnailForEntityId(m.getId(), size++);
+ }
+ }
+ }
+
+ /**
+ * Logs and handles API error scenario
+ */
+ private void handleError(Throwable throwable) {
+ Timber.e(throwable, "Error occurred while loading queried depictions");
+ view.initErrorView();
+ view.showSnackbar();
+ }
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/depictions/subClass/models/SparqlResponses.kt b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/models/SparqlResponses.kt
new file mode 100644
index 00000000000..da9781c06ae
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/depictions/subClass/models/SparqlResponses.kt
@@ -0,0 +1,26 @@
+package fr.free.nrw.commons.depictions.subClass.models
+
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
+
+data class SparqlResponse(val results: Result) {
+ fun toDepictedItems() =
+ results.bindings.map {
+ DepictedItem(
+ it.itemLabel.value,
+ it.itemDescription?.value ?: "",
+ "",
+ false,
+ it.item.value.substringAfterLast("/")
+ )
+ }
+}
+
+data class Result(val bindings: List)
+
+data class Binding(
+ val item: SparqInfo,
+ val itemLabel: SparqInfo,
+ val itemDescription: SparqInfo? = null
+)
+
+data class SparqInfo(val type: String, val value: String)
diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
index 1686dba3e24..db7c7fd26ba 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
@@ -4,17 +4,17 @@
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.AboutActivity;
import fr.free.nrw.commons.WelcomeActivity;
+import fr.free.nrw.commons.achievements.AchievementsActivity;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity;
import fr.free.nrw.commons.bookmarks.BookmarksActivity;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.MainActivity;
-import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
+import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.explore.SearchActivity;
-import fr.free.nrw.commons.explore.ExploreActivity;
+import fr.free.nrw.commons.explore.categories.ExploreActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
-import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.review.ReviewActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
import fr.free.nrw.commons.upload.UploadActivity;
@@ -68,7 +68,7 @@ public abstract class ActivityBuilderModule {
abstract ExploreActivity bindExploreActivity();
@ContributesAndroidInjector
- abstract ProfileActivity bindAchievementsActivity();
+ abstract AchievementsActivity bindAchievementsActivity();
@ContributesAndroidInjector
abstract BookmarksActivity bindBookmarksActivity();
diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java
index 7fc66e507bf..f813de7629b 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java
@@ -2,7 +2,6 @@
import com.google.gson.Gson;
-import fr.free.nrw.commons.explore.categories.CategoriesModule;
import javax.inject.Singleton;
import dagger.Component;
@@ -11,9 +10,11 @@
import dagger.android.support.AndroidSupportInjectionModule;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.LoginActivity;
+import fr.free.nrw.commons.contributions.ContributionViewHolder;
import fr.free.nrw.commons.contributions.ContributionsModule;
-import fr.free.nrw.commons.explore.depictions.DepictionModule;
+import fr.free.nrw.commons.depictions.DepictionModule;
import fr.free.nrw.commons.explore.SearchModule;
+import fr.free.nrw.commons.nearby.PlaceRenderer;
import fr.free.nrw.commons.review.ReviewController;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.upload.FileProcessor;
@@ -34,12 +35,7 @@
ActivityBuilderModule.class,
FragmentBuilderModule.class,
ServiceBuilderModule.class,
- ContentProviderBuilderModule.class,
- UploadModule.class,
- ContributionsModule.class,
- SearchModule.class,
- DepictionModule.class,
- CategoriesModule.class
+ ContentProviderBuilderModule.class, UploadModule.class, ContributionsModule.class, SearchModule.class, DepictionModule.class
})
public interface CommonsApplicationComponent extends AndroidInjector {
void inject(CommonsApplication application);
@@ -53,6 +49,8 @@ public interface CommonsApplicationComponent extends AndroidInjector {
Timber.tag("OkHttp").v(message);
});
- httpLoggingInterceptor.setLevel(BuildConfig.DEBUG ? Level.BODY: Level.BASIC);
+ httpLoggingInterceptor.level(BuildConfig.DEBUG ? Level.BODY: Level.BASIC);
return httpLoggingInterceptor;
}
@Provides
@Singleton
public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient,
- DepictsClient depictsClient,
@Named("tools_forge") HttpUrl toolsForgeUrl,
- @Named("test_tools_forge") HttpUrl testToolsForgeUrl,
@Named("default_preferences") JsonKvStore defaultKvStore,
Gson gson) {
return new OkHttpJsonApiClient(okHttpClient,
- depictsClient,
toolsForgeUrl,
- testToolsForgeUrl,
WIKIDATA_SPARQL_QUERY_URL,
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,
gson);
@@ -126,14 +116,6 @@ public HttpUrl provideToolsForgeUrl() {
return HttpUrl.parse(TOOLS_FORGE_URL);
}
- @Provides
- @Named("test_tools_forge")
- @NonNull
- @SuppressWarnings("ConstantConditions")
- public HttpUrl provideTestToolsForgeUrl() {
- return HttpUrl.parse(TEST_TOOLS_FORGE_URL);
- }
-
@Provides
@Singleton
@Named(NAMED_COMMONS_WIKI_SITE)
@@ -249,21 +231,4 @@ public UserInterface provideUserInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSi
public WikidataInterface provideWikidataInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikiDataWikiSite) {
return ServiceFactory.get(wikiDataWikiSite, BuildConfig.WIKIDATA_URL, WikidataInterface.class);
}
-
- /**
- * Add provider for PageMediaInterface
- * It creates a retrofit service for the wiki site using device's current language
- */
- @Provides
- @Singleton
- public PageMediaInterface providePageMediaInterface(@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) WikiSite wikiSite) {
- return ServiceFactory.get(wikiSite, wikiSite.url(), PageMediaInterface.class);
- }
-
- @Provides
- @Singleton
- @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
- public WikiSite provideLanguageWikipediaSite() {
- return WikiSite.forLanguageCode(Locale.getDefault().getLanguage());
- }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java
index e86b06a7148..4d18c3a1984 100644
--- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java
@@ -5,37 +5,35 @@
import android.view.View;
import android.widget.FrameLayout;
import android.widget.SearchView;
-import androidx.annotation.NonNull;
+
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.viewpager.widget.ViewPager;
-import butterknife.BindView;
-import butterknife.ButterKnife;
+
import com.google.android.material.tabs.TabLayout;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxSearchView;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryImagesCallback;
-import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment;
-import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment;
-import fr.free.nrw.commons.explore.media.SearchMediaFragment;
-import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
-import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
+import fr.free.nrw.commons.explore.categories.SearchCategoryFragment;
+import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragment;
+import fr.free.nrw.commons.explore.images.SearchImageFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.FragmentUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import javax.inject.Inject;
-import timber.log.Timber;
/**
* Represents search screen of this app
@@ -51,16 +49,14 @@ public class SearchActivity extends NavigationBaseActivity
@BindView(R.id.tab_layout) TabLayout tabLayout;
@BindView(R.id.viewPager) ViewPager viewPager;
- @Inject
- RecentSearchesDao recentSearchesDao;
-
- private SearchMediaFragment searchMediaFragment;
+ private SearchImageFragment searchImageFragment;
private SearchCategoryFragment searchCategoryFragment;
private SearchDepictionsFragment searchDepictionsFragment;
private RecentSearchesFragment recentSearchesFragment;
private FragmentManager supportFragmentManager;
private MediaDetailPagerFragment mediaDetails;
ViewPagerAdapter viewPagerAdapter;
+ private String query;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -99,10 +95,10 @@ private void setSearchHistoryFragment() {
public void setTabs() {
List fragmentList = new ArrayList<>();
List titleList = new ArrayList<>();
- searchMediaFragment = new SearchMediaFragment();
+ searchImageFragment = new SearchImageFragment();
searchDepictionsFragment = new SearchDepictionsFragment();
searchCategoryFragment= new SearchCategoryFragment();
- fragmentList.add(searchMediaFragment);
+ fragmentList.add(searchImageFragment);
titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase());
fragmentList.add(searchCategoryFragment);
titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase());
@@ -115,57 +111,45 @@ public void setTabs() {
.takeUntil(RxView.detaches(searchView))
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
- .subscribe(query -> {
- //update image list
- if (!TextUtils.isEmpty(query)) {
- saveRecentSearch(query.toString());
- viewPager.setVisibility(View.VISIBLE);
- tabLayout.setVisibility(View.VISIBLE);
- searchHistoryContainer.setVisibility(View.GONE);
+ .subscribe( query -> {
+ this.query = query.toString();
+ //update image list
+ if (!TextUtils.isEmpty(query)) {
+ viewPager.setVisibility(View.VISIBLE);
+ tabLayout.setVisibility(View.VISIBLE);
+ searchHistoryContainer.setVisibility(View.GONE);
- if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) {
- searchDepictionsFragment.onQueryUpdated(query.toString());
- }
+ if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) {
+ searchDepictionsFragment.updateDepictionList(query.toString());
+ }
- if (FragmentUtils.isFragmentUIActive(searchMediaFragment)) {
- searchMediaFragment.onQueryUpdated(query.toString());
- }
+ if (FragmentUtils.isFragmentUIActive(searchImageFragment)) {
+ searchImageFragment.updateImageList(query.toString());
+ }
- if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) {
- searchCategoryFragment.onQueryUpdated(query.toString());
- }
+ if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) {
+ searchCategoryFragment.updateCategoryList(query.toString());
+ }
- } else {
- //Open RecentSearchesFragment
- recentSearchesFragment.updateRecentSearches();
- viewPager.setVisibility(View.GONE);
- tabLayout.setVisibility(View.GONE);
- setSearchHistoryFragment();
- searchHistoryContainer.setVisibility(View.VISIBLE);
+ }else {
+ //Open RecentSearchesFragment
+ recentSearchesFragment.updateRecentSearches();
+ viewPager.setVisibility(View.GONE);
+ tabLayout.setVisibility(View.GONE);
+ setSearchHistoryFragment();
+ searchHistoryContainer.setVisibility(View.VISIBLE);
+ }
}
- }, Timber::e
));
}
- private void saveRecentSearch(@NonNull final String query) {
- final RecentSearch recentSearch = recentSearchesDao.find(query);
- // Newly searched query...
- if (recentSearch == null) {
- recentSearchesDao.save(new RecentSearch(null, query, new Date()));
- }
- else {
- recentSearch.setLastSearched(new Date());
- recentSearchesDao.save(recentSearch);
- }
- }
-
/**
* returns Media Object at position
* @param i position of Media in the imagesRecyclerView adapter.
*/
@Override
public Media getMediaAtPosition(int i) {
- return searchMediaFragment.getMediaAtPosition(i);
+ return searchImageFragment.getImageAtPosition(i);
}
/**
@@ -173,12 +157,7 @@ public Media getMediaAtPosition(int i) {
*/
@Override
public int getTotalMediaCount() {
- return searchMediaFragment.getTotalMediaCount();
- }
-
- @Override
- public Integer getContributionStateAt(int position) {
- return null;
+ return searchImageFragment.getTotalImagesCount();
}
/**
@@ -196,8 +175,7 @@ public void viewPagerNotifyDataSetChanged() {
* Open media detail pager fragment on click of image in search results
* @param index item index that should be opened
*/
- @Override
- public void onMediaClicked(int index) {
+ public void onSearchImageClicked(int index) {
ViewUtil.hideKeyboard(this.findViewById(R.id.searchBox));
toolbar.setVisibility(View.GONE);
tabLayout.setVisibility(View.GONE);
@@ -232,7 +210,7 @@ protected void onResume() {
//FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time.
//FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894
// This is called on screen rotation when user is inside media details. Ideally it should show Media Details but since we are not saving the state now. We are throwing the user to search screen otherwise the app was crashing.
- //
+ //
onBackPressed();
}
super.onResume();
@@ -269,6 +247,17 @@ public void updateText(String query) {
viewPager.requestFocus();
}
+ /**
+ * This method is called when viewPager has reached its end.
+ * Fetches more images using search query and adds it to the recycler view and viewpager adapter
+ */
+ @Override
+ public void requestMoreImages() {
+ if (searchImageFragment!=null){
+ searchImageFragment.addImagesToList(query);
+ }
+ }
+
@Override protected void onDestroy() {
super.onDestroy();
//Dispose the disposables when the activity is destroyed
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchModule.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchModule.java
new file mode 100644
index 00000000000..f9894645109
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchModule.java
@@ -0,0 +1,19 @@
+package fr.free.nrw.commons.explore;
+
+import dagger.Binds;
+import dagger.Module;
+import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentContract;
+import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentPresenter;
+
+/**
+ * The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
+ */
+@Module
+public abstract class SearchModule {
+
+ @Binds
+ public abstract SearchDepictionsFragmentContract.UserActionListener bindsSearchDepictionsFragmentPresenter(
+ SearchDepictionsFragmentPresenter
+ presenter
+ );
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.java
new file mode 100644
index 00000000000..da1c447a431
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.java
@@ -0,0 +1,230 @@
+package fr.free.nrw.commons.explore.categories;
+
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.pedrogomez.renderers.RVRendererAdapter;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.category.CategoryClient;
+import fr.free.nrw.commons.category.CategoryDetailsActivity;
+import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
+import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
+import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
+import fr.free.nrw.commons.kvstore.JsonKvStore;
+import fr.free.nrw.commons.utils.NetworkUtils;
+import fr.free.nrw.commons.utils.ViewUtil;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.schedulers.Schedulers;
+import timber.log.Timber;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+/**
+ * Displays the category search screen.
+ */
+
+public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
+
+ private static int TIMEOUT_SECONDS = 15;
+
+ @BindView(R.id.imagesListBox)
+ RecyclerView categoriesRecyclerView;
+ @BindView(R.id.imageSearchInProgress)
+ ProgressBar progressBar;
+ @BindView(R.id.imagesNotFound)
+ TextView categoriesNotFoundView;
+ String query;
+ @BindView(R.id.bottomProgressBar)
+ ProgressBar bottomProgressBar;
+ boolean isLoadingCategories;
+
+ @Inject RecentSearchesDao recentSearchesDao;
+ @Inject CategoryClient categoryClient;
+
+ @Inject
+ @Named("default_preferences")
+ JsonKvStore basicKvStore;
+
+ private RVRendererAdapter categoriesAdapter;
+ private List queryList = new ArrayList<>();
+
+ private final SearchCategoriesAdapterFactory adapterFactory = new SearchCategoriesAdapterFactory(item -> {
+ // Called on Click of a individual category Item
+ // Open Category Details activity
+ CategoryDetailsActivity.startYourself(getContext(), item);
+ saveQuery(query);
+ });
+
+ /**
+ * This method saves Search Query in the Recent Searches Database.
+ * @param query
+ */
+ private void saveQuery(String query) {
+ RecentSearch recentSearch = recentSearchesDao.find(query);
+
+ // Newly searched query...
+ if (recentSearch == null) {
+ recentSearch = new RecentSearch(null, query, new Date());
+ }
+ else {
+ recentSearch.setLastSearched(new Date());
+ }
+ recentSearchesDao.save(recentSearch);
+
+ }
+
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
+ View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
+ ButterKnife.bind(this, rootView);
+ if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
+ categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ }
+ else{
+ categoriesRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
+ }
+ ArrayList items = new ArrayList<>();
+ categoriesAdapter = adapterFactory.create(items);
+ categoriesRecyclerView.setAdapter(categoriesAdapter);
+ categoriesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ super.onScrollStateChanged(recyclerView, newState);
+ // check if end of recycler view is reached, if yes then add more results to existing results
+ if (!recyclerView.canScrollVertically(1)) {
+ addCategoriesToList(query);
+ }
+ }
+ });
+ return rootView;
+ }
+
+ /**
+ * Checks for internet connection and then initializes the recycler view with 25 categories of the searched query
+ * Clearing categoryAdapter every time new keyword is searched so that user can see only new results
+ */
+ public void updateCategoryList(String query) {
+ this.query = query;
+ categoriesNotFoundView.setVisibility(GONE);
+ if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
+ handleNoInternet();
+ return;
+ }
+ bottomProgressBar.setVisibility(View.VISIBLE);
+ progressBar.setVisibility(GONE);
+ queryList.clear();
+ categoriesAdapter.clear();
+ compositeDisposable.add(categoryClient.searchCategories(query,25)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .doOnSubscribe(disposable -> saveQuery(query))
+ .collect(ArrayList::new, ArrayList::add)
+ .subscribe(this::handleSuccess, this::handleError));
+ }
+
+
+ /**
+ * Adds 25 more results to existing search results
+ */
+ public void addCategoriesToList(String query) {
+ if(isLoadingCategories) return;
+ isLoadingCategories=true;
+ this.query = query;
+ bottomProgressBar.setVisibility(View.VISIBLE);
+ progressBar.setVisibility(GONE);
+ compositeDisposable.add(categoryClient.searchCategories(query,25, queryList.size())
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .collect(ArrayList::new, ArrayList::add)
+ .subscribe(this::handlePaginationSuccess, this::handleError));
+ }
+
+ /**
+ * Handles the success scenario
+ * it initializes the recycler view by adding items to the adapter
+ */
+ private void handlePaginationSuccess(List mediaList) {
+ queryList.addAll(mediaList);
+ progressBar.setVisibility(View.GONE);
+ bottomProgressBar.setVisibility(GONE);
+ categoriesAdapter.addAll(mediaList);
+ categoriesAdapter.notifyDataSetChanged();
+ isLoadingCategories=false;
+ }
+
+
+
+ /**
+ * Handles the success scenario
+ * it initializes the recycler view by adding items to the adapter
+ */
+ private void handleSuccess(List mediaList) {
+ queryList = mediaList;
+ if (mediaList == null || mediaList.isEmpty()) {
+ initErrorView();
+ }
+ else {
+
+ bottomProgressBar.setVisibility(View.GONE);
+ progressBar.setVisibility(GONE);
+ categoriesAdapter.addAll(mediaList);
+ categoriesAdapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Logs and handles API error scenario
+ */
+ private void handleError(Throwable throwable) {
+ Timber.e(throwable, "Error occurred while loading queried categories");
+ try {
+ initErrorView();
+ ViewUtil.showShortSnackbar(categoriesRecyclerView, R.string.error_loading_categories);
+ }catch (Exception e){
+ e.printStackTrace();
+ }
+
+ }
+
+ /**
+ * Handles the UI updates for a error scenario
+ */
+ private void initErrorView() {
+ progressBar.setVisibility(GONE);
+ categoriesNotFoundView.setVisibility(VISIBLE);
+ categoriesNotFoundView.setText(getString(R.string.categories_not_found,query));
+ }
+
+ /**
+ * Handles the UI updates for no internet scenario
+ */
+ private void handleNoInternet() {
+ progressBar.setVisibility(GONE);
+ ViewUtil.showShortSnackbar(categoriesRecyclerView, R.string.no_internet);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/DepictsClient.java b/app/src/main/java/fr/free/nrw/commons/explore/depictions/DepictsClient.java
new file mode 100644
index 00000000000..dcbf69a5f5c
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/DepictsClient.java
@@ -0,0 +1,175 @@
+package fr.free.nrw.commons.explore.depictions;
+
+import androidx.annotation.Nullable;
+import fr.free.nrw.commons.BuildConfig;
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.depictions.models.Search;
+import fr.free.nrw.commons.media.MediaInterface;
+import fr.free.nrw.commons.upload.depicts.DepictsInterface;
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
+import fr.free.nrw.commons.utils.CommonsDateUtil;
+import fr.free.nrw.commons.wikidata.WikidataProperties;
+import io.reactivex.Observable;
+import io.reactivex.Single;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.wikipedia.wikidata.DataValue.DataValueString;
+import org.wikipedia.wikidata.Statement_partial;
+
+/**
+ * Depicts Client to handle custom calls to Commons Wikibase APIs
+ */
+@Singleton
+public class DepictsClient {
+
+ private final DepictsInterface depictsInterface;
+ private final MediaInterface mediaInterface;
+ private static final String NO_DEPICTED_IMAGE = "No Image for Depiction";
+
+ @Inject
+ public DepictsClient(DepictsInterface depictsInterface, MediaInterface mediaInterface) {
+ this.depictsInterface = depictsInterface;
+ this.mediaInterface = mediaInterface;
+ }
+
+ /**
+ * Search for depictions using the search item
+ * @return list of depicted items
+ */
+ public Observable searchForDepictions(String query, int limit, int offset) {
+ return depictsInterface.searchForDepicts(
+ query,
+ String.valueOf(limit),
+ Locale.getDefault().getLanguage(),
+ Locale.getDefault().getLanguage(),
+ String.valueOf(offset)
+ )
+ .flatMap(depictSearchResponse ->Observable.fromIterable(depictSearchResponse.getSearch()))
+ .map(DepictedItem::new);
+ }
+
+ /**
+ * Get URL for image using image name
+ * Ex: title = Guion Bluford
+ * Url = https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/Guion_Bluford.jpg/70px-Guion_Bluford.jpg
+ */
+ private String getThumbnailUrl(String title) {
+ String baseUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/";
+ title = title.replace(" ", "_");
+ String MD5Hash = getMd5(title);
+ /**
+ * We use 70 pixels as the size of our Thumbnail (as it is the perfect fits our UI)
+ */
+ return baseUrl + MD5Hash.charAt(0) + '/' + MD5Hash.charAt(0) + MD5Hash.charAt(1) + '/' + title + "/70px-" + title;
+ }
+
+ /**
+ * Ex: entityId = Q357458
+ * value returned = Elgin Baylor Night program.jpeg
+ */
+ public Single getP18ForItem(String entityId) {
+ return depictsInterface.getImageForEntity(entityId)
+ .map(claimsResponse -> {
+ final List imageClaim = claimsResponse.getClaims()
+ .get(WikidataProperties.IMAGE.getPropertyName());
+ if (imageClaim != null) {
+ final DataValueString dataValue = (DataValueString) imageClaim
+ .get(0)
+ .getMainSnak()
+ .getDataValue();
+ return getThumbnailUrl((dataValue.getValue()));
+ }
+ return NO_DEPICTED_IMAGE;
+ })
+ .singleOrError();
+ }
+
+ /**
+ * @return list of images for a particular depict entity
+ */
+ public Observable> fetchImagesForDepictedItem(String query, int sroffset) {
+ return mediaInterface.fetchImagesForDepictedItem("haswbstatement:" + BuildConfig.DEPICTS_PROPERTY + "=" + query, String.valueOf(sroffset))
+ .map(mwQueryResponse -> {
+ List mediaList = new ArrayList<>();
+ for (Search s: mwQueryResponse.getQuery().getSearch()) {
+ Media media = new Media(null,
+ getUrl(s.getTitle()),
+ s.getTitle(),
+ "",
+ 0,
+ safeParseDate(s.getTimestamp()),
+ safeParseDate(s.getTimestamp()),
+ ""
+ );
+ mediaList.add(media);
+ }
+ return mediaList;
+ });
+ }
+
+ /**
+ * Get url for the image from media of depictions
+ * Ex: Tiger_Woods
+ * Value: https://upload.wikimedia.org/wikipedia/commons/thumb/6/67/Tiger_Woods.jpg/70px-Tiger_Woods.jpg
+ */
+ private String getUrl(String title) {
+ String baseUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/";
+ title = title.substring(title.indexOf(':')+1);
+ title = title.replace(" ", "_");
+ String MD5Hash = getMd5(title);
+ return baseUrl + MD5Hash.charAt(0) + '/' + MD5Hash.charAt(0) + MD5Hash.charAt(1) + '/' + title + "/640px-" + title;
+ }
+
+ /**
+ * Generates MD5 hash for the filename
+ */
+ public String getMd5(String input)
+ {
+ try {
+
+ // Static getInstance method is called with hashing MD5
+ MessageDigest md = MessageDigest.getInstance("MD5");
+
+ // digest() method is called to calculate message digest
+ // of an input digest() return array of byte
+ byte[] messageDigest = md.digest(input.getBytes());
+
+ // Convert byte array into signum representation
+ BigInteger no = new BigInteger(1, messageDigest);
+
+ // Convert message digest into hex value
+ String hashtext = no.toString(16);
+ while (hashtext.length() < 32) {
+ hashtext = "0" + hashtext;
+ }
+ return hashtext;
+ }
+
+ // For specifying wrong message digest algorithms
+ catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Parse the date string into the required format
+ * @param dateStr
+ * @return date in the required format
+ */
+ @Nullable
+ private static Date safeParseDate(String dateStr) {
+ try {
+ return CommonsDateUtil.getIso8601DateFormatShort().parse(dateStr);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsAdapterFactory.java
new file mode 100644
index 00000000000..71d64b0ea16
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsAdapterFactory.java
@@ -0,0 +1,31 @@
+package fr.free.nrw.commons.explore.depictions;
+
+import com.pedrogomez.renderers.ListAdapteeCollection;
+import com.pedrogomez.renderers.RVRendererAdapter;
+import com.pedrogomez.renderers.RendererBuilder;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
+
+/**
+ * Adapter factory for Items in Explore
+ */
+
+public class SearchDepictionsAdapterFactory {
+ private final SearchDepictionsRenderer.DepictCallback listener;
+
+ public SearchDepictionsAdapterFactory(SearchDepictionsRenderer.DepictCallback listener) {
+ this.listener = listener;
+ }
+
+ public RVRendererAdapter create() {
+ List searchImageItemList = new ArrayList<>();
+ RendererBuilder builder = new RendererBuilder().bind(DepictedItem.class, new SearchDepictionsRenderer(listener));
+ ListAdapteeCollection collection = new ListAdapteeCollection<>(
+ searchImageItemList != null ? searchImageItemList : Collections.emptyList());
+ return new RVRendererAdapter<>(builder, collection);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragment.java
new file mode 100644
index 00000000000..10ccf4ad4b5
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragment.java
@@ -0,0 +1,237 @@
+package fr.free.nrw.commons.explore.depictions;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import com.pedrogomez.renderers.RVRendererAdapter;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
+import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
+import fr.free.nrw.commons.utils.NetworkUtils;
+import fr.free.nrw.commons.utils.ViewUtil;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import javax.inject.Inject;
+
+/**
+ * Display depictions in search fragment
+ */
+public class SearchDepictionsFragment extends CommonsDaggerSupportFragment implements SearchDepictionsFragmentContract.View {
+
+ @BindView(R.id.imagesListBox)
+ RecyclerView depictionsRecyclerView;
+ @BindView(R.id.imageSearchInProgress)
+ ProgressBar progressBar;
+ @BindView(R.id.imagesNotFound)
+ TextView depictionNotFound;
+ @BindView(R.id.bottomProgressBar)
+ ProgressBar bottomProgressBar;
+ RecyclerView.LayoutManager layoutManager;
+ private boolean isLoading = true;
+ private int PAGE_SIZE = 25;
+ @Inject
+ SearchDepictionsFragmentPresenter presenter;
+ private final SearchDepictionsAdapterFactory adapterFactory = new SearchDepictionsAdapterFactory(new SearchDepictionsRenderer.DepictCallback() {
+ @Override
+ public void depictsClicked(DepictedItem item) {
+ WikidataItemDetailsActivity.startYourself(getContext(), item);
+ presenter.saveQuery();
+ }
+
+ /**
+ *fetch thumbnail image for all the depicted items (if available)
+ */
+ @Override
+ public void fetchThumbnailUrlForEntity(String entityId, int position) {
+ presenter.fetchThumbnailForEntityId(entityId,position);
+ }
+
+ });
+ private RVRendererAdapter depictionsAdapter;
+ private boolean isLastPage;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
+ ButterKnife.bind(this, rootView);
+ if (getActivity().getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_PORTRAIT) {
+ layoutManager = new LinearLayoutManager(getContext());
+ } else {
+ layoutManager = new GridLayoutManager(getContext(), 2);
+ }
+ depictionsRecyclerView.setLayoutManager(layoutManager);
+ depictionsAdapter = adapterFactory.create();
+ depictionsRecyclerView.setAdapter(depictionsAdapter);
+ depictionsRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ super.onScrollStateChanged(recyclerView, newState);
+ }
+
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ super.onScrolled(recyclerView, dx, dy);
+
+ int visibleItemCount = layoutManager.getChildCount();
+ int totalItemCount = layoutManager.getItemCount();
+ int firstVisibleItemPosition=0;
+ if(layoutManager instanceof GridLayoutManager){
+ firstVisibleItemPosition=((GridLayoutManager) layoutManager).findFirstVisibleItemPosition();
+ } else {
+ firstVisibleItemPosition=((LinearLayoutManager)layoutManager).findFirstVisibleItemPosition();
+ }
+
+ /**
+ * If the user isn't currently loading items and the last page hasn’t been reached,
+ * then it checks against the current position in view to decide whether or not to load more items.
+ */
+ if (!isLoading && !isLastPage) {
+ if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount
+ && firstVisibleItemPosition >= 0
+ && totalItemCount >= PAGE_SIZE) {
+ loadMoreItems(false);
+ }
+ }
+ }
+ });
+ return rootView;
+ }
+
+ /**
+ * Fetch PAGE_SIZE number of items
+ */
+ private void loadMoreItems(boolean reInitialise) {
+ presenter.updateDepictionList(presenter.getQuery(),PAGE_SIZE, reInitialise);
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ presenter.onAttachView(this);
+ }
+
+ /**
+ * Called when user selects "Items" from Search Activity
+ * to load the list of depictions from API
+ *
+ * @param query string searched in the Explore Activity
+ */
+ public void updateDepictionList(String query) {
+ presenter.initializeQuery(query);
+ if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
+ handleNoInternet();
+ return;
+ }
+ loadMoreItems(true);
+ }
+
+ /**
+ * Handles the UI updates for a error scenario
+ */
+ @Override
+ public void initErrorView() {
+ isLoading = false;
+ progressBar.setVisibility(GONE);
+ bottomProgressBar.setVisibility(GONE);
+ depictionNotFound.setVisibility(VISIBLE);
+ String no_depiction = getString(R.string.depictions_not_found);
+ depictionNotFound.setText(String.format(Locale.getDefault(), no_depiction, presenter.getQuery()));
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ depictionsAdapter.clear();
+ depictionsRecyclerView.cancelPendingInputEvents();
+ }
+
+ /**
+ * Handles the UI updates for no internet scenario
+ */
+ @Override
+ public void handleNoInternet() {
+ progressBar.setVisibility(GONE);
+ ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.no_internet);
+ }
+
+ /**
+ * If a non empty list is successfully returned from the api then modify the view
+ * like hiding empty labels, hiding progressbar and notifying the apdapter that list of items has been fetched from the API
+ */
+ @Override
+ public void onSuccess(List mediaList) {
+ isLoading = false;
+ progressBar.setVisibility(View.GONE);
+ depictionNotFound.setVisibility(GONE);
+ bottomProgressBar.setVisibility(GONE);
+ int itemCount = layoutManager.getItemCount();
+ depictionsAdapter.addAll(mediaList);
+ if(itemCount!=0) {
+ depictionsAdapter.notifyItemRangeInserted(itemCount, mediaList.size()-1);
+ }else{
+ depictionsAdapter.notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void loadingDepictions(boolean isLoading) {
+ depictionNotFound.setVisibility(GONE);
+ bottomProgressBar.setVisibility(View.VISIBLE);
+ progressBar.setVisibility(GONE);
+ this.isLoading = isLoading;
+ }
+
+ @Override
+ public void clearAdapter() {
+ depictionsAdapter.clear();
+ }
+
+ @Override
+ public void showSnackbar() {
+ ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.error_loading_depictions);
+ }
+
+ @Override
+ public RVRendererAdapter getAdapter() {
+ return depictionsAdapter;
+ }
+
+ @Override
+ public void onImageUrlFetched(String response, int position) {
+ depictionsAdapter.getItem(position).setImageUrl(response);
+ depictionsAdapter.notifyItemChanged(position);
+ }
+
+ /**
+ * Inform the view that there are no more items to be loaded for this search query
+ * or reset the isLastPage for the current query
+ * @param isLastPage
+ */
+ @Override
+ public void setIsLastPage(boolean isLastPage) {
+ this.isLastPage=isLastPage;
+ progressBar.setVisibility(GONE);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragmentContract.java b/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragmentContract.java
new file mode 100644
index 00000000000..49ce776b31c
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragmentContract.java
@@ -0,0 +1,94 @@
+package fr.free.nrw.commons.explore.depictions;
+
+import com.pedrogomez.renderers.RVRendererAdapter;
+
+import java.util.List;
+
+import fr.free.nrw.commons.BasePresenter;
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
+
+/**
+ * The contract with with SearchDepictionsFragment and its presenter would talk to each other
+ */
+public interface SearchDepictionsFragmentContract {
+
+ interface View {
+ /**
+ * Handles the UI updates for a error scenario
+ */
+ void initErrorView();
+
+ /**
+ * Handles the UI updates for no internet scenario
+ */
+ void handleNoInternet();
+
+ /**
+ * If a non empty list is successfully returned from the api then modify the view
+ * like hiding empty labels, hiding progressbar and notifying the apdapter that list of items has been fetched from the API
+ */
+ void onSuccess(List mediaList);
+
+ /**
+ * load depictions
+ */
+ void loadingDepictions(boolean isLoading);
+
+ /**
+ * clear adapter
+ */
+ void clearAdapter();
+
+ /**
+ * show snackbar
+ */
+ void showSnackbar();
+
+ /**
+ * @return adapter
+ */
+ RVRendererAdapter getAdapter();
+
+ void onImageUrlFetched(String response, int position);
+
+ /**
+ * Inform the view that there are no more items to be loaded for this search query
+ * or reset the isLastPage for the current query
+ * @param isLastPage
+ */
+ void setIsLastPage(boolean isLastPage);
+ }
+
+ interface UserActionListener extends BasePresenter {
+
+ /**
+ * Called when user selects "Items" from Search Activity
+ * to load the list of depictions from API
+ *
+ * @param query string searched in the Explore Activity
+ * @param reInitialise
+ */
+ void updateDepictionList(String query, int pageSize, boolean reInitialise);
+
+ /**
+ * This method saves Search Query in the Recent Searches Database.
+ */
+ void saveQuery();
+
+ /**
+ * Whenever a new query is initiated from the search activity clear the previous adapter
+ * and add new value of the query
+ */
+ void initializeQuery(String query);
+
+ /**
+ * @return query
+ */
+ String getQuery();
+
+ /**
+ * After all the depicted items are loaded fetch thumbnail image for all the depicted items (if available)
+ */
+ void fetchThumbnailForEntityId(String entityId,int position);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragmentPresenter.java b/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragmentPresenter.java
new file mode 100644
index 00000000000..780fe06228a
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragmentPresenter.java
@@ -0,0 +1,180 @@
+package fr.free.nrw.commons.explore.depictions;
+
+import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
+import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
+
+import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
+import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
+import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
+import fr.free.nrw.commons.kvstore.JsonKvStore;
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
+import io.reactivex.Scheduler;
+import io.reactivex.disposables.CompositeDisposable;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
+import javax.inject.Named;
+import timber.log.Timber;
+
+/**
+ * The presenter class for SearchDepictionsFragment
+ */
+public class SearchDepictionsFragmentPresenter extends CommonsDaggerSupportFragment implements SearchDepictionsFragmentContract.UserActionListener {
+
+ /**
+ * This creates a dynamic proxy instance of the class,
+ * proxy is to control access to the target object
+ * here our target object is the view.
+ * Thus we when onDettach method of fragment is called we replace the binding of view to our object with the proxy instance
+ */
+ private static final SearchDepictionsFragmentContract.View DUMMY = (SearchDepictionsFragmentContract.View) Proxy
+ .newProxyInstance(
+ SearchDepictionsFragmentContract.View.class.getClassLoader(),
+ new Class[]{SearchDepictionsFragmentContract.View.class},
+ (proxy, method, methodArgs) -> null);
+ private static int TIMEOUT_SECONDS = 15;
+ protected CompositeDisposable compositeDisposable = new CompositeDisposable();
+ private final Scheduler ioScheduler;
+ private final Scheduler mainThreadScheduler;
+
+ boolean isLoadingDepictions;
+ String query;
+ RecentSearchesDao recentSearchesDao;
+ DepictsClient depictsClient;
+ JsonKvStore basicKvStore;
+ private SearchDepictionsFragmentContract.View view = DUMMY;
+ private List queryList = new ArrayList<>();
+ int offset=0;
+ int size = 0;
+
+ @Inject
+ public SearchDepictionsFragmentPresenter(@Named("default_preferences") JsonKvStore basicKvStore,
+ RecentSearchesDao recentSearchesDao,
+ DepictsClient depictsClient,
+ @Named(IO_THREAD) Scheduler ioScheduler,
+ @Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
+ this.basicKvStore = basicKvStore;
+ this.recentSearchesDao = recentSearchesDao;
+ this.depictsClient = depictsClient;
+ this.ioScheduler = ioScheduler;
+ this.mainThreadScheduler = mainThreadScheduler;
+ }
+
+ @Override
+ public void onAttachView(SearchDepictionsFragmentContract.View view) {
+ this.view = view;
+ }
+
+ @Override
+ public void onDetachView() {
+ this.view = DUMMY;
+ }
+
+ /**
+ * Called when user selects "Items" from Search Activity
+ * to load the list of depictions from API
+ *
+ * @param query string searched in the Explore Activity
+ * @param reInitialise
+ */
+ @Override
+ public void updateDepictionList(String query, int pageSize, boolean reInitialise) {
+ this.query = query;
+ view.loadingDepictions(true);
+ if (reInitialise) {
+ size = 0;
+ }
+ saveQuery();
+ compositeDisposable.add(depictsClient.searchForDepictions(query, 25, offset)
+ .subscribeOn(ioScheduler)
+ .observeOn(mainThreadScheduler)
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .doOnSubscribe(disposable -> saveQuery())
+ .collect(ArrayList::new, ArrayList::add)
+ .subscribe(this::handleSuccess, this::handleError));
+ }
+
+ /**
+ * Logs and handles API error scenario
+ */
+ private void handleError(Throwable throwable) {
+ Timber.e(throwable, "Error occurred while loading queried depictions");
+ view.initErrorView();
+ view.showSnackbar();
+ }
+
+ /**
+ * This method saves Search Query in the Recent Searches Database.
+ */
+ @Override
+ public void saveQuery() {
+ RecentSearch recentSearch = recentSearchesDao.find(query);
+
+ // Newly searched query...
+ if (recentSearch == null) {
+ recentSearch = new RecentSearch(null, query, new Date());
+ } else {
+ recentSearch.setLastSearched(new Date());
+ }
+ recentSearchesDao.save(recentSearch);
+
+ }
+
+ /**
+ * Whenever a new query is initiated from the search activity clear the previous adapter
+ * and add new value of the query
+ */
+ @Override
+ public void initializeQuery(String query) {
+ this.query = query;
+ this.queryList.clear();
+ offset = 0;//Reset the offset on query change
+ compositeDisposable.clear();
+ view.setIsLastPage(false);
+ view.clearAdapter();
+ }
+
+ @Override
+ public String getQuery() {
+ return query;
+ }
+
+ /**
+ * Handles the success scenario
+ * it initializes the recycler view by adding items to the adapter
+ */
+ public void handleSuccess(List mediaList) {
+ if (mediaList == null || mediaList.isEmpty()) {
+ if(queryList.isEmpty()){
+ view.initErrorView();
+ }else{
+ view.setIsLastPage(true);
+ }
+ } else {
+ this.queryList.addAll(mediaList);
+ view.onSuccess(mediaList);
+ offset=queryList.size();
+ for (DepictedItem m : mediaList) {
+ fetchThumbnailForEntityId(m.getId(), size++);
+ }
+ }
+ }
+
+ /**
+ * After all the depicted items are loaded fetch thumbnail image for all the depicted items (if available)
+ */
+ @Override
+ public void fetchThumbnailForEntityId(String entityId,int position) {
+ compositeDisposable.add(depictsClient.getP18ForItem(entityId)
+ .subscribeOn(ioScheduler)
+ .observeOn(mainThreadScheduler)
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .subscribe(response -> {
+ view.onImageUrlFetched(response,position);
+ }));
+ }
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsRenderer.java b/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsRenderer.java
new file mode 100644
index 00000000000..581858ef974
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsRenderer.java
@@ -0,0 +1,127 @@
+package fr.free.nrw.commons.explore.depictions;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+
+import com.facebook.common.executors.CallerThreadExecutor;
+import com.facebook.common.references.CloseableReference;
+import com.facebook.datasource.DataSource;
+import com.facebook.drawee.backends.pipeline.Fresco;
+import com.facebook.imagepipeline.core.ImagePipeline;
+import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
+import com.facebook.imagepipeline.image.CloseableImage;
+import com.facebook.imagepipeline.request.ImageRequest;
+import com.facebook.imagepipeline.request.ImageRequestBuilder;
+import com.pedrogomez.renderers.Renderer;
+
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
+import timber.log.Timber;
+
+/**
+ * Renderer for DepictedItem
+ */
+public class SearchDepictionsRenderer extends Renderer {
+
+ @BindView(R.id.depicts_label)
+ TextView tvDepictionLabel;
+
+ @BindView(R.id.description)
+ TextView tvDepictionDesc;
+
+ @BindView(R.id.depicts_image)
+ ImageView imageView;
+
+ private DepictCallback listener;
+
+ int size = 0;
+ private final static String NO_IMAGE_FOR_DEPICTION = "No Image for Depiction";
+
+ public SearchDepictionsRenderer(DepictCallback listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ protected void setUpView(View rootView) {
+ ButterKnife.bind(this, rootView);
+ }
+
+ @Override
+ protected void hookListeners(View rootView) {
+ rootView.setOnClickListener(v -> {
+ DepictedItem item = getContent();
+ if (listener != null) {
+ listener.depictsClicked(item);
+ }
+ });
+ }
+
+ @Override
+ protected View inflate(LayoutInflater inflater, ViewGroup parent) {
+ return inflater.inflate(R.layout.item_depictions, parent, false);
+ }
+
+ /**
+ * Render value to all the items in the search depictions list
+ */
+ @Override
+ public void render() {
+ DepictedItem item = getContent();
+ tvDepictionLabel.setText(item.getName());
+ tvDepictionDesc.setText(item.getDescription());
+ imageView.setImageDrawable(getContext().getResources().getDrawable(R.drawable.ic_wikidata_logo_24dp));
+
+ Timber.e("line86"+item.getImageUrl());
+ if (!TextUtils.isEmpty(item.getImageUrl())) {
+ if (!item.getImageUrl().equals(NO_IMAGE_FOR_DEPICTION) && !item.getImageUrl().equals(""))
+ {
+ ImageRequest imageRequest = ImageRequestBuilder
+ .newBuilderWithSource(Uri.parse(item.getImageUrl()))
+ .setAutoRotateEnabled(true)
+ .build();
+
+ ImagePipeline imagePipeline = Fresco.getImagePipeline();
+ final DataSource>
+ dataSource = imagePipeline.fetchDecodedImage(imageRequest, getContext());
+
+ dataSource.subscribe(new BaseBitmapDataSubscriber() {
+
+ @Override
+ public void onNewResultImpl(@Nullable Bitmap bitmap) {
+ if (dataSource.isFinished() && bitmap != null) {
+ Timber.d("Bitmap loaded from url %s", item.getImageUrl());
+ //imageView.setImageBitmap(Bitmap.createBitmap(bitmap));
+ imageView.post(() -> imageView.setImageBitmap(Bitmap.createBitmap(bitmap)));
+ dataSource.close();
+ }
+ }
+
+ @Override
+ public void onFailureImpl(DataSource dataSource) {
+ Timber.d("Error getting bitmap from image url %s", item.getImageUrl());
+ if (dataSource != null) {
+ dataSource.close();
+ }
+ }
+ }, CallerThreadExecutor.getInstance());
+ }
+ }
+ }
+
+ public interface DepictCallback {
+ void depictsClicked(DepictedItem item);
+
+ void fetchThumbnailUrlForEntity(String entityId,int position);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java
new file mode 100644
index 00000000000..28099f8ce66
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java
@@ -0,0 +1,302 @@
+package fr.free.nrw.commons.explore.images;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
+
+import android.annotation.SuppressLint;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import com.pedrogomez.renderers.RVRendererAdapter;
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
+import fr.free.nrw.commons.explore.SearchActivity;
+import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
+import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
+import fr.free.nrw.commons.kvstore.JsonKvStore;
+import fr.free.nrw.commons.media.MediaClient;
+import fr.free.nrw.commons.utils.NetworkUtils;
+import fr.free.nrw.commons.utils.ViewUtil;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.schedulers.Schedulers;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
+import javax.inject.Named;
+import timber.log.Timber;
+
+/**
+ * Displays the image search screen.
+ */
+
+public class SearchImageFragment extends CommonsDaggerSupportFragment {
+
+ private static int TIMEOUT_SECONDS = 15;
+
+ @BindView(R.id.imagesListBox)
+ RecyclerView imagesRecyclerView;
+ @BindView(R.id.imageSearchInProgress)
+ ProgressBar progressBar;
+ @BindView(R.id.imagesNotFound)
+ TextView imagesNotFoundView;
+ String query;
+ @BindView(R.id.bottomProgressBar)
+ ProgressBar bottomProgressBar;
+
+ @Inject RecentSearchesDao recentSearchesDao;
+ @Inject
+ MediaClient mediaClient;
+ @Inject
+ @Named("default_preferences")
+ JsonKvStore defaultKvStore;
+
+ /**
+ * A variable to store number of list items for whom API has been called to fetch captions
+ */
+ private int mediaSize = 0;
+
+ private RVRendererAdapter imagesAdapter;
+ private List queryList = new ArrayList<>();
+
+ private final SearchImagesAdapterFactory adapterFactory = new SearchImagesAdapterFactory(item -> {
+ // Called on Click of a individual media Item
+ int index = queryList.indexOf(item);
+ ((SearchActivity)getContext()).onSearchImageClicked(index);
+ saveQuery(query);
+ });
+
+ /**
+ * This method saves Search Query in the Recent Searches Database.
+ * @param query
+ */
+ private void saveQuery(String query) {
+ RecentSearch recentSearch = recentSearchesDao.find(query);
+
+ // Newly searched query...
+ if (recentSearch == null) {
+ recentSearch = new RecentSearch(null, query, new Date());
+ }
+ else {
+ recentSearch.setLastSearched(new Date());
+ }
+
+ recentSearchesDao.save(recentSearch);
+
+ }
+
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
+ View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
+ ButterKnife.bind(this, rootView);
+ if (getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
+ imagesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ }
+ else{
+ imagesRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
+ }
+ ArrayList items = new ArrayList<>();
+ imagesAdapter = adapterFactory.create(items);
+ imagesRecyclerView.setAdapter(imagesAdapter);
+ imagesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ super.onScrollStateChanged(recyclerView, newState);
+ // check if end of recycler view is reached, if yes then add more results to existing results
+ if (!recyclerView.canScrollVertically(1)) {
+ addImagesToList(query);
+ }
+ }
+ });
+ return rootView;
+ }
+
+ /**
+ * Checks for internet connection and then initializes the recycler view with 25 images of the searched query
+ * Clearing imageAdapter every time new keyword is searched so that user can see only new results
+ */
+ @SuppressLint("CheckResult")
+ public void updateImageList(String query) {
+ this.query = query;
+ if (imagesNotFoundView != null) {
+ imagesNotFoundView.setVisibility(GONE);
+ }
+ if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
+ handleNoInternet();
+ return;
+ }
+ progressBar.setVisibility(View.VISIBLE);
+ bottomProgressBar.setVisibility(GONE);
+ queryList.clear();
+ imagesAdapter.clear();
+ compositeDisposable.add(mediaClient.getMediaListFromSearch(query)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .doOnSubscribe(disposable -> saveQuery(query))
+ .subscribe(this::handleSuccess, this::handleError));
+ }
+
+
+ /**
+ * Adds more results to existing search results
+ */
+ @SuppressLint("CheckResult")
+ public void addImagesToList(String query) {
+ this.query = query;
+ bottomProgressBar.setVisibility(View.VISIBLE);
+ progressBar.setVisibility(GONE);
+ compositeDisposable.add(mediaClient.getMediaListFromSearch(query)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .subscribe(this::handlePaginationSuccess, this::handleError));
+ }
+
+ /**
+ * Handles the success scenario
+ * it initializes the recycler view by adding items to the adapter
+ * @param mediaList List of media to be added
+ */
+ private void handlePaginationSuccess(List mediaList) {
+ progressBar.setVisibility(View.GONE);
+ bottomProgressBar.setVisibility(GONE);
+ if (mediaList.size() != 0 && !queryList.get(queryList.size() - 1).getFilename().equals(mediaList.get(mediaList.size() - 1).getFilename())) {
+ queryList.addAll(mediaList);
+ imagesAdapter.addAll(mediaList);
+ imagesAdapter.notifyDataSetChanged();
+ ((SearchActivity) getContext()).viewPagerNotifyDataSetChanged();
+ }
+ }
+
+
+
+ /**
+ * Handles the success scenario
+ * it initializes the recycler view by adding items to the adapter
+ * @param mediaList List of media to be shown
+ */
+ private void handleSuccess(List mediaList) {
+ queryList = mediaList;
+ if (mediaList == null || mediaList.isEmpty()) {
+ initErrorView();
+ }
+ else {
+ bottomProgressBar.setVisibility(View.GONE);
+ progressBar.setVisibility(GONE);
+ imagesAdapter.addAll(mediaList);
+ imagesAdapter.notifyDataSetChanged();
+ ((SearchActivity)getContext()).viewPagerNotifyDataSetChanged();
+ for (Media m : mediaList) {
+ final String pageId = m.getPageId();
+ if (pageId != null) {
+ replaceTitlesWithCaptions(PAGE_ID_PREFIX + pageId, mediaSize++);
+ }
+ }
+ }
+ }
+
+ /**
+ * In explore we first show title and simultaneously call the API to retrieve captions
+ * When captions are retrieved they replace title
+ */
+
+ public void replaceTitlesWithCaptions(String wikibaseIdentifier, int position) {
+ compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .subscribe(subscriber -> {
+ handleLabelforImage(subscriber, position);
+ }));
+
+ }
+
+ private void handleLabelforImage(String s, int position) {
+ if (!s.trim().equals(getString(R.string.detail_caption_empty))) {
+ imagesAdapter.getItem(position).setThumbnailTitle(s);
+ imagesAdapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Logs and handles API error scenario
+ * @param throwable
+ */
+ private void handleError(Throwable throwable) {
+ Timber.e(throwable, "Error occurred while loading queried images");
+ try {
+ ViewUtil.showShortSnackbar(imagesRecyclerView, R.string.error_loading_images);
+ }catch (Exception e){
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Handles the UI updates for a error scenario
+ */
+ private void initErrorView() {
+ progressBar.setVisibility(GONE);
+ imagesNotFoundView.setVisibility(VISIBLE);
+ imagesNotFoundView.setText(getString(R.string.images_not_found,query));
+ }
+
+ /**
+ * Handles the UI updates for no internet scenario
+ */
+ private void handleNoInternet() {
+ if (null
+ != getView()) {//We have exposed public methods to update our ui, we will have to add null checks until we make this lifecycle aware
+ if (null != progressBar) {
+ progressBar.setVisibility(GONE);
+ }
+ ViewUtil.showShortSnackbar(imagesRecyclerView, R.string.no_internet);
+ } else {
+ Timber.d("Attempt to update fragment ui after its view was destroyed");
+ }
+ }
+
+ /**
+ * returns total number of images present in the recyclerview adapter.
+ */
+ public int getTotalImagesCount(){
+ if (imagesAdapter == null) {
+ return 0;
+ }
+ else {
+ return imagesAdapter.getItemCount();
+ }
+ }
+
+ /**
+ * returns Media Object at position
+ * @param i position of Media in the recyclerview adapter.
+ */
+ public Media getImageAtPosition(int i) {
+ if (imagesAdapter.getItem(i).getFilename() == null) {
+ // not yet ready to return data
+ return null;
+ }
+ else {
+ return imagesAdapter.getItem(i);
+ }
+ }
+
+ @Override public void onDestroyView() {
+ super.onDestroyView();
+ compositeDisposable.clear();
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesRenderer.java b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesRenderer.java
new file mode 100644
index 00000000000..8985a84845d
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesRenderer.java
@@ -0,0 +1,74 @@
+package fr.free.nrw.commons.explore.images;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.facebook.drawee.view.SimpleDraweeView;
+import com.pedrogomez.renderers.Renderer;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.R;
+
+/**
+ * presentation logic of individual image in search is handled here
+ **/
+class SearchImagesRenderer extends Renderer {
+ @BindView(R.id.categoryImageTitle) TextView tvImageName;
+ @BindView(R.id.categoryImageAuthor) TextView categoryImageAuthor;
+ @BindView(R.id.categoryImageView) SimpleDraweeView browseImage;
+
+ private final ImageClickedListener listener;
+
+ SearchImagesRenderer(ImageClickedListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
+ return layoutInflater.inflate(R.layout.layout_category_images, viewGroup, false);
+ }
+
+ @Override
+ protected void setUpView(View view) {
+ ButterKnife.bind(this, view);
+ }
+
+ @Override
+ protected void hookListeners(View view) {
+ view.setOnClickListener(v -> {
+ Media item = getContent();
+ if (listener != null) {
+ listener.imageClicked(item);
+ }
+ });
+ }
+
+ @Override
+ public void render() {
+ Media item = getContent();
+ tvImageName.setText(item.getThumbnailTitle());
+ browseImage.setImageURI(item.getThumbUrl());
+ setAuthorView(item, categoryImageAuthor);
+ }
+
+ interface ImageClickedListener {
+ void imageClicked(Media item);
+ }
+
+ /**
+ * formats author name as "Uploaded by: authorName" and sets it in textview
+ */
+ private void setAuthorView(Media item, TextView author) {
+ if (item.getCreator() != null && !item.getCreator().equals("")) {
+ author.setVisibility(View.VISIBLE);
+ String uploadedByTemplate = getContext().getString(R.string.image_uploaded_by);
+ author.setText(String.format(uploadedByTemplate, item.getCreator()));
+ } else {
+ author.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/Depictions.kt b/app/src/main/java/fr/free/nrw/commons/media/Depictions.kt
new file mode 100644
index 00000000000..ae4191d0697
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/Depictions.kt
@@ -0,0 +1,30 @@
+package fr.free.nrw.commons.media
+
+import android.os.Parcelable
+import androidx.annotation.WorkerThread
+import fr.free.nrw.commons.wikidata.WikidataProperties.DEPICTS
+import kotlinx.android.parcel.Parcelize
+import org.wikipedia.wikidata.DataValue.DataValueEntityId
+import org.wikipedia.wikidata.Entities
+import java.util.*
+
+@Parcelize
+data class Depictions(val depictions: List) : Parcelable {
+ companion object {
+ @JvmStatic
+ @WorkerThread
+ fun from(entities: Entities, mediaClient: MediaClient) =
+ Depictions(
+ entities.first?.statements
+ ?.getOrElse(DEPICTS.propertyName, { emptyList() })
+ ?.map { statement ->
+ (statement.mainSnak.dataValue as DataValueEntityId).value.id
+ }
+ ?.map { id -> IdAndLabel(id, fetchLabel(mediaClient, id)) }
+ ?: emptyList()
+ )
+
+ private fun fetchLabel(mediaClient: MediaClient, id: String) =
+ mediaClient.getLabelForDepiction(id, Locale.getDefault().language).blockingGet()
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/IdAndLabel.kt b/app/src/main/java/fr/free/nrw/commons/media/IdAndLabel.kt
new file mode 100644
index 00000000000..00ca69f0825
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/IdAndLabel.kt
@@ -0,0 +1,14 @@
+package fr.free.nrw.commons.media
+
+import android.os.Parcelable
+import kotlinx.android.parcel.Parcelize
+import org.wikipedia.wikidata.Entities
+
+@Parcelize
+data class IdAndLabel(val entityId: String, val entityLabel: String) : Parcelable {
+ constructor(entityId: String, entities: MutableMap) : this(
+ entityId,
+ entities.values.first().labels().values.first().value()
+ )
+}
+
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java
new file mode 100644
index 00000000000..a6ba1897e8f
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java
@@ -0,0 +1,226 @@
+package fr.free.nrw.commons.media;
+
+
+import androidx.annotation.NonNull;
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.utils.CommonsDateUtil;
+import io.reactivex.Observable;
+import io.reactivex.Single;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.wikipedia.dataclient.mwapi.MwQueryResponse;
+import org.wikipedia.wikidata.Entities;
+import org.wikipedia.wikidata.Entities.Entity;
+import org.wikipedia.wikidata.Entities.Label;
+import timber.log.Timber;
+
+/**
+ * Media Client to handle custom calls to Commons MediaWiki APIs
+ */
+@Singleton
+public class MediaClient {
+
+ private final MediaInterface mediaInterface;
+ private final MediaDetailInterface mediaDetailInterface;
+
+ //OkHttpJsonApiClient used JsonKvStore for this. I don't know why.
+ private Map> continuationStore;
+ public static final String NO_CAPTION = "No caption";
+ private static final String NO_DEPICTION = "No depiction";
+
+ @Inject
+ public MediaClient(MediaInterface mediaInterface, MediaDetailInterface mediaDetailInterface) {
+ this.mediaInterface = mediaInterface;
+ this.mediaDetailInterface = mediaDetailInterface;
+ this.continuationStore = new HashMap<>();
+ }
+
+ /**
+ * Checks if a page exists on Commons
+ * The same method can be used to check for file or talk page
+ *
+ * @param title File:Test.jpg or Commons:Deletion_requests/File:Test1.jpeg
+ */
+ public Single checkPageExistsUsingTitle(String title) {
+ return mediaInterface.checkPageExistsUsingTitle(title)
+ .map(mwQueryResponse -> mwQueryResponse
+ .query().firstPage().pageId() > 0)
+ .singleOrError();
+ }
+
+ /**
+ * Take the fileSha and returns whether a file with a matching SHA exists or not
+ *
+ * @param fileSha SHA of the file to be checked
+ */
+ public Single checkFileExistsUsingSha(String fileSha) {
+ return mediaInterface.checkFileExistsUsingSha(fileSha)
+ .map(mwQueryResponse -> mwQueryResponse
+ .query().allImages().size() > 0)
+ .singleOrError();
+ }
+
+ /**
+ * This method takes the category 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.
+ *
+ * @param category the search category. Must start with "Category:"
+ * @return
+ */
+ public Single> getMediaListFromCategory(String category) {
+ return responseToMediaList(
+ continuationStore.containsKey("category_" + category) ?
+ mediaInterface.getMediaListFromCategory(category, 10, continuationStore.get("category_" + category)) : //if true
+ mediaInterface.getMediaListFromCategory(category, 10, Collections.emptyMap()),
+ "category_" + category); //if false
+
+ }
+
+ /**
+ * 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.
+ *
+ * @param keyword the search keyword
+ * @return
+ */
+ public Single> getMediaListFromSearch(String keyword) {
+ return responseToMediaList(
+ continuationStore.containsKey("search_" + keyword) && (continuationStore.get("search_" + keyword) != null) ?
+ mediaInterface.getMediaListFromSearch(keyword, 10, continuationStore.get("search_" + keyword)) : //if true
+ mediaInterface.getMediaListFromSearch(keyword, 10, Collections.emptyMap()), //if false
+ "search_" + keyword);
+
+ }
+
+ private Single> responseToMediaList(Observable response, String key) {
+ return response.flatMap(mwQueryResponse -> {
+ if (null == mwQueryResponse
+ || null == mwQueryResponse.query()
+ || null == mwQueryResponse.query().pages()) {
+ return Observable.empty();
+ }
+ continuationStore.put(key, mwQueryResponse.continuation());
+ return Observable.fromIterable(mwQueryResponse.query().pages());
+ })
+ .map(Media::from)
+ .collect(ArrayList::new, List::add);
+ }
+
+ /**
+ * Fetches Media object from the imageInfo API
+ *
+ * @param titles the tiles to be searched for. Can be filename or template name
+ * @return
+ */
+ public Single getMedia(String titles) {
+ return mediaInterface.getMedia(titles)
+ .flatMap(mwQueryResponse -> {
+ if (null == mwQueryResponse
+ || null == mwQueryResponse.query()
+ || null == mwQueryResponse.query().firstPage()) {
+ return Observable.empty();
+ }
+ return Observable.just(mwQueryResponse.query().firstPage());
+ })
+ .map(Media::from)
+ .single(Media.EMPTY);
+ }
+
+ /**
+ * The method returns the picture of the day
+ *
+ * @return Media object corresponding to the picture of the day
+ */
+ @NonNull
+ public Single getPictureOfTheDay() {
+ String date = CommonsDateUtil.getIso8601DateFormatShort().format(new Date());
+ Timber.d("Current date is %s", date);
+ String template = "Template:Potd/" + date;
+ return mediaInterface.getMediaWithGenerator(template)
+ .flatMap(mwQueryResponse -> {
+ if (null == mwQueryResponse
+ || null == mwQueryResponse.query()
+ || null == mwQueryResponse.query().firstPage()) {
+ return Observable.empty();
+ }
+ return Observable.just(mwQueryResponse.query().firstPage());
+ })
+ .map(Media::from)
+ .single(Media.EMPTY);
+ }
+
+
+ @NonNull
+ public Single getPageHtml(String title){
+ return mediaInterface.getPageHtml(title)
+ .filter(MwParseResponse::success)
+ .map(MwParseResponse::parse)
+ .map(MwParseResult::text)
+ .first("");
+ }
+
+
+ /**
+ * @return caption for image using wikibaseIdentifier
+ */
+ public Single getCaptionByWikibaseIdentifier(String wikibaseIdentifier) {
+ return mediaDetailInterface.getCaptionForImage(Locale.getDefault().getLanguage(), wikibaseIdentifier)
+ .map(mediaDetailResponse -> {
+ if (isSuccess(mediaDetailResponse)) {
+ for (Entity wikibaseItem : mediaDetailResponse.entities().values()) {
+ for (Label label : wikibaseItem.labels().values()) {
+ return label.value();
+ }
+ }
+ }
+ return NO_CAPTION;
+ })
+ .singleOrError();
+ }
+
+ private boolean isSuccess(Entities response) {
+ return response != null && response.getSuccess() == 1 && response.entities() != null;
+ }
+
+ /**
+ * Fetches Structured data from API
+ *
+ * @param filename
+ * @return a map containing caption and depictions (empty string in the map if no caption/depictions)
+ */
+ public Single getDepictions(String filename) {
+ return mediaDetailInterface.fetchEntitiesByFileName(Locale.getDefault().getLanguage(), filename)
+ .map(entities -> Depictions.from(entities, this))
+ .singleOrError();
+ }
+
+ /**
+ * Gets labels for Depictions using Entity Id from MediaWikiAPI
+ *
+ * @param entityId EntityId (Ex: Q81566) of the depict entity
+ * @return label
+ */
+ public Single getLabelForDepiction(String entityId, String language) {
+ return mediaDetailInterface.getEntity(entityId, language)
+ .map(entities -> {
+ if (isSuccess(entities)) {
+ for (Entity entity : entities.entities().values()) {
+ for (Label label : entity.labels().values()) {
+ return label.value();
+ }
+ }
+ }
+ throw new RuntimeException("failed getEntities");
+ })
+ .singleOrError();
+ }
+
+ }
+
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java
index baf6cac8b27..a9a402b6c72 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java
@@ -1,10 +1,13 @@
package fr.free.nrw.commons.media;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
import android.annotation.SuppressLint;
-import android.graphics.drawable.Animatable;
import android.app.AlertDialog;
-import android.content.Intent;
import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Animatable;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
@@ -22,28 +25,17 @@
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
-
+import androidx.annotation.Nullable;
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.OnClick;
import com.facebook.drawee.backends.pipeline.Fresco;
-import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.controller.ControllerListener;
+import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.SimpleDraweeView;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
-
-import org.apache.commons.lang3.StringUtils;
-import org.wikipedia.util.DateUtil;
-
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.Locale;
-
-import javax.inject.Inject;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import butterknife.OnClick;
-import androidx.annotation.Nullable;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.R;
@@ -53,19 +45,24 @@
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.delete.DeleteHelper;
import fr.free.nrw.commons.delete.ReasonBuilder;
+import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.ui.widget.CompatTextView;
import fr.free.nrw.commons.ui.widget.HtmlTextView;
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Locale;
+import javax.inject.Inject;
+import org.apache.commons.lang3.StringUtils;
+import org.wikipedia.util.DateUtil;
import timber.log.Timber;
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
-
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private boolean editable;
@@ -108,6 +105,12 @@ public static MediaDetailFragment forMedia(int index, boolean editable, boolean
LinearLayout imageSpacer;
@BindView(R.id.mediaDetailTitle)
TextView title;
+ @BindView(R.id.caption_layout)
+ LinearLayout captionLayout;
+ @BindView(R.id.depicts_layout)
+ LinearLayout depictsLayout;
+ @BindView(R.id.media_detail_caption)
+ TextView mediaCaption;
@BindView(R.id.mediaDetailDesc)
HtmlTextView desc;
@BindView(R.id.mediaDetailAuthor)
@@ -126,6 +129,8 @@ public static MediaDetailFragment forMedia(int index, boolean editable, boolean
LinearLayout nominatedForDeletion;
@BindView(R.id.mediaDetailCategoryContainer)
LinearLayout categoryContainer;
+ @BindView(R.id.media_detail_depiction_container)
+ LinearLayout depictionContainer;
@BindView(R.id.authorLinearLayout)
LinearLayout authorLayout;
@BindView(R.id.nominateDeletion)
@@ -134,8 +139,15 @@ public static MediaDetailFragment forMedia(int index, boolean editable, boolean
ScrollView scrollView;
private ArrayList categoryNames;
+ /**
+ * Depicts is a feature part of Structured data. Multiple Depictions can be added for an image just like categories.
+ * However unlike categories depictions is multi-lingual
+ * Ex: key: en value: monument
+ */
+ private Depictions depictions;
private boolean categoriesLoaded = false;
private boolean categoriesPresent = false;
+ private boolean depictionLoaded = false;
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
private ViewTreeObserver.OnScrollChangedListener scrollListener;
@@ -243,7 +255,7 @@ private void displayMediaDetails() {
desc.setHtmlText(media.getDescription());
license.setText(media.getLicense());
- Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename())
+ Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename(), media.getPageId())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::setTextFields);
@@ -318,18 +330,32 @@ private void setTextFields(Media media) {
coordinates.setText(prettyCoordinates(media));
uploadedDate.setText(prettyUploadedDate(media));
mediaDiscussion.setText(prettyDiscussion(media));
+ if (prettyCaption(media).equals(getContext().getString(R.string.detail_caption_empty))) {
+ captionLayout.setVisibility(GONE);
+ } else mediaCaption.setText(prettyCaption(media));
+
categoryNames.clear();
categoryNames.addAll(media.getCategories());
+ depictions=media.getDepiction();
+
+ depictionLoaded = true;
+
categoriesLoaded = true;
categoriesPresent = (categoryNames.size() > 0);
if (!categoriesPresent) {
// Stick in a filler element.
categoryNames.add(getString(R.string.detail_panel_cats_none));
}
+
rebuildCatList();
+ if(depictions != null) {
+ rebuildDepictionList();
+ }
+ else depictsLayout.setVisibility(GONE);
+
if (media.getCreator() == null || media.getCreator().equals("")) {
authorLayout.setVisibility(GONE);
} else {
@@ -339,6 +365,21 @@ private void setTextFields(Media media) {
checkDeletion(media);
}
+ /**
+ * Populates media details fragment with depiction list
+ */
+ private void rebuildDepictionList() {
+ depictionContainer.removeAllViews();
+ for (IdAndLabel depiction : depictions.getDepictions()) {
+ depictionContainer.addView(
+ buildDepictLabel(
+ depiction.getEntityLabel(),
+ depiction.getEntityId(),
+ depictionContainer
+ ));
+ }
+ }
+
@OnClick(R.id.mediaDetailLicense)
public void onMediaDetailLicenceClicked(){
String url = media.getLicenseUrl();
@@ -505,6 +546,26 @@ private void rebuildCatList() {
}
}
+ /**
+ * Add view to depictions obtained also tapping on depictions should open the url
+ */
+ private View buildDepictLabel(String depictionName, String entityId, LinearLayout depictionContainer) {
+ final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, depictionContainer, false);
+ final CompatTextView textView = item.findViewById(R.id.mediaDetailCategoryItemText);
+
+ textView.setText(depictionName);
+ if (depictionLoaded) {
+ item.setOnClickListener(view -> {
+ DepictedItem depictedItem = new DepictedItem(depictionName, "", "", false, entityId);
+ Intent intent = new Intent(getContext(), WikidataItemDetailsActivity.class);
+ intent.putExtra("wikidataItemName", depictedItem.getName());
+ intent.putExtra("entityId", depictedItem.getId());
+ getContext().startActivity(intent);
+ });
+ }
+ return item;
+ }
+
private View buildCatLabel(final String catName, ViewGroup categoryContainer) {
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false);
final CompatTextView textView = item.findViewById(R.id.mediaDetailCategoryItemText);
@@ -534,9 +595,24 @@ private void updateTheDarkness() {
image.setAlpha(1.0f - scrollPercentage);
}
+ /**
+ * Returns captions for media details
+ *
+ * @param media object of class media
+ * @return caption as string
+ */
+ private String prettyCaption(Media media) {
+ String caption = media.getCaption().trim();
+ if (caption.equals("")) {
+ return getString(R.string.detail_caption_empty);
+ } else {
+ return caption;
+ }
+ }
+
private String prettyDescription(Media media) {
// @todo use UI language when multilingual descs are available
- String desc = media.getDescription(locale.getLanguage()).trim();
+ String desc = media.getDescription();
if (desc.equals("")) {
return getString(R.string.detail_description_empty);
} else {
@@ -582,7 +658,7 @@ private String prettyCoordinates(Media media) {
}
private void checkDeletion(Media media){
- if (media.getRequestedDeletion()){
+ if (media.isRequestedDeletion()){
delete.setVisibility(GONE);
nominatedForDeletion.setVisibility(VISIBLE);
} else if (!isCategoryImage) {
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailInterface.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailInterface.java
index ef9559c29ab..c972ccc12cc 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailInterface.java
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailInterface.java
@@ -1,7 +1,6 @@
package fr.free.nrw.commons.media;
import io.reactivex.Observable;
-import io.reactivex.Single;
import org.wikipedia.wikidata.Entities;
import retrofit2.http.GET;
import retrofit2.http.Query;
@@ -21,11 +20,12 @@ public interface MediaDetailInterface {
/**
* Gets labels for Depictions using Entity Id from MediaWikiAPI
- * @param entityId EntityId (Ex: Q81566) of the depict entity
*
+ * @param entityId EntityId (Ex: Q81566) of the depict entity
+ * @param language user's locale
*/
@GET("/w/api.php?format=json&action=wbgetentities&props=labels&languagefallback=1")
- Single getEntity(@Query("ids") String entityId);
+ Observable getEntity(@Query("ids") String entityId, @Query("languages") String language);
/**
* Fetches caption using wikibaseIdentifier
@@ -33,5 +33,5 @@ public interface MediaDetailInterface {
* @param wikibaseIdentifier pageId for the media
*/
@GET("/w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1&sites=commonswiki")
- Observable getEntityForImage(@Query("languages") String language, @Query("ids") String wikibaseIdentifier);
+ Observable getCaptionForImage(@Query("languages") String language, @Query("ids") String wikibaseIdentifier);
}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java
index 59f779b4cdb..39c81386e39 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java
@@ -13,6 +13,7 @@
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.Toast;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java
index c104235a987..10dd02a8323 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java
@@ -1,8 +1,11 @@
package fr.free.nrw.commons.media;
-import io.reactivex.Single;
-import java.util.Map;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
+
+import java.util.Map;
+
+import fr.free.nrw.commons.depictions.models.DepictionResponse;
+import io.reactivex.Observable;
import retrofit2.http.GET;
import retrofit2.http.Query;
import retrofit2.http.QueryMap;
@@ -14,7 +17,6 @@ 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.
*
@@ -22,7 +24,7 @@ public interface MediaInterface {
* @return
*/
@GET("w/api.php?action=query&format=json&formatversion=2")
- Single checkPageExistsUsingTitle(@Query("titles") String title);
+ Observable checkPageExistsUsingTitle(@Query("titles") String title);
/**
* Check if file exists
@@ -31,7 +33,7 @@ public interface MediaInterface {
* @return
*/
@GET("w/api.php?action=query&format=json&formatversion=2&list=allimages")
- Single checkFileExistsUsingSha(@Query("aisha1") String aisha1);
+ Observable checkFileExistsUsingSha(@Query("aisha1") String aisha1);
/**
* This method retrieves a list of Media objects filtered using image generator query
@@ -44,34 +46,20 @@ public interface MediaInterface {
@GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
"&generator=categorymembers&gcmtype=file&gcmsort=timestamp&gcmdir=desc" + //Category parameters
MEDIA_PARAMS)
- Single getMediaListFromCategory(@Query("gcmtitle") String category, @Query("gcmlimit") int itemLimit, @QueryMap Map continuation);
-
-
- /**
- * This method retrieves a list of Media objects for a given user name
- *
- * @param username user's Wikimedia Commons username.
- * @param itemLimit how many images are returned
- * @param continuation the continuation string from the previous query or empty map
- * @return
- */
- @GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
- "&generator=allimages&gaisort=timestamp&gaidir=older" + MEDIA_PARAMS)
- Single getMediaListForUser(@Query("gaiuser") String username,
- @Query("gailimit") int itemLimit, @QueryMap(encoded = true) Map continuation);
+ Observable getMediaListFromCategory(@Query("gcmtitle") String category, @Query("gcmlimit") int itemLimit, @QueryMap Map continuation);
/**
* This method retrieves a list of Media objects filtered using image generator query
*
* @param keyword the searched keyword
* @param itemLimit how many images are returned
- * @param offset the offset in the result set
+ * @param continuation the continuation string from the previous query
* @return
*/
@GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
"&generator=search&gsrwhat=text&gsrnamespace=6" + //Search parameters
MEDIA_PARAMS)
- Single getMediaListFromSearch(@Query("gsrsearch") String keyword, @Query("gsrlimit") int itemLimit, @Query("gsroffset") int offset);
+ Observable getMediaListFromSearch(@Query("gsrsearch") String keyword, @Query("gsrlimit") int itemLimit, @QueryMap Map continuation);
/**
* Fetches Media object from the imageInfo API
@@ -81,17 +69,7 @@ Single getMediaListForUser(@Query("gaiuser") String username,
*/
@GET("w/api.php?action=query&format=json&formatversion=2" +
MEDIA_PARAMS)
- Single getMedia(@Query("titles") String title);
-
- /**
- * Fetches Media object from the imageInfo API
- *
- * @param pageIds the ids to be searched for
- * @return
- */
- @GET("w/api.php?action=query&format=json&formatversion=2" +
- MEDIA_PARAMS)
- Single getMediaById(@Query("pageids") String pageIds);
+ Observable getMedia(@Query("titles") String title);
/**
* Fetches Media object from the imageInfo API
@@ -102,10 +80,10 @@ Single getMediaListForUser(@Query("gaiuser") String username,
*/
@GET("w/api.php?action=query&format=json&formatversion=2&generator=images" +
MEDIA_PARAMS)
- Single getMediaWithGenerator(@Query("titles") String title);
+ Observable getMediaWithGenerator(@Query("titles") String title);
@GET("w/api.php?format=json&action=parse&prop=text")
- Single getPageHtml(@Query("page") String title);
+ Observable getPageHtml(@Query("page") String title);
/**
* Fetches caption using file name
@@ -113,18 +91,16 @@ Single getMediaListForUser(@Query("gaiuser") String username,
* @param filename name of the file to be used for fetching captions
* */
@GET("w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1")
- Single fetchCaptionByFilename(@Query("language") String language, @Query("titles") String filename);
+ Observable fetchCaptionByFilename(@Query("language") String language, @Query("titles") String filename);
/**
* Fetches list of images from a depiction entity
- * @param query depictionEntityId
- * @param srlimit the number of items to fetch
+ *
+ * @param query depictionEntityId
* @param sroffset number od depictions already fetched, this is useful in implementing pagination
*/
- @GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
- "&generator=search&gsrnamespace=6" + //Search parameters
- MEDIA_PARAMS)
- Single fetchImagesForDepictedItem(@Query("gsrsearch") String query,
- @Query("gsrlimit")String srlimit, @Query("gsroffset") String sroffset);
+
+ @GET("w/api.php?action=query&list=search&format=json&srnamespace=6")
+ Observable fetchImagesForDepictedItem(@Query("srsearch") String query, @Query("sroffset") String sroffset);
}
diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java
index 6b306690b74..c3fcf9b089b 100644
--- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java
+++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java
@@ -1,21 +1,16 @@
package fr.free.nrw.commons.mwapi;
-import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LEADERBOARD_END_POINT;
-import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.UPDATE_AVATAR_END_POINT;
-
import android.text.TextUtils;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
+import fr.free.nrw.commons.achievements.FeaturedImages;
+import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
-import fr.free.nrw.commons.explore.depictions.DepictsClient;
+import fr.free.nrw.commons.depictions.subClass.models.SparqlResponse;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.model.NearbyResponse;
import fr.free.nrw.commons.nearby.model.NearbyResultItem;
-import fr.free.nrw.commons.profile.achievements.FeaturedImages;
-import fr.free.nrw.commons.profile.achievements.FeedbackResponse;
-import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse;
-import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse;
import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.ConfigUtils;
@@ -43,9 +38,7 @@
public class OkHttpJsonApiClient {
private final OkHttpClient okHttpClient;
- private final DepictsClient depictsClient;
private final HttpUrl wikiMediaToolforgeUrl;
- private final HttpUrl wikiMediaTestToolforgeUrl;
private final String sparqlQueryUrl;
private final String campaignsUrl;
private final Gson gson;
@@ -53,107 +46,17 @@ public class OkHttpJsonApiClient {
@Inject
public OkHttpJsonApiClient(OkHttpClient okHttpClient,
- DepictsClient depictsClient,
HttpUrl wikiMediaToolforgeUrl,
- HttpUrl wikiMediaTestToolforgeUrl,
String sparqlQueryUrl,
String campaignsUrl,
Gson gson) {
this.okHttpClient = okHttpClient;
- this.depictsClient = depictsClient;
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
- this.wikiMediaTestToolforgeUrl = wikiMediaTestToolforgeUrl;
this.sparqlQueryUrl = sparqlQueryUrl;
this.campaignsUrl = campaignsUrl;
this.gson = gson;
}
- /**
- * The method will gradually calls the leaderboard API and fetches the leaderboard
- * @param userName username of leaderboard user
- * @param duration duration for leaderboard
- * @param category category for leaderboard
- * @param limit page size limit for list
- * @param offset offset for the list
- * @return LeaderboardResponse object
- */
- @NonNull
- public Observable getLeaderboard(String userName, String duration, String category, String limit, String offset) {
- final String fetchLeaderboardUrlTemplate = wikiMediaTestToolforgeUrl
- + LEADERBOARD_END_POINT;
- String url = String.format(Locale.ENGLISH,
- fetchLeaderboardUrlTemplate,
- userName,
- duration,
- category,
- limit,
- offset);
- HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
- urlBuilder.addQueryParameter("user", userName);
- urlBuilder.addQueryParameter("duration", duration);
- urlBuilder.addQueryParameter("category", category);
- urlBuilder.addQueryParameter("limit", limit);
- urlBuilder.addQueryParameter("offset", offset);
- Timber.i("Url %s", urlBuilder.toString());
- Request request = new Request.Builder()
- .url(urlBuilder.toString())
- .build();
- return Observable.fromCallable(() -> {
- Response response = okHttpClient.newCall(request).execute();
- if (response != null && response.body() != null && response.isSuccessful()) {
- String json = response.body().string();
- if (json == null) {
- return new LeaderboardResponse();
- }
- Timber.d("Response for leaderboard is %s", json);
- try {
- return gson.fromJson(json, LeaderboardResponse.class);
- } catch (Exception e) {
- return new LeaderboardResponse();
- }
- }
- return new LeaderboardResponse();
- });
- }
-
- /**
- * This method will update the leaderboard user avatar
- * @param username username to update
- * @param avatar url of the new avatar
- * @return UpdateAvatarResponse object
- */
- @NonNull
- public Single setAvatar(String username, String avatar) {
- final String urlTemplate = wikiMediaTestToolforgeUrl
- + UPDATE_AVATAR_END_POINT;
- return Single.fromCallable(() -> {
- String url = String.format(Locale.ENGLISH,
- urlTemplate,
- username,
- avatar);
- HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
- urlBuilder.addQueryParameter("user", username);
- urlBuilder.addQueryParameter("avatar", avatar);
- Timber.i("Url %s", urlBuilder.toString());
- Request request = new Request.Builder()
- .url(urlBuilder.toString())
- .build();
- Response response = okHttpClient.newCall(request).execute();
- if (response != null && response.body() != null && response.isSuccessful()) {
- String json = response.body().string();
- if (json == null) {
- return null;
- }
- try {
- return gson.fromJson(json, UpdateAvatarResponse.class);
- } catch (Exception e) {
- return new UpdateAvatarResponse();
- }
- }
- return null;
- });
- }
-
@NonNull
public Single getUploadCount(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
@@ -239,6 +142,7 @@ public Single getAchievements(String userName) {
userName);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
+ Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
@@ -302,36 +206,34 @@ public Observable> getNearbyPlaces(LatLng cur, String language, doub
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
* bridge -> suspended bridge, aqueduct, etc
*/
- public Single> getChildDepictions(String qid, int startPosition,
- int limit) throws IOException {
- return depictedItemsFrom(sparqlQuery(qid, startPosition, limit,"/queries/subclasses_query.rq"));
+ public Observable> getChildQIDs(String qid) throws IOException {
+ return depictedItemsFrom(sparqlQuery(qid, "/queries/subclasses_query.rq"));
}
/**
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
* bridge -> suspended bridge, aqueduct, etc
*/
- public Single> getParentDepictions(String qid, int startPosition,
- int limit) throws IOException {
- return depictedItemsFrom(sparqlQuery(qid, startPosition, limit,
- "/queries/parentclasses_query.rq"));
+ public Observable> getParentQIDs(String qid) throws IOException {
+ return depictedItemsFrom(sparqlQuery(qid, "/queries/parentclasses_query.rq"));
}
- private Single> depictedItemsFrom(Request request) {
- return depictsClient.toDepictions(Single.fromCallable(() -> {
+ private Observable> depictedItemsFrom(Request request) {
+ return Observable.fromCallable(() -> {
try (ResponseBody body = okHttpClient.newCall(request).execute().body()) {
- return gson.fromJson(body.string(), SparqlResponse.class);
+ return gson.fromJson(body.string(), SparqlResponse.class).toDepictedItems();
+ }catch (Exception e) {
+ Timber.e(e);
+ return new ArrayList();
}
- }).doOnError(Timber::e));
+ }).doOnError(Timber::e);
}
@NotNull
- private Request sparqlQuery(String qid, int startPosition, int limit, String fileName) throws IOException {
- String query = FileUtils.readFromResource(fileName)
- .replace("${QID}", qid)
- .replace("${LANG}", "\"" + Locale.getDefault().getLanguage() + "\"")
- .replace("${LIMIT}",""+ limit)
- .replace("${OFFSET}",""+ startPosition);
+ private Request sparqlQuery(String qid, String fileName) throws IOException {
+ String query = FileUtils.readFromResource(fileName).
+ replace("${QID}", qid)
+ .replace("${LANG}", "\"" + Locale.getDefault().getLanguage() + "\"");
HttpUrl.Builder urlBuilder = HttpUrl
.parse(sparqlQueryUrl)
.newBuilder()
diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRemoteDataSource.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRemoteDataSource.java
new file mode 100644
index 00000000000..7ea44ca68d2
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRemoteDataSource.java
@@ -0,0 +1,232 @@
+package fr.free.nrw.commons.repository;
+
+import fr.free.nrw.commons.upload.ImageCoordinates;
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import fr.free.nrw.commons.category.CategoriesModel;
+import fr.free.nrw.commons.category.CategoryItem;
+import fr.free.nrw.commons.contributions.Contribution;
+import fr.free.nrw.commons.filepicker.UploadableFile;
+import fr.free.nrw.commons.location.LatLng;
+import fr.free.nrw.commons.nearby.NearbyPlaces;
+import fr.free.nrw.commons.nearby.Place;
+import fr.free.nrw.commons.upload.SimilarImageInterface;
+import fr.free.nrw.commons.upload.UploadController;
+import fr.free.nrw.commons.upload.UploadModel;
+import fr.free.nrw.commons.upload.UploadModel.UploadItem;
+import fr.free.nrw.commons.upload.structure.depictions.DepictModel;
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
+import io.reactivex.Observable;
+import io.reactivex.Single;
+
+/**
+ * This class would act as the data source for remote operations for UploadActivity
+ */
+@Singleton
+public class UploadRemoteDataSource {
+
+ private static final double NEARBY_RADIUS_IN_KILO_METERS = 0.1; //100 meters
+
+ private UploadModel uploadModel;
+ private UploadController uploadController;
+ private CategoriesModel categoriesModel;
+ private DepictModel depictModel;
+ private NearbyPlaces nearbyPlaces;
+
+ @Inject
+ public UploadRemoteDataSource(UploadModel uploadModel, UploadController uploadController,
+ CategoriesModel categoriesModel, NearbyPlaces nearbyPlaces, DepictModel depictModel) {
+ this.uploadModel = uploadModel;
+ this.uploadController = uploadController;
+ this.categoriesModel = categoriesModel;
+ this.nearbyPlaces = nearbyPlaces;
+ this.depictModel = depictModel;
+ }
+
+ /**
+ * asks the UploadModel to build the contributions
+ *
+ * @return
+ */
+ public Observable buildContributions() {
+ return uploadModel.buildContributions();
+ }
+
+ /**
+ * asks the UploadService to star the uplaod for
+ *
+ * @param contribution
+ */
+ public void startUpload(Contribution contribution) {
+ uploadController.startUpload(contribution);
+ }
+
+ /**
+ * returns the list of UploadItem from the UploadModel
+ *
+ * @return
+ */
+ public List getUploads() {
+ return uploadModel.getUploads();
+ }
+
+ /**
+ * Prepare the UploadService for the upload
+ */
+ public void prepareService() {
+ uploadController.prepareService();
+ }
+
+ /**
+ * Clean up the selected categories
+ */
+ public void cleanUp(){
+ //This needs further refactoring, this should not be here, right now the structure wont suppoort rhis
+ categoriesModel.cleanUp();
+ depictModel.cleanUp();
+ }
+
+ /**
+ * returnt the list of selected categories
+ *
+ * @return
+ */
+ public List getSelectedCategories() {
+ return categoriesModel.getSelectedCategories();
+ }
+
+ /**
+ * all categories from MWApi
+ *
+ * @param query
+ * @param imageTitleList
+ * @return
+ */
+ public Observable searchAll(String query, List imageTitleList) {
+ return categoriesModel.searchAll(query, imageTitleList);
+ }
+
+ /**
+ * returns the string list of categories
+ *
+ * @return
+ */
+ public List getCategoryStringList() {
+ return categoriesModel.getCategoryStringList();
+ }
+
+ /**
+ * sets the selected categories in the UploadModel
+ *
+ * @param categoryStringList
+ */
+ public void setSelectedCategories(List categoryStringList) {
+ uploadModel.setSelectedCategories(categoryStringList);
+ }
+
+ /**
+ * handles category selection/unselection
+ *
+ * @param categoryItem
+ */
+ public void onCategoryClicked(CategoryItem categoryItem) {
+ categoriesModel.onCategoryItemClicked(categoryItem);
+ }
+
+ /**
+ * returns category sorted based on similarity with query
+ *
+ * @param query
+ * @return
+ */
+ public Comparator sortBySimilarity(String query) {
+ return categoriesModel.sortBySimilarity(query);
+ }
+
+ /**
+ * prunes the category list for irrelevant categories see #750
+ *
+ * @param name
+ * @return
+ */
+ public boolean containsYear(String name) {
+ return categoriesModel.containsYear(name);
+ }
+
+ /**
+ * pre process the UploadableFile
+ *
+ * @param uploadableFile
+ * @param place
+ * @param similarImageInterface
+ * @return
+ */
+ public Observable preProcessImage(UploadableFile uploadableFile, Place place,
+ SimilarImageInterface similarImageInterface) {
+ return uploadModel.preProcessImage(uploadableFile, place, similarImageInterface);
+ }
+
+ /**
+ * ask the UplaodModel for the image quality of the UploadItem
+ *
+ * @param uploadItem
+ * @return
+ */
+ public Single getImageQuality(UploadItem uploadItem) {
+ return uploadModel.getImageQuality(uploadItem);
+ }
+
+ /**
+ * gets nearby places matching with upload item's GPS location
+ *
+ * @param latitude
+ * @param longitude
+ * @return
+ */
+ public Place getNearbyPlaces(double latitude, double longitude) {
+ try {
+ List fromWikidataQuery = nearbyPlaces.getFromWikidataQuery(new LatLng(latitude, longitude, 0.0f),
+ Locale.getDefault().getLanguage(),
+ NEARBY_RADIUS_IN_KILO_METERS);
+ return fromWikidataQuery.size() > 0 ? fromWikidataQuery.get(0) : null;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ /**
+ * handles category selection/unselection
+ * @param depictedItem
+ */
+
+ public void onDepictedItemClicked(DepictedItem depictedItem) {
+ uploadModel.onDepictItemClicked(depictedItem);
+ }
+
+ /**
+ * returns the list of selected depictions
+ * @return
+ */
+
+ public List getSelectedDepictions() {
+ return uploadModel.getSelectedDepictions();
+ }
+
+ /**
+ * get all depictions
+ */
+
+ public Observable searchAllEntities(String query) {
+ return depictModel.searchAllEntities(query);
+ }
+
+ public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) {
+ uploadModel.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java
index 1e7090b189a..71d5ba50699 100644
--- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java
+++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java
@@ -1,27 +1,22 @@
package fr.free.nrw.commons.repository;
-import fr.free.nrw.commons.category.CategoriesModel;
+import fr.free.nrw.commons.upload.ImageCoordinates;
+import java.util.Comparator;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
import fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.filepicker.UploadableFile;
-import fr.free.nrw.commons.location.LatLng;
-import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.nearby.Place;
-import fr.free.nrw.commons.upload.ImageCoordinates;
import fr.free.nrw.commons.upload.SimilarImageInterface;
-import fr.free.nrw.commons.upload.UploadController;
-import fr.free.nrw.commons.upload.UploadItem;
-import fr.free.nrw.commons.upload.UploadModel;
-import fr.free.nrw.commons.upload.structure.depictions.DepictModel;
+import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
-import io.reactivex.Flowable;
+
import io.reactivex.Observable;
import io.reactivex.Single;
-import java.io.IOException;
-import java.util.List;
-import java.util.Locale;
-import javax.inject.Inject;
-import javax.inject.Singleton;
/**
* The repository class for UploadActivity
@@ -29,25 +24,14 @@
@Singleton
public class UploadRepository {
- private final UploadModel uploadModel;
- private final UploadController uploadController;
- private final CategoriesModel categoriesModel;
- private final NearbyPlaces nearbyPlaces;
- private final DepictModel depictModel;
-
- private static final double NEARBY_RADIUS_IN_KILO_METERS = 0.1; //100 meters
+ private UploadLocalDataSource localDataSource;
+ private UploadRemoteDataSource remoteDataSource;
@Inject
- public UploadRepository(UploadModel uploadModel,
- UploadController uploadController,
- CategoriesModel categoriesModel,
- NearbyPlaces nearbyPlaces,
- DepictModel depictModel) {
- this.uploadModel = uploadModel;
- this.uploadController = uploadController;
- this.categoriesModel = categoriesModel;
- this.nearbyPlaces = nearbyPlaces;
- this.depictModel = depictModel;
+ public UploadRepository(UploadLocalDataSource localDataSource,
+ UploadRemoteDataSource remoteDataSource) {
+ this.localDataSource = localDataSource;
+ this.remoteDataSource = remoteDataSource;
}
/**
@@ -56,7 +40,7 @@ public UploadRepository(UploadModel uploadModel,
* @return
*/
public Observable buildContributions() {
- return uploadModel.buildContributions();
+ return remoteDataSource.buildContributions();
}
/**
@@ -65,7 +49,7 @@ public Observable buildContributions() {
* @param contribution
*/
public void startUpload(Contribution contribution) {
- uploadController.startUpload(contribution);
+ remoteDataSource.startUpload(contribution);
}
/**
@@ -74,24 +58,22 @@ public void startUpload(Contribution contribution) {
* @return
*/
public List getUploads() {
- return uploadModel.getUploads();
+ return remoteDataSource.getUploads();
}
/**
* asks the RemoteDataSource to prepare the Upload Service
*/
public void prepareService() {
- uploadController.prepareService();
+ remoteDataSource.prepareService();
}
/**
*Prepare for a fresh upload
*/
public void cleanup() {
- uploadModel.cleanUp();
- //This needs further refactoring, this should not be here, right now the structure wont suppoort rhis
- categoriesModel.cleanUp();
- depictModel.cleanUp();
+ localDataSource.cleanUp();
+ remoteDataSource.cleanUp();
}
/**
@@ -100,7 +82,7 @@ public void cleanup() {
* @return
*/
public List getSelectedCategories() {
- return categoriesModel.getSelectedCategories();
+ return remoteDataSource.getSelectedCategories();
}
/**
@@ -108,12 +90,20 @@ public List getSelectedCategories() {
*
* @param query
* @param imageTitleList
- * @param selectedDepictions
* @return
*/
- public Observable> searchAll(String query, List imageTitleList,
- List selectedDepictions) {
- return categoriesModel.searchAll(query, imageTitleList, selectedDepictions);
+ public Observable searchAll(String query, List imageTitleList) {
+ return remoteDataSource.searchAll(query, imageTitleList);
+ }
+
+ /**
+ * returns the string list of categories
+ *
+ * @return
+ */
+
+ public List getCategoryStringList() {
+ return remoteDataSource.getCategoryStringList();
}
/**
@@ -122,7 +112,7 @@ public Observable> searchAll(String query, List image
* @param categoryStringList
*/
public void setSelectedCategories(List categoryStringList) {
- uploadModel.setSelectedCategories(categoryStringList);
+ remoteDataSource.setSelectedCategories(categoryStringList);
}
/**
@@ -131,7 +121,17 @@ public void setSelectedCategories(List categoryStringList) {
* @param categoryItem
*/
public void onCategoryClicked(CategoryItem categoryItem) {
- categoriesModel.onCategoryItemClicked(categoryItem);
+ remoteDataSource.onCategoryClicked(categoryItem);
+ }
+
+ /**
+ * returns category sorted based on similarity with query
+ *
+ * @param query
+ * @return
+ */
+ public Comparator super CategoryItem> sortBySimilarity(String query) {
+ return remoteDataSource.sortBySimilarity(query);
}
/**
@@ -141,7 +141,7 @@ public void onCategoryClicked(CategoryItem categoryItem) {
* @return
*/
public boolean containsYear(String name) {
- return categoriesModel.containsYear(name);
+ return remoteDataSource.containsYear(name);
}
/**
@@ -150,7 +150,7 @@ public boolean containsYear(String name) {
* @return
*/
public List getLicenses() {
- return uploadModel.getLicenses();
+ return localDataSource.getLicenses();
}
/**
@@ -159,7 +159,7 @@ public List getLicenses() {
* @return
*/
public String getSelectedLicense() {
- return uploadModel.getSelectedLicense();
+ return localDataSource.getSelectedLicense();
}
/**
@@ -168,7 +168,7 @@ public String getSelectedLicense() {
* @return
*/
public int getCount() {
- return uploadModel.getCount();
+ return localDataSource.getCount();
}
/**
@@ -181,8 +181,7 @@ public int getCount() {
*/
public Observable preProcessImage(UploadableFile uploadableFile, Place place,
SimilarImageInterface similarImageInterface) {
- return uploadModel.preProcessImage(uploadableFile, place,
- similarImageInterface);
+ return remoteDataSource.preProcessImage(uploadableFile, place, similarImageInterface);
}
/**
@@ -192,7 +191,17 @@ public Observable preProcessImage(UploadableFile uploadableFile, Pla
* @return
*/
public Single getImageQuality(UploadItem uploadItem) {
- return uploadModel.getImageQuality(uploadItem);
+ return remoteDataSource.getImageQuality(uploadItem);
+ }
+
+ /**
+ * asks the LocalDataSource to update the Upload Item
+ *
+ * @param index
+ * @param uploadItem
+ */
+ public void updateUploadItem(int index, UploadItem uploadItem) {
+ localDataSource.updateUploadItem(index, uploadItem);
}
/**
@@ -201,7 +210,7 @@ public Single getImageQuality(UploadItem uploadItem) {
* @param filePath
*/
public void deletePicture(String filePath) {
- uploadModel.deletePicture(filePath);
+ localDataSource.deletePicture(filePath);
}
/**
@@ -211,10 +220,38 @@ public void deletePicture(String filePath) {
* @return
*/
public UploadItem getPreviousUploadItem(int index) {
- if (index - 1 >= 0) {
- return uploadModel.getItems().get(index - 1);
- }
- return null; //There is no previous item to copy details
+ return localDataSource.getPreviousUploadItem(index);
+ }
+
+ /**
+ * Save boolean value locally
+ *
+ * @param key
+ * @param value
+ */
+ public void saveValue(String key, boolean value) {
+ localDataSource.saveValue(key, value);
+ }
+
+ /**
+ * save string value locally
+ *
+ * @param key
+ * @param value
+ */
+ public void saveValue(String key, String value) {
+ localDataSource.saveValue(key, value);
+ }
+
+ /**
+ * fetch the string value for the associated key
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ public String getValue(String key, String value) {
+ return localDataSource.getValue(key, value);
}
/**
@@ -223,11 +260,11 @@ public UploadItem getPreviousUploadItem(int index) {
* @param licenseName
*/
public void setSelectedLicense(String licenseName) {
- uploadModel.setSelectedLicense(licenseName);
+ localDataSource.setSelectedLicense(licenseName);
}
public void onDepictItemClicked(DepictedItem depictedItem) {
- uploadModel.onDepictItemClicked(depictedItem);
+ remoteDataSource.onDepictedItemClicked(depictedItem);
}
/**
@@ -237,7 +274,7 @@ public void onDepictItemClicked(DepictedItem depictedItem) {
*/
public List getSelectedDepictions() {
- return uploadModel.getSelectedDepictions();
+ return remoteDataSource.getSelectedDepictions();
}
/**
@@ -247,8 +284,8 @@ public List getSelectedDepictions() {
* @return
*/
- public Flowable> searchAllEntities(String query) {
- return depictModel.searchAllEntities(query);
+ public Observable searchAllEntities(String query) {
+ return remoteDataSource.searchAllEntities(query);
}
/**
@@ -258,18 +295,10 @@ public Flowable> searchAllEntities(String query) {
* @return
*/
public Place checkNearbyPlaces(double decLatitude, double decLongitude) {
- try {
- List fromWikidataQuery = nearbyPlaces.getFromWikidataQuery(new LatLng(
- decLatitude, decLongitude, 0.0f),
- Locale.getDefault().getLanguage(),
- NEARBY_RADIUS_IN_KILO_METERS);
- return fromWikidataQuery.size() > 0 ? fromWikidataQuery.get(0) : null;
- } catch (IOException e) {
- return null;
- }
+ return remoteDataSource.getNearbyPlaces(decLatitude, decLongitude);
}
public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) {
- uploadModel.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex);
+ remoteDataSource.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex);
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt
index ff3f63eb86a..14ff8b12600 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt
@@ -180,7 +180,7 @@ class FileProcessor @Inject constructor(
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(
- gpsCategoryModel::setCategoriesFromLocation,
+ { gpsCategoryModel.categoryList = it },
{
Timber.e(it)
gpsCategoryModel.clear()
@@ -209,7 +209,7 @@ class FileProcessor @Inject constructor(
.filter { it.size >= MIN_NEARBY_RESULTS }
.take(1)
.subscribe(
- { depictsModel.nearbyPlaces.offer(it) },
+ { depictsModel.nearbyPlaces = it },
{ Timber.e(it) }
)
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GpsCategoryModel.java b/app/src/main/java/fr/free/nrw/commons/upload/GpsCategoryModel.java
new file mode 100644
index 00000000000..78eece3b33b
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/GpsCategoryModel.java
@@ -0,0 +1,36 @@
+package fr.free.nrw.commons.upload;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+@Singleton
+public class GpsCategoryModel {
+ private Set categorySet;
+
+ @Inject
+ public GpsCategoryModel() {
+ clear();
+ }
+
+ public void clear() {
+ categorySet = new HashSet<>();
+ }
+
+ public boolean getGpsCatExists() {
+ return !categorySet.isEmpty();
+ }
+
+ public List getCategoryList() {
+ return new ArrayList<>(categorySet);
+ }
+
+ public void setCategoryList(List categoryList) {
+ clear();
+ categorySet.addAll(categoryList != null ? categoryList : new ArrayList<>());
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java
index e04afe9792f..7bcec4fb0d6 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java
@@ -45,7 +45,7 @@ public ImageProcessingService(FileUtilsWrapper fileUtilsWrapper,
* Check image quality before upload - checks duplicate image - checks dark image - checks
* geolocation for image - check for valid title
*/
- Single validateImage(UploadItem uploadItem) {
+ Single validateImage(UploadModel.UploadItem uploadItem) {
int currentImageQuality = uploadItem.getImageQuality();
Timber.d("Current image quality is %d", currentImageQuality);
if (currentImageQuality == ImageUtils.IMAGE_KEEP) {
@@ -97,7 +97,7 @@ private Single checkEXIF(String filepath) {
* @param uploadItem
* @return
*/
- private Single validateItemTitle(UploadItem uploadItem) {
+ private Single validateItemTitle(UploadModel.UploadItem uploadItem) {
Timber.d("Checking for image title %s", uploadItem.getUploadMediaDetails());
List