#3780 Create media using a combination of Entities & MwQueryResult (#3786)

* #3468 Switch from RvRenderer to AdapterDelegates - replace SearchDepictionsRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace UploadCategoryDepictionsRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - update BaseAdapter to be easier to use

* #3468 Switch from RvRenderer to AdapterDelegates - replace SearchImagesRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace SearchCategoriesRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace NotificationRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace UploadDepictsRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace PlaceRenderer

* #3756 Convert SearchDepictionsFragment to use Pagination - convert SearchDepictionsFragment

* #3756 Convert SearchDepictionsFragment to use Pagination - fix presenter unit tests now that view is not nullable - fix Category prefix imports

* #3756 Convert SearchDepictionsFragment to use Pagination - test DataSource related classes

* #3756 Convert SearchDepictionsFragment to use Pagination - reset rx scheduler - ignore failing test

* #3760 Convert SearchCategoriesFragment to use Pagination - extract functionality of pagination to base classes - add category pagination

* #3772 Convert SearchImagesFragment to use Pagination  - convert SearchImagesFragment - tidy up showing the empty view - make search fragments show snackbar with appropriate text

* #3772 Convert SearchImagesFragment to use Pagination  - allow viewpager to load more data

* #3760 remove test that got re-added by merge

* #3760 remove duplicate dependency

* #3772 fix compilation

* #3780 Create media using a combination of Entities & MwQueryResult - construct media with an entity - move fields from media down to contribution - move dynamic fields outside of media - remove unused constructors - remove all unnecessary fetching of captions/descriptions - bump database version

* #3808 Construct media objects that depict an item id correctly - use generator to construct media for DepictedImages

* #3780 Create media using a combination of Entities & MwQueryResult - update wikicode to align with expected behaviour

* #3780 Create media using a combination of Entities & MwQueryResult - replace old site of thumbnail title with most relevant caption
This commit is contained in:
Seán Mac Gillicuddy 2020-06-25 08:20:01 +01:00 committed by GitHub
parent bf4b7e2efc
commit 4b22583b60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 803 additions and 1532 deletions

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -8,44 +7,25 @@ import androidx.annotation.Nullable;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.media.Depictions; import java.io.Serializable;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.gallery.ExtMetadata;
import org.wikipedia.gallery.ImageInfo;
import org.wikipedia.page.PageTitle; import org.wikipedia.page.PageTitle;
@Entity @Entity
public class Media implements Parcelable { public class Media implements Parcelable {
public static final Media EMPTY = new Media("");
// Primary metadata fields
@Nullable
private Uri localUri;
private String thumbUrl; private String thumbUrl;
private String imageUrl; private String imageUrl;
private String filename; private String filename;
private String thumbnailTitle; private String fallbackDescription; // monolingual description on input...
/*
* 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
*/
private String caption;
private String description; // monolingual description on input...
private String discussion;
private long dataLength;
private Date dateCreated;
@Nullable private Date dateUploaded; @Nullable private Date dateUploaded;
private String license; private String license;
private String licenseUrl; private String licenseUrl;
@ -57,13 +37,14 @@ public class Media implements Parcelable {
@NonNull @NonNull
private String pageId; private String pageId;
private List<String> categories; // as loaded at runtime? private List<String> 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; @Nullable private LatLng coordinates;
@NotNull
private Map<String, String> captions = Collections.emptyMap();
@NotNull
private Map<String, String> descriptions = Collections.emptyMap();
@NotNull
private List<String> depictionIds = Collections.emptyList();
/** /**
* Provides local constructor * Provides local constructor
@ -72,6 +53,82 @@ public class Media implements Parcelable {
pageId = UUID.randomUUID().toString(); pageId = UUID.randomUUID().toString();
} }
/**
* Constructor with all parameters
*/
public Media(final String thumbUrl,
final String imageUrl,
final String filename,
final String fallbackDescription,
@Nullable final Date dateUploaded,
final String license,
final String licenseUrl,
final String creator,
@NonNull final String pageId,
final List<String> categories,
@Nullable final LatLng coordinates,
@NotNull final Map<String, String> captions,
@NotNull final Map<String, String> descriptions,
@NotNull final List<String> depictionIds) {
this.thumbUrl = thumbUrl;
this.imageUrl = imageUrl;
this.filename = filename;
this.fallbackDescription = fallbackDescription;
this.dateUploaded = dateUploaded;
this.license = license;
this.licenseUrl = licenseUrl;
this.creator = creator;
this.pageId = pageId;
this.categories = categories;
this.coordinates = coordinates;
this.captions = captions;
this.descriptions = descriptions;
this.depictionIds = depictionIds;
}
public Media(Media media) {
this(media.getThumbUrl(), media.getImageUrl(), media.getFilename(),
media.getFallbackDescription(), media.getDateUploaded(), media.getLicense(),
media.getLicenseUrl(), media.getCreator(), media.getPageId(), media.getCategories(),
media.getCoordinates(), media.getCaptions(), media.getDescriptions(),
media.getDepictionIds());
}
public Media(final String filename,
Map<String, String> captions, final String fallbackDescription,
final String creator, final List<String> categories) {
this();
thumbUrl = null;
this.imageUrl = null;
this.filename = filename;
this.fallbackDescription = fallbackDescription;
this.dateUploaded = new Date();
this.creator = creator;
this.categories = categories;
this.captions=captions;
}
protected Media(final Parcel in) {
this(in.readString(), in.readString(), in.readString(),
in.readString(), readDateUploaded(in), in.readString(),
in.readString(), in.readString(), in.readString(), readList(in),
in.readParcelable(LatLng.class.getClassLoader()),
((Map<String, String>) in.readSerializable()),
((Map<String, String>) in.readSerializable()),
readList(in));
}
private static List<String> readList(Parcel in) {
final List<String> list = new ArrayList<>();
in.readStringList(list);
return list;
}
private static Date readDateUploaded(Parcel in) {
final long tmpDateUploaded = in.readLong();
return tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded);
}
public static final Creator<Media> CREATOR = new Creator<Media>() { public static final Creator<Media> CREATOR = new Creator<Media>() {
@Override @Override
public Media createFromParcel(final Parcel source) { public Media createFromParcel(final Parcel source) {
@ -84,192 +141,6 @@ public class Media implements Parcelable {
} }
}; };
/**
* Provides a minimal constructor
*
* @param filename Media filename
*/
public Media(final String filename) {
this();
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(final Uri localUri, final String imageUrl, final String filename,
final String description,
final long dataLength, final Date dateCreated, final Date dateUploaded,
final String creator) {
this();
this.localUri = localUri;
thumbUrl = imageUrl;
this.imageUrl = imageUrl;
this.filename = filename;
this.description = description;
this.dataLength = dataLength;
this.dateCreated = dateCreated;
this.dateUploaded = dateUploaded;
this.creator = creator;
}
/**
* Constructor with all parameters
*/
public Media(final String pageId,
final Uri localUri,
final String thumbUrl,
final String imageUrl,
final String filename,
final String description,
final String discussion,
final long dataLength,
final Date dateCreated,
final Date dateUploaded,
final String license,
final String licenseUrl,
final String creator,
final List<String> categories,
final boolean requestedDeletion,
final LatLng coordinates) {
this.pageId = pageId;
this.localUri = localUri;
this.thumbUrl = thumbUrl;
this.imageUrl = imageUrl;
this.filename = filename;
this.description = description;
this.discussion = discussion;
this.dataLength = dataLength;
this.dateCreated = dateCreated;
this.dateUploaded = dateUploaded;
this.license = license;
this.licenseUrl = licenseUrl;
this.creator = creator;
this.categories = categories;
this.requestedDeletion = requestedDeletion;
this.coordinates = coordinates;
}
public Media(final Uri localUri, final String filename,
final String description, final String creator, final List<String> categories) {
this(localUri,null, filename,
description, -1, null, new Date(), creator);
this.categories = categories;
}
public Media(final String title, final Date date, final String user) {
this(null, null, title, "", -1, date, date, user);
}
protected Media(final Parcel in) {
localUri = in.readParcelable(Uri.class.getClassLoader());
thumbUrl = in.readString();
imageUrl = in.readString();
filename = in.readString();
thumbnailTitle = in.readString();
caption = in.readString();
description = in.readString();
discussion = in.readString();
dataLength = in.readLong();
final long tmpDateCreated = in.readLong();
dateCreated = tmpDateCreated == -1 ? null : new Date(tmpDateCreated);
final long tmpDateUploaded = in.readLong();
dateUploaded = tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded);
license = in.readString();
licenseUrl = in.readString();
creator = in.readString();
pageId = in.readString();
final ArrayList<String> list = new ArrayList<>();
in.readStringList(list);
categories = list;
in.readParcelable(Depictions.class.getClassLoader());
requestedDeletion = in.readByte() != 0;
coordinates = in.readParcelable(LatLng.class.getClassLoader());
}
/**
* Creating Media object from MWQueryPage.
* Earlier only basic details were set for the media object but going forward,
* a full media object(with categories, descriptions, coordinates etc) can be constructed using this method
*
* @param page response from the API
* @return Media object
*/
@NonNull
public static Media from(final MwQueryPage page) {
final ImageInfo imageInfo = page.imageInfo();
if (imageInfo == null) {
return new Media(); // null is not allowed
}
final ExtMetadata metadata = imageInfo.getMetadata();
if (metadata == null) {
final Media media = new Media(null, imageInfo.getOriginalUrl(),
page.title(), "", 0, null, null, null);
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
media.setThumbUrl(imageInfo.getThumbUrl());
}
return media;
}
final 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()));
final String latitude = metadata.getGpsLatitude();
final String longitude = metadata.getGpsLongitude();
if (!StringUtils.isBlank(latitude) && !StringUtils.isBlank(longitude)) {
final 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(final ExtMetadata metadata) {
try {
final String artistHtml = metadata.artist();
return artistHtml.substring(artistHtml.indexOf("title=\""), artistHtml.indexOf("\">"))
.replace("title=\"User:", "");
} catch (final Exception ex) {
return "";
}
}
@Nullable @Nullable
public String getThumbUrl() { public String getThumbUrl() {
return thumbUrl; return thumbUrl;
@ -283,23 +154,6 @@ public class Media implements Parcelable {
return filename != null ? getPageTitle().getDisplayTextWithoutNamespace().replaceFirst("[.][^.]+$", "") : ""; return filename != null ? getPageTitle().getDisplayTextWithoutNamespace().replaceFirst("[.][^.]+$", "") : "";
} }
@Nullable
private static Date safeParseDate(final String dateStr) {
try {
return CommonsDateUtil.getMediaSimpleDateFormat().parse(dateStr);
} catch (final ParseException e) {
return null;
}
}
/**
* @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 * Gets file page title
* @return New media page title * @return New media page title
@ -308,13 +162,6 @@ public class Media implements Parcelable {
return Utils.getPageTitle(getFilename()); return Utils.getPageTitle(getFilename());
} }
/**
* Gets local URI
* @return Media local URI
*/
public Uri getLocalUri() {
return localUri;
}
/** /**
* Gets image URL * Gets image URL
@ -348,38 +195,12 @@ public class Media implements Parcelable {
this.pageId = pageId; this.pageId = pageId;
} }
/**
* Gets the file discussion as a string.
* @return file discussion as a string
*/
public String getDiscussion() {
return discussion;
}
/** /**
* Gets the file description. * Gets the file description.
* @return file description as a string * @return file description as a string
*/ */
public String getDescription() { public String getFallbackDescription() {
return description; return fallbackDescription;
}
/**
* 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;
} }
/** /**
@ -390,36 +211,12 @@ public class Media implements Parcelable {
this.filename = filename; this.filename = filename;
} }
/**
* Gets the dataLength of the file.
* @return file dataLength as a long
*/
public long getDataLength() {
return dataLength;
}
/**
* Sets the discussion of the file.
* @param discussion
*/
public void setDiscussion(final String discussion) {
this.discussion = discussion;
}
/**
* Gets the creation date of the file.
* @return creation date as a Date
*/
public Date getDateCreated() {
return dateCreated;
}
/** /**
* Sets the file description. * Sets the file description.
* @param description the new description of the file * @param fallbackDescription the new description of the file
*/ */
public void setDescription(final String description) { public void setFallbackDescription(final String fallbackDescription) {
this.description = description; this.fallbackDescription = fallbackDescription;
} }
/** /**
@ -440,14 +237,6 @@ public class Media implements Parcelable {
return creator; return creator;
} }
/**
* Sets the dataLength of the file.
* @param dataLength as a long
*/
public void setDataLength(final long dataLength) {
this.dataLength = dataLength;
}
/** /**
* Gets the license name of the file. * Gets the license name of the file.
* @return license as a String * @return license as a String
@ -456,13 +245,6 @@ public class Media implements Parcelable {
return license; return license;
} }
/**
* Set Caption(if available) as the thumbnail title of the image
*/
public void setThumbnailTitle(final String title) {
thumbnailTitle = title;
}
public String getLicenseUrl() { public String getLicenseUrl() {
return licenseUrl; return licenseUrl;
} }
@ -492,24 +274,10 @@ public class Media implements Parcelable {
* Gets the categories the file falls under. * Gets the categories the file falls under.
* @return file categories as an ArrayList of Strings * @return file categories as an ArrayList of Strings
*/ */
@SuppressWarnings("unchecked")
public List<String> getCategories() { public List<String> getCategories() {
return categories; return categories;
} }
/**
* Sets the license name of the file.
* @param license license name as a String
*/
public void setLicenseInformation(final String license, String licenseUrl) {
this.license = license;
if (!licenseUrl.startsWith("http://") && !licenseUrl.startsWith("https://")) {
licenseUrl = "https://" + licenseUrl;
}
this.licenseUrl = licenseUrl;
}
/** /**
* Sets the coordinates of where the file was created. * Sets the coordinates of where the file was created.
* @param coordinates file coordinates as a LatLng * @param coordinates file coordinates as a LatLng
@ -523,7 +291,18 @@ public class Media implements Parcelable {
* @return * @return
*/ */
public String getWikiCode() { public String getWikiCode() {
return String.format("[[%s|thumb|%s]]", filename, thumbnailTitle); return String.format("[[%s|thumb|%s]]", filename, getMostRelevantCaption());
}
public String getMostRelevantCaption() {
final String languageAppropriateCaption = captions.get(Locale.getDefault().getLanguage());
if (languageAppropriateCaption != null) {
return languageAppropriateCaption;
}
for (String firstCaption : captions.values()) {
return firstCaption;
}
return getDisplayTitle();
} }
/** /**
@ -537,21 +316,6 @@ public class Media implements Parcelable {
this.categories = categories; this.categories = categories;
} }
/**
* Get the value of requested deletion
* @return boolean requestedDeletion
*/
public boolean isRequestedDeletion(){
return requestedDeletion;
}
/**
* Set requested deletion to true
* @param requestedDeletion
*/
public void setRequestedDeletion(final boolean requestedDeletion) {
this.requestedDeletion = requestedDeletion;
}
/** /**
* Sets the license name of the file. * Sets the license name of the file.
@ -562,22 +326,6 @@ public class Media implements Parcelable {
this.license = 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(final String caption) {
this.caption = caption;
}
public void setLocalUri(@Nullable final Uri localUri) {
this.localUri = localUri;
}
public void setImageUrl(final String imageUrl) { public void setImageUrl(final String imageUrl) {
this.imageUrl = imageUrl; this.imageUrl = imageUrl;
} }
@ -590,28 +338,11 @@ public class Media implements Parcelable {
this.licenseUrl = licenseUrl; this.licenseUrl = licenseUrl;
} }
public Depictions getDepictions() {
return depictions;
}
@Override @Override
public int describeContents() { public int describeContents() {
return 0; return 0;
} }
/* Sets depictions for the current media obtained fro Wikibase API*/
public void setDepictions(final Depictions depictions) {
this.depictions = depictions;
}
/**
* Sets the creation date of the file.
* @param date creation date as a Date
*/
public void setDateCreated(final Date date) {
dateCreated = date;
}
/** /**
* Creates a way to transfer information between two or more * Creates a way to transfer information between two or more
* activities. * activities.
@ -620,69 +351,76 @@ public class Media implements Parcelable {
*/ */
@Override @Override
public void writeToParcel(final Parcel dest, final int flags) { public void writeToParcel(final Parcel dest, final int flags) {
dest.writeParcelable(localUri, flags);
dest.writeString(thumbUrl); dest.writeString(thumbUrl);
dest.writeString(imageUrl); dest.writeString(imageUrl);
dest.writeString(filename); dest.writeString(filename);
dest.writeString(thumbnailTitle); dest.writeString(fallbackDescription);
dest.writeString(caption);
dest.writeString(description);
dest.writeString(discussion);
dest.writeLong(dataLength);
dest.writeLong(dateCreated != null ? dateCreated.getTime() : -1);
dest.writeLong(dateUploaded != null ? dateUploaded.getTime() : -1); dest.writeLong(dateUploaded != null ? dateUploaded.getTime() : -1);
dest.writeString(license); dest.writeString(license);
dest.writeString(licenseUrl); dest.writeString(licenseUrl);
dest.writeString(creator); dest.writeString(creator);
dest.writeString(pageId); dest.writeString(pageId);
dest.writeStringList(categories); dest.writeStringList(categories);
dest.writeParcelable(depictions, flags);
dest.writeByte(requestedDeletion ? (byte) 1 : (byte) 0);
dest.writeParcelable(coordinates, flags); dest.writeParcelable(coordinates, flags);
dest.writeSerializable((Serializable) captions);
dest.writeSerializable((Serializable) descriptions);
dest.writeList(depictionIds);
}
public Map<String, String> getCaptions() {
return captions;
}
public void setCaptions(Map<String, String> captions) {
this.captions = captions;
}
public Map<String, String> getDescriptions() {
return descriptions;
}
public void setDescriptions(Map<String, String> descriptions) {
this.descriptions = descriptions;
}
public List<String> getDepictionIds() {
return depictionIds;
}
public void setDepictionIds(final List<String> depictionIds) {
this.depictionIds = depictionIds;
} }
/**
* Equals implementation that matches all parameters for equality check
*/
@Override @Override
public boolean equals(final Object o) { public boolean equals(final Object o) {
if (this == o) { if (this == o) {
return true; return true;
} }
if (!(o instanceof Media)) { if (o == null || getClass() != o.getClass()) {
return false; return false;
} }
final Media media = (Media) o; final Media media = (Media) o;
return getDataLength() == media.getDataLength() && return Objects.equals(thumbUrl, media.thumbUrl) &&
isRequestedDeletion() == media.isRequestedDeletion() && Objects.equals(imageUrl, media.imageUrl) &&
Objects.equals(getLocalUri(), media.getLocalUri()) && Objects.equals(filename, media.filename) &&
Objects.equals(getThumbUrl(), media.getThumbUrl()) && Objects.equals(fallbackDescription, media.fallbackDescription) &&
Objects.equals(getImageUrl(), media.getImageUrl()) && Objects.equals(dateUploaded, media.dateUploaded) &&
Objects.equals(getFilename(), media.getFilename()) && Objects.equals(license, media.license) &&
Objects.equals(getThumbnailTitle(), media.getThumbnailTitle()) && Objects.equals(licenseUrl, media.licenseUrl) &&
Objects.equals(getCaption(), media.getCaption()) && Objects.equals(creator, media.creator) &&
Objects.equals(getDescription(), media.getDescription()) && pageId.equals(media.pageId) &&
Objects.equals(getDiscussion(), media.getDiscussion()) && Objects.equals(categories, media.categories) &&
Objects.equals(getDateCreated(), media.getDateCreated()) && Objects.equals(coordinates, media.coordinates) &&
Objects.equals(getDateUploaded(), media.getDateUploaded()) && captions.equals(media.captions) &&
Objects.equals(getLicense(), media.getLicense()) && descriptions.equals(media.descriptions) &&
Objects.equals(getLicenseUrl(), media.getLicenseUrl()) && depictionIds.equals(media.depictionIds);
Objects.equals(getCreator(), media.getCreator()) &&
getPageId().equals(media.getPageId()) &&
Objects.equals(getCategories(), media.getCategories()) &&
Objects.equals(getDepictions(), media.getDepictions()) &&
Objects.equals(getCoordinates(), media.getCoordinates());
} }
/**
* Hashcode implementation that uses all parameters for calculating hash
*/
@Override @Override
public int hashCode() { public int hashCode() {
return Objects return Objects
.hash(getLocalUri(), getThumbUrl(), getImageUrl(), getFilename(), getThumbnailTitle(), .hash(thumbUrl, imageUrl, filename, fallbackDescription, dateUploaded, license,
getCaption(), getDescription(), getDiscussion(), getDataLength(), getDateCreated(), licenseUrl,
getDateUploaded(), getLicense(), getLicenseUrl(), getCreator(), getPageId(), creator, pageId, categories, coordinates, captions, descriptions, depictionIds);
getCategories(), getDepictions(), isRequestedDeletion(), getCoordinates());
} }
} }

View file

@ -1,11 +1,10 @@
package fr.free.nrw.commons package fr.free.nrw.commons
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX
import fr.free.nrw.commons.media.Depictions import fr.free.nrw.commons.media.IdAndCaptions
import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.functions.Function5
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -13,107 +12,40 @@ import javax.inject.Singleton
/** /**
* Fetch additional media data from the network that we don't store locally. * 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. * This includes things like category lists and multilingual descriptions, which are not intrinsic
* to the media and may change due to editing.
*/ */
@Singleton @Singleton
class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClient) { class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClient) {
/** fun fetchDepictionIdsAndLabels(media: Media) =
* Simplified method to extract all details required to show media details. mediaClient.getEntities(media.depictionIds)
* It fetches media object, deletion status, talk page and captions for the filename .map {
* @param filename for which the details are to be fetched it.entities()
* @return full Media object with all details including deletion status and talk page .mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
*/
fun fetchMediaDetails(filename: String, pageId: String?): Single<Media> {
return Single.zip(
getMediaFromFileName(filename),
mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/$filename"),
getDiscussion(filename),
if (pageId != null)
getCaption(DepictedImagesFragment.PAGE_ID_PREFIX + pageId)
else Single.just(MediaClient.NO_CAPTION),
getDepictions(filename),
Function5 { media: Media, deletionStatus: Boolean, discussion: String, caption: String, depictions: Depictions ->
combineToMedia(
media,
deletionStatus,
discussion,
caption,
depictions
)
} }
) .map { it.map { (key, value) -> IdAndCaptions(key, value) } }
} .onErrorReturn { emptyList() }
private fun combineToMedia( fun checkDeletionRequestExists(media: Media) =
media: Media, mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
deletionStatus: Boolean,
discussion: String,
caption: String,
depictions: Depictions
): Media {
media.discussion = discussion
media.caption = caption
media.depictions = depictions
if (deletionStatus) {
media.isRequestedDeletion = true
}
return media
}
/** fun fetchDiscussion(media: Media) =
* Obtains captions using filename mediaClient.getPageHtml(media.filename.replace("File", "File talk"))
* @param wikibaseIdentifier .map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() }
* .onErrorReturn {
* @return caption for the image in user's locale Timber.d("Error occurred while fetching discussion")
* Ex: "a nice painting" (english locale) and "No Caption" in case the caption is not available for the image
*/
private fun getCaption(wikibaseIdentifier: String): Single<String> {
return mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier)
}
/**
* Fetch depictions from the MediaWiki API
* @param filename the filename we will return the caption for
* @return Depictions
*/
private fun getDepictions(filename: String): Single<Depictions> {
return mediaClient.getDepictions(filename)
.doOnError { throwable: 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
*/
fun getMediaFromFileName(filename: String?): Single<Media> {
return mediaClient.getMedia(filename)
}
/**
* Fetch talk page from the MediaWiki API
* @param filename
* @return
*/
private fun getDiscussion(filename: String): Single<String> {
return mediaClient.getPageHtml(filename.replace("File", "File talk"))
.map { discussion: String? ->
HtmlCompat.fromHtml(
discussion!!,
HtmlCompat.FROM_HTML_MODE_LEGACY
).toString()
}
.onErrorReturn { throwable: Throwable? ->
Timber.e(throwable, "Error occurred while fetching discussion")
"" ""
} }
}
fun refresh(media: Media): Single<Media> {
return Single.ambArray(
mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId)
.onErrorResumeNext { Single.never() },
mediaClient.getMedia(media.filename)
.onErrorResumeNext { Single.never() }
)
}
} }

View file

@ -1,13 +1,5 @@
package fr.free.nrw.commons.bookmarks.pictures; package fr.free.nrw.commons.bookmarks.pictures;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.bookmarks.Bookmark; import fr.free.nrw.commons.bookmarks.Bookmark;
import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.media.MediaClient;
@ -15,6 +7,10 @@ import io.reactivex.Observable;
import io.reactivex.ObservableSource; import io.reactivex.ObservableSource;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.functions.Function; import io.reactivex.functions.Function;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton @Singleton
public class BookmarkPicturesController { public class BookmarkPicturesController {
@ -40,16 +36,13 @@ public class BookmarkPicturesController {
currentBookmarks = bookmarks; currentBookmarks = bookmarks;
return Observable.fromIterable(bookmarks) return Observable.fromIterable(bookmarks)
.flatMap((Function<Bookmark, ObservableSource<Media>>) this::getMediaFromBookmark) .flatMap((Function<Bookmark, ObservableSource<Media>>) this::getMediaFromBookmark)
.filter(media -> media != null && !StringUtils.isBlank(media.getFilename()))
.toList(); .toList();
} }
private Observable<Media> getMediaFromBookmark(Bookmark bookmark) { private Observable<Media> getMediaFromBookmark(Bookmark bookmark) {
Media dummyMedia = new Media("");
return mediaClient.getMedia(bookmark.getMediaName()) return mediaClient.getMedia(bookmark.getMediaName())
.map(media -> media == null ? dummyMedia : media) .toObservable()
.onErrorReturn(throwable -> dummyMedia) .onErrorResumeNext(Observable.empty());
.toObservable();
} }
/** /**

View file

@ -2,7 +2,6 @@ package fr.free.nrw.commons.category;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.os.Bundle; import android.os.Bundle;
@ -253,37 +252,6 @@ public class CategoryImagesListFragment extends DaggerFragment {
progressBar.setVisibility(GONE); progressBar.setVisibility(GONE);
isLoading = false; isLoading = false;
statusTextView.setVisibility(GONE); statusTextView.setVisibility(GONE);
for (Media m : collection) {
final String pageId = m.getPageId();
if (pageId != null) {
replaceTitlesWithCaptions(PAGE_ID_PREFIX + pageId, mediaSize++);
}
}
}
/**
* fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available)
* else show filename
*/
public void replaceTitlesWithCaptions(String wikibaseIdentifier, int i) {
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.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();
}
} }
/** /**

View file

@ -88,7 +88,7 @@ public class GridViewAdapter extends ArrayAdapter {
SimpleDraweeView imageView = convertView.findViewById(R.id.categoryImageView); SimpleDraweeView imageView = convertView.findViewById(R.id.categoryImageView);
TextView fileName = convertView.findViewById(R.id.categoryImageTitle); TextView fileName = convertView.findViewById(R.id.categoryImageTitle);
TextView author = convertView.findViewById(R.id.categoryImageAuthor); TextView author = convertView.findViewById(R.id.categoryImageAuthor);
fileName.setText(item.getThumbnailTitle()); fileName.setText(item.getMostRelevantCaption());
setAuthorView(item, author); setAuthorView(item, author);
imageView.setImageURI(item.getThumbUrl()); imageView.setImageURI(item.getThumbUrl());
return convertView; return convertView;

View file

@ -1,17 +1,18 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import androidx.annotation.Nullable;
import androidx.room.Entity; import androidx.room.Entity;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.UploadItem; import fr.free.nrw.commons.upload.UploadItem;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.WikidataPlace; import fr.free.nrw.commons.upload.WikidataPlace;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
@Entity(tableName = "contribution") @Entity(tableName = "contribution")
@ -34,24 +35,23 @@ public class Contribution extends Media {
*/ */
private List<DepictedItem> depictedItems = new ArrayList<>(); private List<DepictedItem> depictedItems = new ArrayList<>();
private String mimeType; private String mimeType;
/** @Nullable
* This hasmap stores the list of multilingual captions, where key of the HashMap is the language private Uri localUri;
* and value is the caption in the corresponding language Ex: key = "en", value: "<caption in private long dataLength;
* short in English>" key = "de" , value: "<caption in german>" private Date dateCreated;
*/
private HashMap<String, String> captions = new HashMap<>();
public Contribution() { public Contribution() {
} }
public Contribution(final UploadItem item, final SessionManager sessionManager, public Contribution(final UploadItem item, final SessionManager sessionManager,
final List<DepictedItem> depictedItems, final List<String> categories) { final List<DepictedItem> depictedItems, final List<String> categories) {
super(item.getMediaUri(), super(
item.getFileName(), item.getFileName(),
UploadMediaDetail.formatList(item.getUploadMediaDetails()), UploadMediaDetail.formatCaptions(item.getUploadMediaDetails()),
UploadMediaDetail.formatDescriptions(item.getUploadMediaDetails()),
sessionManager.getAuthorName(), sessionManager.getAuthorName(),
categories); categories);
captions = new HashMap<>(UploadMediaDetail.formatCaptions(item.getUploadMediaDetails())); localUri = item.getMediaUri();
decimalCoords = item.getGpsCoords().getDecimalCoords(); decimalCoords = item.getGpsCoords().getDecimalCoords();
dateCreatedSource = ""; dateCreatedSource = "";
this.depictedItems = depictedItems; this.depictedItems = depictedItems;
@ -117,24 +117,6 @@ public class Contribution extends Media {
this.mimeType = mimeType; this.mimeType = mimeType;
} }
/**
* Captions are a feature part of Structured data. They are meant to store short, multilingual
* descriptions about files This is a replacement of the previously used titles for images (titles
* were not multilingual) Also now captions replace the previous convention of using title for
* filename
* <p>
* key of the HashMap is the language and value is the caption in the corresponding language
* <p>
* returns list of captions stored in hashmap
*/
public HashMap<String, String> getCaptions() {
return captions;
}
public void setCaptions(HashMap<String, String> captions) {
this.captions = captions;
}
@Override @Override
public int describeContents() { public int describeContents() {
return 0; return 0;
@ -147,7 +129,6 @@ public class Contribution extends Media {
dest.writeLong(transferred); dest.writeLong(transferred);
dest.writeString(decimalCoords); dest.writeString(decimalCoords);
dest.writeString(dateCreatedSource); dest.writeString(dateCreatedSource);
dest.writeSerializable(captions);
} }
/** /**
@ -156,13 +137,7 @@ public class Contribution extends Media {
* @param state * @param state
*/ */
public Contribution(Media media, int state) { public Contribution(Media media, int state) {
super(media.getPageId(), super(media);
media.getLocalUri(), media.getThumbUrl(), media.getImageUrl(), media.getFilename(),
media.getDescription(),
media.getDiscussion(),
media.getDataLength(), media.getDateCreated(), media.getDateUploaded(),
media.getLicense(), media.getLicenseUrl(), media.getCreator(), media.getCategories(),
media.isRequestedDeletion(), media.getCoordinates());
this.state = state; this.state = state;
} }
@ -172,7 +147,6 @@ public class Contribution extends Media {
transferred = in.readLong(); transferred = in.readLong();
decimalCoords = in.readString(); decimalCoords = in.readString();
dateCreatedSource = in.readString(); dateCreatedSource = in.readString();
captions = (HashMap<String, String>) in.readSerializable();
} }
public static final Creator<Contribution> CREATOR = new Creator<Contribution>() { public static final Creator<Contribution> CREATOR = new Creator<Contribution>() {
@ -187,34 +161,60 @@ public class Contribution extends Media {
} }
}; };
/** @Nullable
* Equals implementation of Contributions that compares all parameters for checking equality public Uri getLocalUri() {
*/ return localUri;
}
public void setLocalUri(@Nullable Uri localUri) {
this.localUri = localUri;
}
public long getDataLength() {
return dataLength;
}
public void setDataLength(long dataLength) {
this.dataLength = dataLength;
}
public Date getDateCreated() {
return dateCreated;
}
public void setDateCreated(Date dateCreated) {
this.dateCreated = dateCreated;
}
@Override @Override
public boolean equals(final Object o) { public boolean equals(final Object o) {
if (this == o) { if (this == o) {
return true; return true;
} }
if (!(o instanceof Contribution)) { if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false; return false;
} }
final Contribution that = (Contribution) o; final Contribution that = (Contribution) o;
return getState() == that.getState() && getTransferred() == that.getTransferred() && Objects return state == that.state &&
.equals(getDecimalCoords(), that.getDecimalCoords()) && Objects transferred == that.transferred &&
.equals(getDateCreatedSource(), that.getDateCreatedSource()) && Objects dataLength == that.dataLength &&
.equals(getWikidataPlace(), that.getWikidataPlace()) && Objects Objects.equals(decimalCoords, that.decimalCoords) &&
.equals(getDepictedItems(), that.getDepictedItems()) && Objects Objects.equals(dateCreatedSource, that.dateCreatedSource) &&
.equals(getMimeType(), that.getMimeType()) && Objects Objects.equals(wikidataPlace, that.wikidataPlace) &&
.equals(getCaptions(), that.getCaptions()); Objects.equals(depictedItems, that.depictedItems) &&
Objects.equals(mimeType, that.mimeType) &&
Objects.equals(localUri, that.localUri) &&
Objects.equals(dateCreated, that.dateCreated);
} }
/**
* Hash code implementation of contributions that considers all parameters for calculating hash.
*/
@Override @Override
public int hashCode() { public int hashCode() {
return Objects return Objects
.hash(getState(), getTransferred(), getDecimalCoords(), getDateCreatedSource(), .hash(super.hashCode(), state, transferred, decimalCoords, dateCreatedSource,
getWikidataPlace(), getDepictedItems(), getMimeType(), getCaptions()); wikidataPlace,
depictedItems, mimeType, localUri, dataLength, dateCreated);
} }
} }

View file

@ -62,9 +62,7 @@ class ContributionBoundaryCallback @Inject constructor(
} }
} }
.subscribeOn(ioThreadScheduler) .subscribeOn(ioThreadScheduler)
.subscribe( .subscribe(::saveContributionsToDB) { error: Throwable ->
::saveContributionsToDB
) { error: Throwable ->
Timber.e( Timber.e(
"Failed to fetch contributions: %s", "Failed to fetch contributions: %s",
error.message error.message

View file

@ -1,12 +1,9 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
@ -24,8 +21,6 @@ import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import org.wikipedia.dataclient.WikiSite;
import timber.log.Timber;
public class ContributionViewHolder extends RecyclerView.ViewHolder { public class ContributionViewHolder extends RecyclerView.ViewHolder {
@ -65,8 +60,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
public void init(final int position, final Contribution contribution) { public void init(final int position, final Contribution contribution) {
this.contribution = contribution; this.contribution = contribution;
fetchAndDisplayCaption(contribution);
this.position = position; this.position = position;
titleView.setText(contribution.getMostRelevantCaption());
final String imageSource = chooseImageSource(contribution.getThumbUrl(), final String imageSource = chooseImageSource(contribution.getThumbUrl(),
contribution.getLocalUri()); contribution.getLocalUri());
if (!TextUtils.isEmpty(imageSource)) { if (!TextUtils.isEmpty(imageSource)) {
@ -116,37 +111,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
} }
} }
/**
* 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(final Contribution contribution) {
if ((contribution.getState() != Contribution.STATE_COMPLETED)) {
titleView.setText(contribution.getDisplayTitle());
} else {
final String pageId = contribution.getPageId();
if (pageId != null) {
Timber.d("Fetching caption for %s", contribution.getFilename());
final String wikibaseMediaId = PAGE_ID_PREFIX
+ pageId; // Create Wikibase media id from the page id. Example media id: M80618155 for https://commons.wikimedia.org/wiki/File:Tantanmen.jpeg with has the pageid 80618155
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseMediaId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber -> {
if (!subscriber.trim().equals(MediaClient.NO_CAPTION)) {
titleView.setText(subscriber);
} else {
titleView.setText(contribution.getDisplayTitle());
}
}));
} else {
titleView.setText(contribution.getDisplayTitle());
}
}
}
/** /**
* Checks if a media exists on the corresponding Wikipedia article Currently the check is made for * Checks if a media exists on the corresponding Wikipedia article Currently the check is made for
* the device's current language Wikipedia * the device's current language Wikipedia

View file

@ -1,9 +1,6 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import java.util.List;
import fr.free.nrw.commons.BasePresenter; import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.Media;
/** /**
* The contract for Contributions View & Presenter * The contract for Contributions View & Presenter
@ -21,8 +18,5 @@ public class ContributionsContract {
void deleteUpload(Contribution contribution); void deleteUpload(Contribution contribution);
void updateContribution(Contribution contribution);
void fetchMediaDetails(Contribution contribution);
} }
} }

View file

@ -46,9 +46,8 @@ public class ContributionsListAdapter extends
* Initializes the view holder with contribution data * Initializes the view holder with contribution data
*/ */
@Override @Override
public void onBindViewHolder(@NonNull final ContributionViewHolder holder, final int position) { public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) {
final Contribution contribution = getItem(position); holder.init(position, getItem(position));
holder.init(position, contribution);
} }
Contribution getContributionForPosition(final int position) { Contribution getContributionForPosition(final int position) {

View file

@ -1,30 +1,10 @@
package fr.free.nrw.commons.contributions; 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.MediaDataExtractor; import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener; import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
import fr.free.nrw.commons.di.CommonsApplicationModule; 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.Scheduler;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; 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;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -77,24 +57,4 @@ public class ContributionsPresenter implements UserActionListener {
.subscribeOn(ioThreadScheduler) .subscribeOn(ioThreadScheduler)
.subscribe()); .subscribe());
} }
@Override
public void updateContribution(Contribution contribution) {
compositeDisposable.add(repository
.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);
}));
}
} }

View file

@ -10,7 +10,7 @@ import fr.free.nrw.commons.contributions.ContributionDao
* The database for accessing the respective DAOs * The database for accessing the respective DAOs
* *
*/ */
@Database(entities = [Contribution::class], version = 2, exportSchema = false) @Database(entities = [Contribution::class], version = 3, exportSchema = false)
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun contributionDao(): ContributionDao abstract fun contributionDao(): ContributionDao

View file

@ -7,11 +7,9 @@ import com.google.gson.reflect.TypeToken;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.location.LatLng; 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.WikidataPlace;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -55,13 +53,13 @@ public class Converters {
} }
@TypeConverter @TypeConverter
public static String mapObjectToString(HashMap<String,String> objectList) { public static String mapObjectToString(Map<String,String> objectList) {
return writeObjectToString(objectList); return writeObjectToString(objectList);
} }
@TypeConverter @TypeConverter
public static HashMap<String,String> stringToMap(String objectList) { public static Map<String,String> stringToMap(String objectList) {
return readObjectWithTypeToken(objectList, new TypeToken<HashMap<String,String>>(){}); return readObjectWithTypeToken(objectList, new TypeToken<Map<String,String>>(){});
} }
@TypeConverter @TypeConverter
@ -94,16 +92,6 @@ public class Converters {
return readObjectWithTypeToken(depictedItems, new TypeToken<List<DepictedItem>>() {}); return readObjectWithTypeToken(depictedItems, new TypeToken<List<DepictedItem>>() {});
} }
@TypeConverter
public static String depictionsToString(Depictions depictedItems) {
return writeObjectToString(depictedItems);
}
@TypeConverter
public static Depictions stringToDepictions(String depictedItems) {
return readObjectFromString(depictedItems, Depictions.class);
}
private static String writeObjectToString(Object object) { private static String writeObjectToString(Object object) {
return object == null ? null : getGson().toJson(object); return object == null ? null : getGson().toJson(object);
} }

View file

@ -88,7 +88,7 @@ public class GridViewAdapter extends ArrayAdapter {
SimpleDraweeView imageView = convertView.findViewById(R.id.depict_image_view); SimpleDraweeView imageView = convertView.findViewById(R.id.depict_image_view);
TextView fileName = convertView.findViewById(R.id.depict_image_title); TextView fileName = convertView.findViewById(R.id.depict_image_title);
TextView author = convertView.findViewById(R.id.depict_image_author); TextView author = convertView.findViewById(R.id.depict_image_author);
fileName.setText(item.getThumbnailTitle()); fileName.setText(item.getDisplayTitle());
setAuthorView(item, author); setAuthorView(item, author);
imageView.setImageURI(item.getThumbUrl()); imageView.setImageURI(item.getThumbUrl());
return convertView; return convertView;

View file

@ -31,10 +31,6 @@ public interface DepictedImagesContract {
*/ */
void setAdapter(List<Media> mediaList); void setAdapter(List<Media> mediaList);
/**
* Seat caption to the image at the given position
*/
void handleLabelforImage(String caption, int position);
/** /**
* Display snackbar * Display snackbar
@ -94,12 +90,6 @@ public interface DepictedImagesContract {
*/ */
void fetchMoreImages(String entityId); void fetchMoreImages(String entityId);
/**
* 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 * add items to query list
*/ */

View file

@ -50,7 +50,6 @@ public class DepictedImagesFragment extends DaggerFragment implements DepictedIm
private String entityId = null; private String entityId = null;
private boolean isLastPage; private boolean isLastPage;
private boolean isLoading = true; private boolean isLoading = true;
private int mediaSize = 0;
@Nullable @Nullable
@Override @Override
@ -146,17 +145,6 @@ public class DepictedImagesFragment extends DaggerFragment implements DepictedIm
}); });
} }
/**
* 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 * Display snackbar
*/ */
@ -257,11 +245,5 @@ public class DepictedImagesFragment extends DaggerFragment implements DepictedIm
progressBar.setVisibility(GONE); progressBar.setVisibility(GONE);
isLoading = false; isLoading = false;
statusTextView.setVisibility(GONE); statusTextView.setVisibility(GONE);
for (Media media : collection) {
final String pageId = media.getPageId();
if (pageId != null) {
presenter.replaceTitlesWithCaptions(PAGE_ID_PREFIX + pageId, mediaSize++);
}
}
} }
} }

View file

@ -5,7 +5,6 @@ import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import fr.free.nrw.commons.Media; 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.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.Scheduler; import io.reactivex.Scheduler;
@ -27,7 +26,6 @@ public class DepictedImagesPresenter implements DepictedImagesContract.UserActio
DepictedImagesContract.View.class.getClassLoader(), DepictedImagesContract.View.class.getClassLoader(),
new Class[]{DepictedImagesContract.View.class}, new Class[]{DepictedImagesContract.View.class},
(proxy, method, methodArgs) -> null); (proxy, method, methodArgs) -> null);
DepictsClient depictsClient;
MediaClient mediaClient; MediaClient mediaClient;
@Named("default_preferences") @Named("default_preferences")
JsonKvStore depictionKvStore; JsonKvStore depictionKvStore;
@ -40,13 +38,13 @@ public class DepictedImagesPresenter implements DepictedImagesContract.UserActio
* Ex: Q9394 * Ex: Q9394
*/ */
private List<Media> queryList = new ArrayList<>(); private List<Media> queryList = new ArrayList<>();
private String entityId;
@Inject @Inject
public DepictedImagesPresenter(@Named("default_preferences") JsonKvStore depictionKvStore, DepictsClient depictsClient, MediaClient mediaClient, @Named(IO_THREAD) Scheduler ioScheduler, public DepictedImagesPresenter(@Named("default_preferences") JsonKvStore depictionKvStore,
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) { MediaClient mediaClient,
@Named(IO_THREAD) Scheduler ioScheduler,
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
this.depictionKvStore = depictionKvStore; this.depictionKvStore = depictionKvStore;
this.depictsClient = depictsClient;
this.ioScheduler = ioScheduler; this.ioScheduler = ioScheduler;
this.mainThreadScheduler = mainThreadScheduler; this.mainThreadScheduler = mainThreadScheduler;
this.mediaClient = mediaClient; this.mediaClient = mediaClient;
@ -68,11 +66,10 @@ public class DepictedImagesPresenter implements DepictedImagesContract.UserActio
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
@Override @Override
public void initList(String entityId) { public void initList(String entityId) {
this.entityId = entityId;
view.setLoadingStatus(true); view.setLoadingStatus(true);
view.progressBarVisible(true); view.progressBarVisible(true);
view.setIsLastPage(false); view.setIsLastPage(false);
compositeDisposable.add(depictsClient.fetchImagesForDepictedItem(entityId, 0) compositeDisposable.add(mediaClient.fetchImagesForDepictedItem(entityId, 0)
.subscribeOn(ioScheduler) .subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler) .observeOn(mainThreadScheduler)
.subscribe(this::handleSuccess, this::handleError)); .subscribe(this::handleSuccess, this::handleError));
@ -86,7 +83,7 @@ public class DepictedImagesPresenter implements DepictedImagesContract.UserActio
@Override @Override
public void fetchMoreImages(String entityId) { public void fetchMoreImages(String entityId) {
view.progressBarVisible(true); view.progressBarVisible(true);
compositeDisposable.add(depictsClient.fetchImagesForDepictedItem(entityId, queryList.size()) compositeDisposable.add(mediaClient.fetchImagesForDepictedItem(entityId, queryList.size())
.subscribeOn(ioScheduler) .subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler) .observeOn(mainThreadScheduler)
.subscribe(this::handlePaginationSuccess, this::handleError)); .subscribe(this::handlePaginationSuccess, this::handleError));
@ -136,20 +133,6 @@ public class DepictedImagesPresenter implements DepictedImagesContract.UserActio
} }
} }
/**
* 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 * add items to query list

View file

@ -1,73 +0,0 @@
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;
}
}

View file

@ -9,18 +9,12 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.viewpager.widget.ViewPager; 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.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import com.google.android.material.tabs.TabLayout;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryImagesCallback; import fr.free.nrw.commons.category.CategoryImagesCallback;
@ -29,6 +23,8 @@ import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.explore.ViewPagerAdapter; import fr.free.nrw.commons.explore.ViewPagerAdapter;
import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity;
import java.util.ArrayList;
import java.util.List;
/** /**
* This activity displays featured images and images uploaded via mobile * This activity displays featured images and images uploaded via mobile

View file

@ -1,8 +1,5 @@
package fr.free.nrw.commons.explore.depictions package fr.free.nrw.commons.explore.depictions
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.depictions.models.DepictionResponse
import fr.free.nrw.commons.depictions.subClass.models.SparqlResponse import fr.free.nrw.commons.depictions.subClass.models.SparqlResponse
import fr.free.nrw.commons.media.MediaInterface import fr.free.nrw.commons.media.MediaInterface
import fr.free.nrw.commons.upload.depicts.DepictsInterface import fr.free.nrw.commons.upload.depicts.DepictsInterface
@ -43,33 +40,6 @@ class DepictsClient @Inject constructor(
.map { it.entities().values.map(::DepictedItem) } .map { it.entities().values.map(::DepictedItem) }
} }
/**
* @return list of images for a particular depict entity
*/
fun fetchImagesForDepictedItem(query: String, sroffset: Int): Observable<List<Media>> {
return mediaInterface.fetchImagesForDepictedItem(
"haswbstatement:" + BuildConfig.DEPICTS_PROPERTY + "=" + query,
sroffset.toString()
)
.map { mwQueryResponse: DepictionResponse ->
mwQueryResponse.query
.search
.map {
Media(
null,
getUrl(it.title),
it.title,
"",
0,
safeParseDate(it.timestamp),
safeParseDate(it.timestamp),
""
)
}
}
}
private fun getUrl(title: String): String { private fun getUrl(title: String): String {
return getImageUrl(title, LARGE_IMAGE_SIZE) return getImageUrl(title, LARGE_IMAGE_SIZE)
} }

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.explore.depictions
import fr.free.nrw.commons.R import fr.free.nrw.commons.R
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity
import fr.free.nrw.commons.explore.BaseSearchFragment import fr.free.nrw.commons.explore.BaseSearchFragment
import fr.free.nrw.commons.explore.SearchFragmentContract
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import javax.inject.Inject import javax.inject.Inject

View file

@ -1,9 +1,93 @@
package fr.free.nrw.commons.explore.media package fr.free.nrw.commons.explore.media
import fr.free.nrw.commons.Media import fr.free.nrw.commons.Media
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.upload.structure.depictions.get
import fr.free.nrw.commons.utils.CommonsDateUtil
import fr.free.nrw.commons.utils.MediaDataExtractorUtil
import fr.free.nrw.commons.wikidata.WikidataProperties
import org.apache.commons.lang3.StringUtils
import org.wikipedia.dataclient.mwapi.MwQueryPage import org.wikipedia.dataclient.mwapi.MwQueryPage
import org.wikipedia.gallery.ExtMetadata
import org.wikipedia.wikidata.DataValue
import org.wikipedia.wikidata.Entities
import java.text.ParseException
import java.util.*
import javax.inject.Inject import javax.inject.Inject
class MediaConverter @Inject constructor() { class MediaConverter @Inject constructor() {
fun convert(mwQueryPage: MwQueryPage): Media = Media.from(mwQueryPage) fun convert(page: MwQueryPage, entity: Entities.Entity): Media {
val imageInfo = page.imageInfo()
requireNotNull(imageInfo) { "No image info" }
val metadata = imageInfo.metadata
requireNotNull(metadata) { "No metadata" }
return Media(
imageInfo.thumbUrl.takeIf { it.isNotBlank() } ?: imageInfo.originalUrl,
imageInfo.originalUrl,
page.title(),
metadata.imageDescription(),
safeParseDate(metadata.dateTime()),
metadata.licenseShortName(),
metadata.prefixedLicenseUrl,
getArtist(metadata),
page.pageId().toString(),
MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories),
metadata.latLng,
entity.labels().mapValues { it.value.value() },
entity.descriptions().mapValues { it.value.value() },
entity.depictionIds()
)
}
/**
* 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
*/
private fun safeParseDate(dateStr: String): Date? {
return try {
CommonsDateUtil.getMediaSimpleDateFormat().parse(dateStr)
} catch (e: ParseException) {
null
}
}
/**
* This method extracts the Commons Username from the artist HTML information
* @param metadata
* @return
*/
private fun getArtist(metadata: ExtMetadata): String? {
return try {
val artistHtml = metadata.artist()
artistHtml.substring(artistHtml.indexOf("title=\""), artistHtml.indexOf("\">"))
.replace("title=\"User:", "")
} catch (ex: java.lang.Exception) {
""
}
}
} }
private fun Entities.Entity.depictionIds() =
this[WikidataProperties.DEPICTS]?.mapNotNull { (it.mainSnak.dataValue as? DataValue.EntityId)?.value?.id }
?: emptyList()
private val ExtMetadata.prefixedLicenseUrl: String
get() = licenseUrl().let {
if (!it.startsWith("http://") && !it.startsWith("https://"))
"https://$it"
else
it
}
private val ExtMetadata.latLng: LatLng?
get() = if (!StringUtils.isBlank(gpsLatitude) && !StringUtils.isBlank(gpsLongitude))
LatLng(gpsLatitude.toDouble(), gpsLongitude.toDouble(), 0.0f)
else
null

View file

@ -1,34 +1,17 @@
package fr.free.nrw.commons.explore.media package fr.free.nrw.commons.explore.media
import fr.free.nrw.commons.Media import fr.free.nrw.commons.Media
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX
import fr.free.nrw.commons.explore.LiveDataConverter import fr.free.nrw.commons.explore.LiveDataConverter
import fr.free.nrw.commons.explore.PageableDataSource import fr.free.nrw.commons.explore.PageableDataSource
import fr.free.nrw.commons.explore.depictions.LoadFunction import fr.free.nrw.commons.explore.depictions.LoadFunction
import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.media.MediaClient.Companion.NO_CAPTION
import javax.inject.Inject import javax.inject.Inject
class PageableMediaDataSource @Inject constructor( class PageableMediaDataSource @Inject constructor(
liveDataConverter: LiveDataConverter, liveDataConverter: LiveDataConverter,
private val mediaConverter: MediaConverter,
private val mediaClient: MediaClient private val mediaClient: MediaClient
) : PageableDataSource<Media>(liveDataConverter) { ) : PageableDataSource<Media>(liveDataConverter) {
override val loadFunction: LoadFunction<Media> = { loadSize: Int, startPosition: Int -> override val loadFunction: LoadFunction<Media> = { loadSize: Int, startPosition: Int ->
mediaClient.getMediaListFromSearch(query, loadSize, startPosition) mediaClient.getMediaListFromSearch(query, loadSize, startPosition).blockingGet()
.map { it.query()?.pages()?.map(mediaConverter::convert) ?: emptyList() }
.map { it.zip(getCaptions(it)) }
.map { it.map { (media, caption) -> media.also { it.caption = caption } } }
.blockingGet()
} }
private fun getCaptions(it: List<Media>) =
mediaClient.getEntities(it.joinToString("|") { PAGE_ID_PREFIX + it.pageId })
.map {
it.entities().values.map { entity ->
entity.labels().values.firstOrNull()?.value() ?: NO_CAPTION
}
}
.blockingGet()
} }

View file

@ -35,7 +35,7 @@ class SearchImagesViewHolder(containerView: View, val onImageClicked: (Int) -> U
override fun bind(item: Pair<Media, Int>) { override fun bind(item: Pair<Media, Int>) {
val media = item.first val media = item.first
categoryImageView.setOnClickListener { onImageClicked(item.second) } categoryImageView.setOnClickListener { onImageClicked(item.second) }
categoryImageTitle.text = media.thumbnailTitle categoryImageTitle.text = media.mostRelevantCaption
categoryImageView.setImageURI(media.thumbUrl) categoryImageView.setImageURI(media.thumbUrl)
if (media.creator?.isNotEmpty() == true) { if (media.creator?.isNotEmpty() == true) {
categoryImageAuthor.visibility = View.VISIBLE categoryImageAuthor.visibility = View.VISIBLE

View file

@ -1,30 +0,0 @@
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.EntityId
import org.wikipedia.wikidata.Entities
import java.util.*
@Parcelize
data class Depictions(val depictions: List<IdAndLabel>) : 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 EntityId).value.id
}
?.map { id -> IdAndLabel(id, fetchLabel(mediaClient, id)) }
?: emptyList()
)
private fun fetchLabel(mediaClient: MediaClient, id: String) =
mediaClient.getLabelForDepiction(id, Locale.getDefault().language).blockingGet()
}
}

View file

@ -0,0 +1,4 @@
package fr.free.nrw.commons.media
data class IdAndCaptions(val id: String, val captions: Map<String, String>)

View file

@ -1,14 +0,0 @@
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<String, Entities.Entity>) : this(
entityId,
entities.values.first().labels().values.first().value()
)
}

View file

@ -1,18 +1,17 @@
package fr.free.nrw.commons.media package fr.free.nrw.commons.media
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.Media import fr.free.nrw.commons.Media
import fr.free.nrw.commons.media.Depictions.Companion.from import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX
import fr.free.nrw.commons.explore.media.MediaConverter
import fr.free.nrw.commons.utils.CommonsDateUtil import fr.free.nrw.commons.utils.CommonsDateUtil
import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import org.wikipedia.dataclient.mwapi.MwQueryPage import org.wikipedia.dataclient.mwapi.MwQueryPage
import org.wikipedia.dataclient.mwapi.MwQueryResponse import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.wikidata.Entities import org.wikipedia.wikidata.Entities
import timber.log.Timber
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.collections.ArrayList
/** /**
* Media Client to handle custom calls to Commons MediaWiki APIs * Media Client to handle custom calls to Commons MediaWiki APIs
@ -21,12 +20,16 @@ import kotlin.collections.ArrayList
class MediaClient @Inject constructor( class MediaClient @Inject constructor(
private val mediaInterface: MediaInterface, private val mediaInterface: MediaInterface,
private val pageMediaInterface: PageMediaInterface, private val pageMediaInterface: PageMediaInterface,
private val mediaDetailInterface: MediaDetailInterface private val mediaDetailInterface: MediaDetailInterface,
private val mediaConverter: MediaConverter
) { ) {
fun getMediaById(id: String) =
responseToMediaList(mediaInterface.getMediaById(id)).map { it.first() }
//OkHttpJsonApiClient used JsonKvStore for this. I don't know why. //OkHttpJsonApiClient used JsonKvStore for this. I don't know why.
private val continuationStore: MutableMap<String, Map<String, String>?> private val continuationStore: MutableMap<String, Map<String, String>?> = mutableMapOf()
private val continuationExists: MutableMap<String, Boolean> private val continuationExists: MutableMap<String, Boolean> = mutableMapOf()
/** /**
* Checks if a page exists on Commons * Checks if a page exists on Commons
@ -36,11 +39,7 @@ class MediaClient @Inject constructor(
*/ */
fun checkPageExistsUsingTitle(title: String?): Single<Boolean> { fun checkPageExistsUsingTitle(title: String?): Single<Boolean> {
return mediaInterface.checkPageExistsUsingTitle(title) return mediaInterface.checkPageExistsUsingTitle(title)
.map { mwQueryResponse: MwQueryResponse -> .map { it.query()!!.firstPage()!!.pageId() > 0 }
mwQueryResponse
.query()!!.firstPage()!!.pageId() > 0
}
.singleOrError()
} }
/** /**
@ -50,11 +49,7 @@ class MediaClient @Inject constructor(
*/ */
fun checkFileExistsUsingSha(fileSha: String?): Single<Boolean> { fun checkFileExistsUsingSha(fileSha: String?): Single<Boolean> {
return mediaInterface.checkFileExistsUsingSha(fileSha) return mediaInterface.checkFileExistsUsingSha(fileSha)
.map { mwQueryResponse: MwQueryResponse -> .map { it.query()!!.allImages().size > 0 }
mwQueryResponse
.query()!!.allImages().size > 0
}
.singleOrError()
} }
/** /**
@ -66,18 +61,13 @@ class MediaClient @Inject constructor(
*/ */
fun getMediaListFromCategory(category: String): Single<List<Media>> { fun getMediaListFromCategory(category: String): Single<List<Media>> {
return responseToMediaList( return responseToMediaList(
if (continuationStore.containsKey("category_$category")) mediaInterface.getMediaListFromCategory( mediaInterface.getMediaListFromCategory(
category, category,
10, 10,
continuationStore["category_$category"] continuationStore["category_$category"] ?: emptyMap()
) else //if true ),
mediaInterface.getMediaListFromCategory(
category,
10,
emptyMap()
),
"category_$category" "category_$category"
) //if false )
} }
/** /**
@ -89,25 +79,16 @@ class MediaClient @Inject constructor(
* @return * @return
*/ */
fun getMediaListForUser(userName: String): Single<List<Media>> { fun getMediaListForUser(userName: String): Single<List<Media>> {
val continuation =
if (continuationStore.containsKey("user_$userName")) continuationStore["user_$userName"] else emptyMap()
return responseToMediaList( return responseToMediaList(
mediaInterface mediaInterface.getMediaListForUser(
.getMediaListForUser(userName, 10, continuation), "user_$userName" userName,
10,
continuationStore["user_$userName"] ?: Collections.emptyMap()
),
"user_$userName"
) )
} }
/**
* Check if media for user has reached the end of the list.
* @param userName
* @return
*/
fun doesMediaListForUserHaveMorePages(userName: String): Boolean {
val key = "user_$userName"
return if (continuationExists.containsKey(key)) {
continuationExists[key]!!
} else true
}
/** /**
* This method takes a keyword as input and returns a list of Media objects filtered using image generator query * This method takes a keyword as input and returns a list of Media objects filtered using image generator query
@ -118,40 +99,45 @@ class MediaClient @Inject constructor(
* @param offset * @param offset
* @return * @return
*/ */
fun getMediaListFromSearch( fun getMediaListFromSearch(keyword: String?, limit: Int, offset: Int) =
keyword: String?, responseToMediaList(mediaInterface.getMediaListFromSearch(keyword, limit, offset))
limit: Int,
offset: Int /**
): Single<MwQueryResponse> { * @return list of images for a particular depict entity
return mediaInterface.getMediaListFromSearch(keyword, limit, offset) */
fun fetchImagesForDepictedItem(query: String, sroffset: Int): Single<List<Media>> {
return responseToMediaList(
mediaInterface.fetchImagesForDepictedItem(
"haswbstatement:" + BuildConfig.DEPICTS_PROPERTY + "=" + query,
sroffset.toString()
)
)
} }
private fun responseToMediaList( private fun responseToMediaList(
response: Observable<MwQueryResponse>, response: Single<MwQueryResponse>,
key: String key: String? = null
): Single<List<Media>> { ): Single<List<Media>> {
return response.flatMap { mwQueryResponse: MwQueryResponse? -> return response.map {
if (null == mwQueryResponse || null == mwQueryResponse.query() || null == mwQueryResponse.query()!! if (key != null) {
.pages() continuationExists[key] =
) { it.continuation()?.let { continuation ->
return@flatMap Observable.empty<MwQueryPage>() continuationStore[key] = continuation
true
} ?: false
} }
if (mwQueryResponse.continuation() != null) { it.query()?.pages() ?: emptyList()
continuationStore[key] = mwQueryResponse.continuation() }.flatMap(::mediaFromPageAndEntity)
continuationExists[key] = true
} else { }
continuationExists[key] = false
private fun mediaFromPageAndEntity(pages: List<MwQueryPage>): Single<List<Media>> {
return getEntities(pages.map { "$PAGE_ID_PREFIX${it.pageId()}" })
.map {
pages.zip(it.entities().values)
.map { (page, entity) -> mediaConverter.convert(page, entity) }
} }
Observable.fromIterable(mwQueryResponse.query()!!.pages())
}
.map { page: MwQueryPage? -> Media.from(page) }
.collect(
{ ArrayList() }
) { obj: MutableList<Media>, e: Media ->
obj.add(
e
)
}.map { it.toList() }
} }
/** /**
@ -161,17 +147,8 @@ class MediaClient @Inject constructor(
* @return * @return
*/ */
fun getMedia(titles: String?): Single<Media> { fun getMedia(titles: String?): Single<Media> {
return mediaInterface.getMedia(titles) return responseToMediaList(mediaInterface.getMedia(titles))
.flatMap { mwQueryResponse: MwQueryResponse? -> .map { it.first() }
if (null == mwQueryResponse || null == mwQueryResponse.query() || null == mwQueryResponse.query()!!
.firstPage()
) {
return@flatMap Observable.empty<MwQueryPage>()
}
Observable.just(mwQueryResponse.query()!!.firstPage())
}
.map { page: MwQueryPage? -> Media.from(page) }
.single(Media.EMPTY)
} }
/** /**
@ -179,122 +156,37 @@ class MediaClient @Inject constructor(
* *
* @return Media object corresponding to the picture of the day * @return Media object corresponding to the picture of the day
*/ */
val pictureOfTheDay: Single<Media> fun getPictureOfTheDay(): Single<Media> {
get() { val date = CommonsDateUtil.getIso8601DateFormatShort().format(Date())
val date = return responseToMediaList(mediaInterface.getMediaWithGenerator("Template:Potd/$date")).map { it.first() }
CommonsDateUtil.getIso8601DateFormatShort().format(Date())
Timber.d("Current date is %s", date) }
val template = "Template:Potd/$date"
return mediaInterface.getMediaWithGenerator(template)
.flatMap { mwQueryResponse: MwQueryResponse? ->
if (null == mwQueryResponse || null == mwQueryResponse.query() || null == mwQueryResponse.query()!!
.firstPage()
) {
return@flatMap Observable.empty<MwQueryPage>()
}
Observable.just(mwQueryResponse.query()!!.firstPage())
}
.map { page: MwQueryPage? -> Media.from(page) }
.single(Media.EMPTY)
}
fun getPageHtml(title: String?): Single<String> { fun getPageHtml(title: String?): Single<String> {
return mediaInterface.getPageHtml(title) return mediaInterface.getPageHtml(title)
.filter { obj: MwParseResponse -> obj.success() } .map { obj: MwParseResponse -> obj.parse()?.text() ?: "" }
.map { obj: MwParseResponse -> obj.parse() }
.map { obj: MwParseResult? -> obj!!.text() }
.first("")
} }
fun getEntities(entityIds: List<String>): Single<Entities> {
return if (entityIds.isEmpty())
Single.error(Exception("empty list passed for ids"))
else
mediaDetailInterface.getEntity(entityIds.joinToString("|"))
}
/** /**
* @return caption for image using wikibaseIdentifier * Check if media for user has reached the end of the list.
* @param userName
* @return
*/ */
fun getCaptionByWikibaseIdentifier(wikibaseIdentifier: String?): Single<String> { fun doesMediaListForUserHaveMorePages(userName: String): Boolean {
return mediaDetailInterface.getEntityForImage( val key = "user_$userName"
Locale.getDefault().language, return if (continuationExists.containsKey(key)) continuationExists[key]!! else true
wikibaseIdentifier
)
.map { mediaDetailResponse: Entities ->
if (isSuccess(mediaDetailResponse)) {
for (wikibaseItem in mediaDetailResponse.entities().values) {
for (label in wikibaseItem.labels().values) {
return@map label.value()
}
}
}
NO_CAPTION
}
.singleOrError()
} }
fun doesPageContainMedia(title: String?): Single<Boolean> { fun doesPageContainMedia(title: String?): Single<Boolean> {
return pageMediaInterface.getMediaList(title) return pageMediaInterface.getMediaList(title)
.map { it.items.isNotEmpty() } .map { it.items.isNotEmpty() }
} }
private fun isSuccess(response: Entities?): Boolean {
return response != null && response.success == 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)
*/
fun getDepictions(filename: String?): Single<Depictions> {
return mediaDetailInterface.fetchEntitiesByFileName(
Locale.getDefault().language, filename
)
.map { entities: Entities? ->
from(
entities!!,
this
)
}
.singleOrError()
}
/**
* Gets labels for Depictions using Entity Id from MediaWikiAPI
*
* @param entityId EntityId (Ex: Q81566) of the depict entity
* @return label
*/
fun getLabelForDepiction(
entityId: String?,
language: String
): Single<String> {
return mediaDetailInterface.getEntity(entityId)
.map { entities: Entities ->
if (isSuccess(entities)) {
for (entity in entities.entities().values) {
val languageToLabelMap =
entity.labels()
if (languageToLabelMap.containsKey(language)) {
return@map languageToLabelMap[language]!!.value()
}
for (label in languageToLabelMap.values) {
return@map label.value()
}
}
}
throw RuntimeException("failed getEntities")
}
}
fun getEntities(entityId: String?): Single<Entities> {
return mediaDetailInterface.getEntity(entityId)
}
companion object {
const val NO_CAPTION = "No caption"
private const val NO_DEPICTION = "No depiction"
}
init {
continuationStore =
HashMap()
continuationExists = HashMap()
}
} }

View file

@ -54,11 +54,12 @@ import fr.free.nrw.commons.ui.widget.HtmlTextView;
import fr.free.nrw.commons.utils.ViewUtilWrapper; import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import javax.inject.Inject; import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.wikipedia.util.DateUtil; import org.wikipedia.util.DateUtil;
@ -70,7 +71,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private boolean isCategoryImage; private boolean isCategoryImage;
private MediaDetailPagerFragment.MediaDetailProvider detailProvider; private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
private int index; private int index;
private Locale locale;
private boolean isDeleted = false; private boolean isDeleted = false;
@ -141,7 +141,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@BindView(R.id.mediaDetailScrollView) @BindView(R.id.mediaDetailScrollView)
ScrollView scrollView; ScrollView scrollView;
private ArrayList<String> categoryNames;
/** /**
* Depicts is a feature part of Structured data. Multiple Depictions can be added for an image just like categories. * 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 * However unlike categories depictions is multi-lingual
@ -150,10 +149,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private ImageInfo imageInfoCache; private ImageInfo imageInfoCache;
private int oldWidthOfImageView; private int oldWidthOfImageView;
private int newWidthOfImageView; private int newWidthOfImageView;
private Depictions depictions;
private boolean categoriesLoaded = false;
private boolean categoriesPresent = false;
private boolean depictionLoaded = false;
private boolean heightVerifyingBoolean = true; // helps in maintaining aspect ratio private boolean heightVerifyingBoolean = true; // helps in maintaining aspect ratio
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once! private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
@ -203,9 +198,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
reasonList.add(getString(R.string.deletion_reason_no_longer_want_public)); reasonList.add(getString(R.string.deletion_reason_no_longer_want_public));
reasonList.add(getString(R.string.deletion_reason_bad_for_my_privacy)); reasonList.add(getString(R.string.deletion_reason_bad_for_my_privacy));
categoryNames = new ArrayList<>();
categoryNames.add(getString(R.string.detail_panel_cats_loading));
final View view = inflater.inflate(R.layout.fragment_media_detail, container, false); final View view = inflater.inflate(R.layout.fragment_media_detail, container, false);
ButterKnife.bind(this,view); ButterKnife.bind(this,view);
@ -217,7 +209,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
authorLayout.setVisibility(GONE); authorLayout.setVisibility(GONE);
} }
locale = getResources().getConfiguration().locale;
return view; return view;
} }
@ -291,19 +282,55 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
} }
private void displayMediaDetails() { private void displayMediaDetails() {
//Always load image from Internet to allow viewing the desc, license, and cats setTextFields(media);
setupImageView(); compositeDisposable.addAll(
title.setText(media.getDisplayTitle()); mediaDataExtractor.fetchDepictionIdsAndLabels(media)
desc.setHtmlText(media.getDescription());
license.setText(media.getLicense());
Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename(), media.getPageId())
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(this::setTextFields); .subscribe(this::onDepictionsLoaded, Timber::e),
compositeDisposable.add(disposable); mediaDataExtractor.checkDeletionRequestExists(media)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onDeletionPageExists, Timber::e),
mediaDataExtractor.fetchDiscussion(media)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onDiscussionLoaded, Timber::e),
mediaDataExtractor.refresh(media)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onMediaRefreshed, Timber::e)
);
} }
private void onMediaRefreshed(Media media) {
setTextFields(media);
compositeDisposable.addAll(
mediaDataExtractor.fetchDepictionIdsAndLabels(media)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onDepictionsLoaded, Timber::e)
);
}
private void onDiscussionLoaded(String discussion) {
mediaDiscussion.setText(prettyDiscussion(discussion.trim()));
}
private void onDeletionPageExists(Boolean deletionPageExists) {
if (deletionPageExists){
delete.setVisibility(GONE);
nominatedForDeletion.setVisibility(VISIBLE);
} else if (!isCategoryImage) {
delete.setVisibility(VISIBLE);
nominatedForDeletion.setVisibility(GONE);
}
}
private void onDepictionsLoaded(List<IdAndCaptions> idAndCaptions){
depictsLayout.setVisibility(idAndCaptions.isEmpty() ? GONE : VISIBLE);
buildDepictionList(idAndCaptions);
}
/** /**
* The imageSpacer is Basically a transparent overlay for the SimpleDraweeView * The imageSpacer is Basically a transparent overlay for the SimpleDraweeView
* which holds the image to be displayed( moreover this image is out of * which holds the image to be displayed( moreover this image is out of
@ -370,58 +397,45 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
} }
private void setTextFields(Media media) { private void setTextFields(Media media) {
this.media = media;
setupImageView(); setupImageView();
title.setText(media.getDisplayTitle());
desc.setHtmlText(prettyDescription(media)); desc.setHtmlText(prettyDescription(media));
license.setText(prettyLicense(media)); license.setText(prettyLicense(media));
coordinates.setText(prettyCoordinates(media)); coordinates.setText(prettyCoordinates(media));
uploadedDate.setText(prettyUploadedDate(media)); uploadedDate.setText(prettyUploadedDate(media));
mediaDiscussion.setText(prettyDiscussion(media));
if (prettyCaption(media).equals(getContext().getString(R.string.detail_caption_empty))) { if (prettyCaption(media).equals(getContext().getString(R.string.detail_caption_empty))) {
captionLayout.setVisibility(GONE); captionLayout.setVisibility(GONE);
} else mediaCaption.setText(prettyCaption(media)); } else {
mediaCaption.setText(prettyCaption(media));
}
final List<String> categories = media.getCategories();
categoryNames.clear(); if (categories.isEmpty()) {
categoryNames.addAll(media.getCategories());
depictions=media.getDepiction();
depictionLoaded = true;
categoriesLoaded = true;
categoriesPresent = (categoryNames.size() > 0);
if (!categoriesPresent) {
// Stick in a filler element. // Stick in a filler element.
categoryNames.add(getString(R.string.detail_panel_cats_none)); categories.add(getString(R.string.detail_panel_cats_none));
} }
rebuildCatList(); rebuildCatList(categories);
if(depictions != null) {
rebuildDepictionList();
}
else depictsLayout.setVisibility(GONE);
if (media.getCreator() == null || media.getCreator().equals("")) { if (media.getCreator() == null || media.getCreator().equals("")) {
authorLayout.setVisibility(GONE); authorLayout.setVisibility(GONE);
} else { } else {
author.setText(media.getCreator()); author.setText(media.getCreator());
} }
checkDeletion(media);
} }
/** /**
* Populates media details fragment with depiction list * Populates media details fragment with depiction list
* @param idAndCaptions
*/ */
private void rebuildDepictionList() { private void buildDepictionList(List<IdAndCaptions> idAndCaptions) {
depictionContainer.removeAllViews(); depictionContainer.removeAllViews();
for (IdAndLabel depiction : depictions.getDepictions()) { for (IdAndCaptions idAndCaption : idAndCaptions) {
depictionContainer.addView( depictionContainer.addView(buildDepictLabel(
buildDepictLabel( idAndCaption.getCaptions().values().iterator().next(),
depiction.getEntityLabel(), idAndCaption.getId(),
depiction.getEntityId(),
depictionContainer depictionContainer
)); ));
} }
@ -446,7 +460,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@OnClick(R.id.copyWikicode) @OnClick(R.id.copyWikicode)
public void onCopyWikicodeClicked(){ public void onCopyWikicodeClicked(){
String data = "[[" + media.getFilename() + "|thumb|" + media.getDescription() + "]]"; String data = "[[" + media.getFilename() + "|thumb|" + media.getFallbackDescription() + "]]";
Utils.copy("wikiCode",data,getContext()); Utils.copy("wikiCode",data,getContext());
Timber.d("Generated wikidata copy code: %s", data); Timber.d("Generated wikidata copy code: %s", data);
@ -573,42 +587,37 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
} }
} }
private void rebuildCatList() { private void rebuildCatList(List<String> categories) {
categoryContainer.removeAllViews(); categoryContainer.removeAllViews();
// @fixme add the category items for (String category : categories) {
categoryContainer.addView(buildCatLabel(sanitise(category), categoryContainer));
//As per issue #1826(see https://github.com/commons-app/apps-android-commons/issues/1826), some categories come suffixed with strings prefixed with |. As per the discussion
//that was meant for alphabetical sorting of the categories and can be safely removed.
for (int i = 0; i < categoryNames.size(); i++) {
String categoryName = categoryNames.get(i);
//Removed everything after '|'
int indexOfPipe = categoryName.indexOf('|');
if (indexOfPipe != -1) {
categoryName = categoryName.substring(0, indexOfPipe);
//Set the updated category to the list as well
categoryNames.set(i, categoryName);
}
View catLabel = buildCatLabel(categoryName, categoryContainer);
categoryContainer.addView(catLabel);
} }
} }
//As per issue #1826(see https://github.com/commons-app/apps-android-commons/issues/1826), some categories come suffixed with strings prefixed with |. As per the discussion
//that was meant for alphabetical sorting of the categories and can be safely removed.
private String sanitise(String category) {
int indexOfPipe = category.indexOf('|');
if (indexOfPipe != -1) {
//Removed everything after '|'
return category.substring(0, indexOfPipe);
}
return category;
}
/** /**
* Add view to depictions obtained also tapping on depictions should open the url * Add view to depictions obtained also tapping on depictions should open the url
*/ */
private View buildDepictLabel(String depictionName, String entityId, LinearLayout depictionContainer) { private View buildDepictLabel(String depictionName, String entityId, LinearLayout depictionContainer) {
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, depictionContainer, false); final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, depictionContainer,false);
final TextView textView = item.findViewById(R.id.mediaDetailCategoryItemText); final TextView textView = item.findViewById(R.id.mediaDetailCategoryItemText);
textView.setText(depictionName); textView.setText(depictionName);
if (depictionLoaded) { item.setOnClickListener(view -> {
item.setOnClickListener(view -> { Intent intent = new Intent(getContext(), WikidataItemDetailsActivity.class);
Intent intent = new Intent(getContext(), WikidataItemDetailsActivity.class); intent.putExtra("wikidataItemName", depictionName);
intent.putExtra("wikidataItemName", depictionName); intent.putExtra("entityId", entityId);
intent.putExtra("entityId", entityId); getContext().startActivity(intent);
getContext().startActivity(intent); });
});
}
return item; return item;
} }
@ -617,7 +626,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
final TextView textView = item.findViewById(R.id.mediaDetailCategoryItemText); final TextView textView = item.findViewById(R.id.mediaDetailCategoryItemText);
textView.setText(catName); textView.setText(catName);
if (categoriesLoaded && categoriesPresent) { if(!getString(R.string.detail_panel_cats_none).equals(catName)) {
textView.setOnClickListener(view -> { textView.setOnClickListener(view -> {
// Open Category Details page // Open Category Details page
String selectedCategoryTitle = CATEGORY_PREFIX + catName; String selectedCategoryTitle = CATEGORY_PREFIX + catName;
@ -636,30 +645,36 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
* @return caption as string * @return caption as string
*/ */
private String prettyCaption(Media media) { private String prettyCaption(Media media) {
String caption = media.getCaption().trim(); for (String caption : media.getCaptions().values()) {
if (caption.equals("")) { if (caption.equals("")) {
return getString(R.string.detail_caption_empty); return getString(R.string.detail_caption_empty);
} else { } else {
return caption; return caption;
}
} }
return getString(R.string.detail_caption_empty);
} }
private String prettyDescription(Media media) { private String prettyDescription(Media media) {
// @todo use UI language when multilingual descs are available final String description = chooseDescription(media);
String desc = media.getDescription(); return description.isEmpty() ? getString(R.string.detail_description_empty)
if (desc.equals("")) { : description;
return getString(R.string.detail_description_empty);
} else {
return desc;
}
} }
private String prettyDiscussion(Media media) {
String disc = media.getDiscussion().trim(); private String chooseDescription(Media media) {
if (disc.equals("")) { final Map<String, String> descriptions = media.getDescriptions();
return getString(R.string.detail_discussion_empty); final String multilingualDesc = descriptions.get(Locale.getDefault().getLanguage());
} else { if (multilingualDesc != null) {
return disc; return multilingualDesc;
} }
for (String description : descriptions.values()) {
return description;
}
return media.getFallbackDescription();
}
private String prettyDiscussion(String discussion) {
return discussion.isEmpty() ? getString(R.string.detail_discussion_empty) : discussion;
} }
private String prettyLicense(Media media) { private String prettyLicense(Media media) {
@ -691,14 +706,4 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
return media.getCoordinates().getPrettyCoordinateString(); return media.getCoordinates().getPrettyCoordinateString();
} }
private void checkDeletion(Media media){
if (media.isRequestedDeletion()){
delete.setVisibility(GONE);
nominatedForDeletion.setVisibility(VISIBLE);
} else if (!isCategoryImage) {
delete.setVisibility(VISIBLE);
nominatedForDeletion.setVisibility(GONE);
}
}
} }

View file

@ -1,7 +1,5 @@
package fr.free.nrw.commons.media; package fr.free.nrw.commons.media;
import fr.free.nrw.commons.depictions.models.DepictionResponse;
import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
import java.util.Map; import java.util.Map;
import org.wikipedia.dataclient.mwapi.MwQueryResponse; import org.wikipedia.dataclient.mwapi.MwQueryResponse;
@ -24,7 +22,7 @@ public interface MediaInterface {
* @return * @return
*/ */
@GET("w/api.php?action=query&format=json&formatversion=2") @GET("w/api.php?action=query&format=json&formatversion=2")
Observable<MwQueryResponse> checkPageExistsUsingTitle(@Query("titles") String title); Single<MwQueryResponse> checkPageExistsUsingTitle(@Query("titles") String title);
/** /**
* Check if file exists * Check if file exists
@ -33,7 +31,7 @@ public interface MediaInterface {
* @return * @return
*/ */
@GET("w/api.php?action=query&format=json&formatversion=2&list=allimages") @GET("w/api.php?action=query&format=json&formatversion=2&list=allimages")
Observable<MwQueryResponse> checkFileExistsUsingSha(@Query("aisha1") String aisha1); Single<MwQueryResponse> checkFileExistsUsingSha(@Query("aisha1") String aisha1);
/** /**
* This method retrieves a list of Media objects filtered using image generator query * This method retrieves a list of Media objects filtered using image generator query
@ -46,7 +44,8 @@ public interface MediaInterface {
@GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters @GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
"&generator=categorymembers&gcmtype=file&gcmsort=timestamp&gcmdir=desc" + //Category parameters "&generator=categorymembers&gcmtype=file&gcmsort=timestamp&gcmdir=desc" + //Category parameters
MEDIA_PARAMS) MEDIA_PARAMS)
Observable<MwQueryResponse> getMediaListFromCategory(@Query("gcmtitle") String category, @Query("gcmlimit") int itemLimit, @QueryMap Map<String, String> continuation); Single<MwQueryResponse> getMediaListFromCategory(@Query("gcmtitle") String category, @Query("gcmlimit") int itemLimit, @QueryMap Map<String, String> continuation);
/** /**
* This method retrieves a list of Media objects for a given user name * This method retrieves a list of Media objects for a given user name
@ -58,7 +57,7 @@ public interface MediaInterface {
*/ */
@GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters @GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
"&generator=allimages&gaisort=timestamp&gaidir=older" + MEDIA_PARAMS) "&generator=allimages&gaisort=timestamp&gaidir=older" + MEDIA_PARAMS)
Observable<MwQueryResponse> getMediaListForUser(@Query("gaiuser") String username, Single<MwQueryResponse> getMediaListForUser(@Query("gaiuser") String username,
@Query("gailimit") int itemLimit, @QueryMap(encoded = true) Map<String, String> continuation); @Query("gailimit") int itemLimit, @QueryMap(encoded = true) Map<String, String> continuation);
/** /**
@ -82,7 +81,17 @@ public interface MediaInterface {
*/ */
@GET("w/api.php?action=query&format=json&formatversion=2" + @GET("w/api.php?action=query&format=json&formatversion=2" +
MEDIA_PARAMS) MEDIA_PARAMS)
Observable<MwQueryResponse> getMedia(@Query("titles") String title); Single<MwQueryResponse> 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<MwQueryResponse> getMediaById(@Query("pageids") String pageIds);
/** /**
* Fetches Media object from the imageInfo API * Fetches Media object from the imageInfo API
@ -93,21 +102,29 @@ public interface MediaInterface {
*/ */
@GET("w/api.php?action=query&format=json&formatversion=2&generator=images" + @GET("w/api.php?action=query&format=json&formatversion=2&generator=images" +
MEDIA_PARAMS) MEDIA_PARAMS)
Observable<MwQueryResponse> getMediaWithGenerator(@Query("titles") String title); Single<MwQueryResponse> getMediaWithGenerator(@Query("titles") String title);
@GET("w/api.php?format=json&action=parse&prop=text") @GET("w/api.php?format=json&action=parse&prop=text")
Observable<MwParseResponse> getPageHtml(@Query("page") String title); Single<MwParseResponse> getPageHtml(@Query("page") String title);
/** /**
* Fetches list of images from a depiction entity * Fetches caption using file name
* *
* @param query depictionEntityId * @param filename name of the file to be used for fetching captions
* @param sroffset number od depictions already fetched, this is useful in implementing * */
* pagination @GET("w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1")
*/ Single<MwQueryResponse> fetchCaptionByFilename(@Query("language") String language, @Query("titles") String filename);
@GET("w/api.php?action=query&list=search&format=json&srnamespace=6") /**
Observable<DepictionResponse> fetchImagesForDepictedItem(@Query("srsearch") String query, * Fetches list of images from a depiction entity
@Query("sroffset") String sroffset); *
* @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<MwQueryResponse> fetchImagesForDepictedItem(@Query("gsrsearch") String query, @Query("gsroffset") String sroffset);
} }

View file

@ -14,18 +14,13 @@ import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.drawerlayout.widget.DrawerLayout; import androidx.drawerlayout.widget.DrawerLayout;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.facebook.drawee.view.SimpleDraweeView; import com.facebook.drawee.view.SimpleDraweeView;
import com.google.android.material.navigation.NavigationView; import com.google.android.material.navigation.NavigationView;
import com.viewpagerindicator.CirclePageIndicator; import com.viewpagerindicator.CirclePageIndicator;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.delete.DeleteHelper; import fr.free.nrw.commons.delete.DeleteHelper;
@ -35,6 +30,7 @@ import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import javax.inject.Inject;
public class ReviewActivity extends NavigationBaseActivity { public class ReviewActivity extends NavigationBaseActivity {
@ -144,10 +140,8 @@ public class ReviewActivity extends NavigationBaseActivity {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> { .subscribe(media -> {
if (media != null) { reviewPagerAdapter.disableButtons();
reviewPagerAdapter.disableButtons(); updateImage(media);
updateImage(media);
}
})); }));
return true; return true;
} }

View file

@ -1,22 +1,19 @@
package fr.free.nrw.commons.review; package fr.free.nrw.commons.review;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.dataclient.mwapi.RecentChange;
import org.wikipedia.util.DateUtil;
import java.util.Collections;
import java.util.Date;
import java.util.Random;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
import java.util.Collections;
import java.util.Date;
import java.util.Random;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.dataclient.mwapi.RecentChange;
import org.wikipedia.util.DateUtil;
@Singleton @Singleton
public class ReviewHelper { public class ReviewHelper {
@ -69,7 +66,6 @@ public class ReviewHelper {
public Single<Media> getRandomMedia() { public Single<Media> getRandomMedia() {
return getRecentChanges() return getRecentChanges()
.flatMapSingle(change -> getRandomMediaFromRecentChange(change)) .flatMapSingle(change -> getRandomMediaFromRecentChange(change))
.onExceptionResumeNext(Observable.just(new Media("")))
.filter(media -> !StringUtils.isBlank(media.getFilename())) .filter(media -> !StringUtils.isBlank(media.getFilename()))
.firstOrError(); .firstOrError();
} }
@ -86,7 +82,7 @@ public class ReviewHelper {
.flatMap(change -> mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + change.getTitle())) .flatMap(change -> mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + change.getTitle()))
.flatMap(isDeleted -> { .flatMap(isDeleted -> {
if (isDeleted) { if (isDeleted) {
return Single.just(new Media("")); return Single.error(new Exception(recentChange.getTitle() + " is deleted"));
} }
return mediaClient.getMedia(recentChange.getTitle()); return mediaClient.getMedia(recentChange.getTitle());
}); });

View file

@ -33,7 +33,7 @@ class PageContentsCreator {
buffer buffer
.append("== {{int:filedesc}} ==\n") .append("== {{int:filedesc}} ==\n")
.append("{{Information\n") .append("{{Information\n")
.append("|description=").append(contribution.getDescription()).append("\n") .append("|description=").append(contribution.getFallbackDescription()).append("\n")
.append("|source=").append("{{own}}\n") .append("|source=").append("{{own}}\n")
.append("|author=[[User:").append(contribution.getCreator()).append("|") .append("|author=[[User:").append(contribution.getCreator()).append("|")
.append(contribution.getCreator()).append("]]\n"); .append(contribution.getCreator()).append("]]\n");

View file

@ -13,7 +13,6 @@ import android.net.Uri;
import android.os.IBinder; import android.os.IBinder;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.text.TextUtils; import android.text.TextUtils;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
@ -110,8 +109,8 @@ public class UploadController {
contribution.setCreator(sessionManager.getAuthorName()); contribution.setCreator(sessionManager.getAuthorName());
} }
if (contribution.getDescription() == null) { if (contribution.getFallbackDescription() == null) {
contribution.setDescription(""); contribution.setFallbackDescription("");
} }
final String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); final String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
@ -164,7 +163,7 @@ public class UploadController {
return mimeType; return mimeType;
} }
private long resolveDataLength(final ContentResolver contentResolver, final Media contribution) { private long resolveDataLength(final ContentResolver contentResolver, final Contribution contribution) {
try { try {
if (contribution.getDataLength() <= 0) { if (contribution.getDataLength() <= 0) {
Timber.d("UploadController/doInBackground, contribution.getLocalUri():%s", contribution.getLocalUri()); Timber.d("UploadController/doInBackground, contribution.getLocalUri():%s", contribution.getLocalUri());
@ -182,7 +181,7 @@ public class UploadController {
return contribution.getDataLength(); return contribution.getDataLength();
} }
private Date resolveDateTakenOrNow(final ContentResolver contentResolver, final Media contribution) { private Date resolveDateTakenOrNow(final ContentResolver contentResolver, final Contribution contribution) {
Timber.d("local uri %s", contribution.getLocalUri()); Timber.d("local uri %s", contribution.getLocalUri());
try(final Cursor cursor = dateTakenCursor(contentResolver, contribution)) { try(final Cursor cursor = dateTakenCursor(contentResolver, contribution)) {
if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) { if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) {
@ -196,7 +195,7 @@ public class UploadController {
} }
} }
private Cursor dateTakenCursor(final ContentResolver contentResolver, final Media contribution) { private Cursor dateTakenCursor(final ContentResolver contentResolver, final Contribution contribution) {
return contentResolver.query(contribution.getLocalUri(), return contentResolver.query(contribution.getLocalUri(),
new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null); new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null);
} }

View file

@ -57,7 +57,7 @@ data class UploadMediaDetail constructor(
* @return a string with the pattern of {{en|1=descriptionText}} * @return a string with the pattern of {{en|1=descriptionText}}
*/ */
@JvmStatic @JvmStatic
fun formatList(descriptions: List<UploadMediaDetail>) = fun formatDescriptions(descriptions: List<UploadMediaDetail>) =
descriptions.filter { it.descriptionText.isNotEmpty() } descriptions.filter { it.descriptionText.isNotEmpty() }
.joinToString { "{{${it.languageCode}|1=${it.descriptionText}}}" } .joinToString { "{{${it.languageCode}|1=${it.descriptionText}}}" }
} }

View file

@ -141,9 +141,6 @@ public class UploadModel {
{ {
final Contribution contribution = new Contribution( final Contribution contribution = new Contribution(
item, sessionManager, newListOf(selectedDepictions), newListOf(selectedCategories)); item, sessionManager, newListOf(selectedDepictions), newListOf(selectedCategories));
Timber.d("Created timestamp while building contribution is %s, %s",
item.getCreatedTimestamp(),
new Date(item.getCreatedTimestamp()));
if (item.getCreatedTimestamp() != -1L) { if (item.getCreatedTimestamp() != -1L) {
contribution.setDateCreated(new Date(item.getCreatedTimestamp())); contribution.setDateCreated(new Date(item.getCreatedTimestamp()));
contribution.setDateCreatedSource(item.getCreatedTimestampSource()); contribution.setDateCreatedSource(item.getCreatedTimestampSource());

View file

@ -16,7 +16,7 @@ internal data class WikidataPlace(
place.wikiDataEntityId!!, place.wikiDataEntityId!!,
place.name, place.name,
place.pic.takeIf { it.isNotBlank() }, place.pic.takeIf { it.isNotBlank() },
if (place.siteLinks.wikipediaLink == null) "" else place.siteLinks.wikipediaLink.toString() place.siteLinks.wikipediaLink?.toString() ?: ""
) )
companion object { companion object {
@ -27,10 +27,6 @@ internal data class WikidataPlace(
} }
fun getWikipediaPageTitle(): String? { fun getWikipediaPageTitle(): String? {
if (wikipediaArticle == null) { return wikipediaArticle?.substringAfterLast("/")
return null
}
val split: Array<String> = wikipediaArticle.split("/".toRegex()).toTypedArray()
return split[split.size - 1]
} }
} }

View file

@ -1,5 +1,6 @@
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.category.CategoryItem import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.location.LatLng import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.nearby.Label import fr.free.nrw.commons.nearby.Label
@ -7,6 +8,7 @@ import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.nearby.Sitelinks import fr.free.nrw.commons.nearby.Sitelinks
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import org.wikipedia.wikidata.* import org.wikipedia.wikidata.*
import java.util.*
fun depictedItem( fun depictedItem(
name: String = "label", name: String = "label",
@ -29,6 +31,38 @@ fun depictedItem(
fun categoryItem(name: String = "name", selected: Boolean = false) = fun categoryItem(name: String = "name", selected: Boolean = false) =
CategoryItem(name, selected) CategoryItem(name, selected)
fun media(
thumbUrl: String? = "thumbUrl",
imageUrl: String? = "imageUrl",
filename: String? = "filename",
fallbackDescription: String? = "fallbackDescription",
dateUploaded: Date? = Date(),
license: String? = "license",
licenseUrl: String? = "licenseUrl",
creator: String? = "creator",
pageId: String = "pageId",
categories: List<String>? = listOf("categories"),
coordinates: LatLng? = LatLng(0.0, 0.0, 0.0f),
captions: Map<String?, String?> = mapOf("en" to "caption"),
descriptions: Map<String?, String?> = mapOf("en" to "description"),
depictionIds: List<String> = listOf("depictionId")
) = Media(
thumbUrl,
imageUrl,
filename,
fallbackDescription,
dateUploaded,
license,
licenseUrl,
creator,
pageId,
categories,
coordinates,
captions,
descriptions,
depictionIds
)
fun place( fun place(
name: String = "name", name: String = "name",
label: Label? = null, label: Label? = null,

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons package fr.free.nrw.commons
import media
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -11,13 +12,15 @@ import org.robolectric.annotation.Config
class MediaTest { class MediaTest {
@Test @Test
fun displayTitleShouldStripExtension() { fun displayTitleShouldStripExtension() {
val m = Media("File:Example.jpg") val m = media(filename = "File:Example.jpg")
assertEquals("Example", m.displayTitle) assertEquals("Example", m.displayTitle)
} }
@Test @Test
fun displayTitleShouldUseSpaceForUnderscore() { fun displayTitleShouldUseSpaceForUnderscore() {
val m = Media("File:Example 1_2.jpg") val m = media(filename = "File:Example 1_2.jpg")
assertEquals("Example 1 2", m.displayTitle) assertEquals("Example 1 2", m.displayTitle)
} }
} }

View file

@ -1,108 +0,0 @@
package fr.free.nrw.commons.bookmarks.pictures;
import android.net.Uri;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.bookmarks.Bookmark;
import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.Single;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
/**
* Tests for bookmark pictures controller
*/
public class BookmarkPicturesControllerTest {
@Mock
MediaClient mediaClient;
@Mock
BookmarkPicturesDao bookmarkDao;
@InjectMocks
BookmarkPicturesController bookmarkPicturesController;
/**
* Init mocks
*/
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
Media mockMedia = getMockMedia();
when(bookmarkDao.getAllBookmarks())
.thenReturn(getMockBookmarkList());
when(mediaClient.getMedia(anyString()))
.thenReturn(Single.just(mockMedia));
}
/**
* Get mock bookmark list
* @return
*/
private List<Bookmark> getMockBookmarkList() {
ArrayList<Bookmark> list = new ArrayList<>();
list.add(new Bookmark("File:Test1.jpg", "Maskaravivek", Uri.EMPTY));
list.add(new Bookmark("File:Test2.jpg", "Maskaravivek", Uri.EMPTY));
return list;
}
/**
* Test case where all bookmark pictures are fetched and media is found against it
*/
@Test
public void loadBookmarkedPictures() {
List<Media> bookmarkedPictures = bookmarkPicturesController.loadBookmarkedPictures().blockingGet();
assertEquals(2, bookmarkedPictures.size());
}
/**
* Test case where all bookmark pictures are fetched and only one media is found
*/
@Test
public void loadBookmarkedPicturesForNullMedia() {
when(mediaClient.getMedia("File:Test1.jpg"))
.thenReturn(Single.error(new NullPointerException("Error occurred")));
when(mediaClient.getMedia("File:Test2.jpg"))
.thenReturn(Single.just(getMockMedia()));
List<Media> bookmarkedPictures = bookmarkPicturesController.loadBookmarkedPictures().blockingGet();
assertEquals(1, bookmarkedPictures.size());
}
private Media getMockMedia() {
return new Media("File:Test.jpg");
}
/**
* Test case where current bookmarks don't match the bookmarks in DB
*/
@Test
public void needRefreshBookmarkedPictures() {
boolean needRefreshBookmarkedPictures = bookmarkPicturesController.needRefreshBookmarkedPictures();
assertTrue(needRefreshBookmarkedPictures);
}
/**
* Test case where the DB is up to date with the bookmarks loaded in the list
*/
@Test
public void doNotNeedRefreshBookmarkedPictures() {
List<Media> bookmarkedPictures = bookmarkPicturesController.loadBookmarkedPictures().blockingGet();
assertEquals(2, bookmarkedPictures.size());
boolean needRefreshBookmarkedPictures = bookmarkPicturesController.needRefreshBookmarkedPictures();
assertFalse(needRefreshBookmarkedPictures);
}
}

View file

@ -0,0 +1,110 @@
package fr.free.nrw.commons.bookmarks.pictures
import android.net.Uri
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.bookmarks.Bookmark
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Single
import media
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import java.util.*
/**
* Tests for bookmark pictures controller
*/
class BookmarkPicturesControllerTest {
@Mock
var mediaClient: MediaClient? = null
@Mock
var bookmarkDao: BookmarkPicturesDao? = null
@InjectMocks
var bookmarkPicturesController: BookmarkPicturesController? = null
/**
* Init mocks
*/
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
val mockMedia = mockMedia
whenever(bookmarkDao!!.allBookmarks)
.thenReturn(mockBookmarkList)
whenever(
mediaClient!!.getMedia(
ArgumentMatchers.anyString()
)
)
.thenReturn(Single.just(mockMedia))
}
/**
* Get mock bookmark list
* @return
*/
private val mockBookmarkList: List<Bookmark>
private get() {
val list = ArrayList<Bookmark>()
list.add(Bookmark("File:Test1.jpg", "Maskaravivek", Uri.EMPTY))
list.add(Bookmark("File:Test2.jpg", "Maskaravivek", Uri.EMPTY))
return list
}
/**
* Test case where all bookmark pictures are fetched and media is found against it
*/
@Test
fun loadBookmarkedPictures() {
val bookmarkedPictures =
bookmarkPicturesController!!.loadBookmarkedPictures().blockingGet()
Assert.assertEquals(2, bookmarkedPictures.size.toLong())
}
/**
* Test case where all bookmark pictures are fetched and only one media is found
*/
@Test
fun loadBookmarkedPicturesForNullMedia() {
whenever(mediaClient!!.getMedia("File:Test1.jpg"))
.thenReturn(Single.error(NullPointerException("Error occurred")))
whenever(mediaClient!!.getMedia("File:Test2.jpg"))
.thenReturn(Single.just(mockMedia))
val bookmarkedPictures =
bookmarkPicturesController!!.loadBookmarkedPictures().blockingGet()
Assert.assertEquals(1, bookmarkedPictures.size.toLong())
}
private val mockMedia: Media
private get() = media(filename="File:Test.jpg")
/**
* Test case where current bookmarks don't match the bookmarks in DB
*/
@Test
fun needRefreshBookmarkedPictures() {
val needRefreshBookmarkedPictures =
bookmarkPicturesController!!.needRefreshBookmarkedPictures()
Assert.assertTrue(needRefreshBookmarkedPictures)
}
/**
* Test case where the DB is up to date with the bookmarks loaded in the list
*/
@Test
fun doNotNeedRefreshBookmarkedPictures() {
val bookmarkedPictures =
bookmarkPicturesController!!.loadBookmarkedPictures().blockingGet()
Assert.assertEquals(2, bookmarkedPictures.size.toLong())
val needRefreshBookmarkedPictures =
bookmarkPicturesController!!.needRefreshBookmarkedPictures()
Assert.assertFalse(needRefreshBookmarkedPictures)
}
}

View file

@ -8,12 +8,12 @@ import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.utils.ViewUtilWrapper import fr.free.nrw.commons.utils.ViewUtilWrapper
import io.reactivex.Single import io.reactivex.Single
import media
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyInt
import org.mockito.InjectMocks import org.mockito.InjectMocks
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.* import org.mockito.Mockito.*
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import java.util.* import java.util.*
@ -55,11 +55,10 @@ class ReasonBuilderTest {
`when`(okHttpJsonApiClient!!.getAchievements(anyString())) `when`(okHttpJsonApiClient!!.getAchievements(anyString()))
.thenReturn(Single.just(mock(FeedbackResponse::class.java))) .thenReturn(Single.just(mock(FeedbackResponse::class.java)))
val media = Media("test_file") val media = media(filename="test_file", dateUploaded = Date())
media.dateUploaded=Date()
reasonBuilder!!.getReason(media, "test") reasonBuilder!!.getReason(media, "test")
verify(sessionManager, times(0))!!.forceLogin(any(Context::class.java)) verify(sessionManager, times(0))!!.forceLogin(any(Context::class.java))
verify(okHttpJsonApiClient, times(1))!!.getAchievements(anyString()) verify(okHttpJsonApiClient, times(1))!!.getAchievements(anyString())
} }
} }

View file

@ -3,10 +3,8 @@ package fr.free.nrw.commons.depictions
import fr.free.nrw.commons.Media import fr.free.nrw.commons.Media
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment
import fr.free.nrw.commons.depictions.Media.DepictedImagesPresenter import fr.free.nrw.commons.depictions.Media.DepictedImagesPresenter
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.kvstore.JsonKvStore import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.schedulers.TestScheduler import io.reactivex.schedulers.TestScheduler
import org.junit.Before import org.junit.Before
@ -27,9 +25,6 @@ class DepictedImagesPresenterTest {
@Mock @Mock
lateinit var jsonKvStore: JsonKvStore lateinit var jsonKvStore: JsonKvStore
@Mock
lateinit var depictsClient: DepictsClient
@Mock @Mock
lateinit var mediaClient: MediaClient lateinit var mediaClient: MediaClient
@ -40,7 +35,7 @@ class DepictedImagesPresenterTest {
@Mock @Mock
lateinit var mediaItem: Media lateinit var mediaItem: Media
var testObservable: Observable<List<Media>>? = null var testSingle: Single<List<Media>>? = null
@Before @Before
@ -49,28 +44,20 @@ class DepictedImagesPresenterTest {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
testScheduler = TestScheduler() testScheduler = TestScheduler()
mediaList.add(mediaItem) mediaList.add(mediaItem)
testObservable = Observable.just(mediaList) testSingle = Single.just(mediaList)
depictedImagesPresenter = DepictedImagesPresenter(jsonKvStore, depictsClient, mediaClient, testScheduler, testScheduler) depictedImagesPresenter = DepictedImagesPresenter(jsonKvStore,
mediaClient, testScheduler, testScheduler)
depictedImagesPresenter.onAttachView(view) depictedImagesPresenter.onAttachView(view)
} }
@Test @Test
fun initList() { fun initList() {
Mockito.`when`( Mockito.`when`(
depictsClient.fetchImagesForDepictedItem(ArgumentMatchers.anyString(), mediaClient.fetchImagesForDepictedItem(ArgumentMatchers.anyString(),
ArgumentMatchers.anyInt()) ArgumentMatchers.anyInt())
).thenReturn(testObservable) ).thenReturn(testSingle)
depictedImagesPresenter.initList("rabbit") depictedImagesPresenter.initList("rabbit")
depictedImagesPresenter.handleSuccess(mediaList) depictedImagesPresenter.handleSuccess(mediaList)
verify(view)?.handleSuccess(mediaList) verify(view)?.handleSuccess(mediaList)
} }
@Test
fun replaceTitlesWithCaptions() {
var stringObservable: Single<String>? = Single.just(String())
Mockito.`when`(mediaClient.getCaptionByWikibaseIdentifier(ArgumentMatchers.anyString()))?.thenReturn(stringObservable)
depictedImagesPresenter.replaceTitlesWithCaptions("File:rabbit.jpg", 0)
testScheduler.triggerActions()
verify(view)?.handleLabelforImage("", 0)
}
} }

View file

@ -4,7 +4,9 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList import androidx.paging.PagedList
import com.jraska.livedata.test import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.* import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import io.reactivex.processors.PublishProcessor import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.TestScheduler import io.reactivex.schedulers.TestScheduler
import org.junit.Before import org.junit.Before

View file

@ -1,10 +1,7 @@
package fr.free.nrw.commons.explore.media package fr.free.nrw.commons.explore.media
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX
import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Single import io.reactivex.Single
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
@ -13,10 +10,6 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.Mock import org.mockito.Mock
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import org.wikipedia.dataclient.mwapi.MwQueryPage
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.dataclient.mwapi.MwQueryResult
import org.wikipedia.wikidata.Entities
class PageableMediaDataSourceTest { class PageableMediaDataSourceTest {
@Mock @Mock
@ -31,41 +24,10 @@ class PageableMediaDataSourceTest {
@Test @Test
fun `loadFunction invokes mediaClient and has Label`() { fun `loadFunction invokes mediaClient and has Label`() {
val (media, entity: Entities.Entity) = expectMediaAndEntity()
val label: Entities.Label = mock()
whenever(entity.labels()).thenReturn(mapOf(" " to label))
whenever(label.value()).thenReturn("label")
val pageableMediaDataSource = PageableMediaDataSource(mock(), mediaConverter, mediaClient)
pageableMediaDataSource.onQueryUpdated("test")
assertThat(pageableMediaDataSource.loadFunction(0,1), `is`(listOf(media)))
verify(media).caption = "label"
}
@Test
fun `loadFunction invokes mediaClient and does not have Label`() {
val (media, entity: Entities.Entity) = expectMediaAndEntity()
whenever(entity.labels()).thenReturn(mapOf())
val pageableMediaDataSource = PageableMediaDataSource(mock(), mediaConverter, mediaClient)
pageableMediaDataSource.onQueryUpdated("test")
assertThat(pageableMediaDataSource.loadFunction(0,1), `is`(listOf(media)))
verify(media).caption = MediaClient.NO_CAPTION
}
private fun expectMediaAndEntity(): Pair<Media, Entities.Entity> {
val queryResponse: MwQueryResponse = mock()
whenever(mediaClient.getMediaListFromSearch("test", 0, 1)) whenever(mediaClient.getMediaListFromSearch("test", 0, 1))
.thenReturn(Single.just(queryResponse)) .thenReturn(Single.just(emptyList()))
val queryResult: MwQueryResult = mock() val pageableMediaDataSource = PageableMediaDataSource(mock(), mediaClient)
whenever(queryResponse.query()).thenReturn(queryResult) pageableMediaDataSource.onQueryUpdated("test")
val queryPage: MwQueryPage = mock() assertThat(pageableMediaDataSource.loadFunction(0,1), `is`(emptyList()))
whenever(queryResult.pages()).thenReturn(listOf(queryPage))
val media = mock<Media>()
whenever(mediaConverter.convert(queryPage)).thenReturn(media)
whenever(media.pageId).thenReturn("1")
val entities: Entities = mock()
whenever(mediaClient.getEntities("${PAGE_ID_PREFIX}1")).thenReturn(Single.just(entities))
val entity: Entities.Entity = mock()
whenever(entities.entities()).thenReturn(mapOf("" to entity))
return Pair(media, entity)
} }
} }

View file

@ -1,11 +1,12 @@
package fr.free.nrw.commons.media package fr.free.nrw.commons.media
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.Media import fr.free.nrw.commons.Media
import fr.free.nrw.commons.explore.media.MediaConverter
import fr.free.nrw.commons.media.model.PageMediaListItem import fr.free.nrw.commons.media.model.PageMediaListItem
import fr.free.nrw.commons.media.model.PageMediaListResponse import fr.free.nrw.commons.media.model.PageMediaListResponse
import fr.free.nrw.commons.utils.CommonsDateUtil import fr.free.nrw.commons.utils.CommonsDateUtil
import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import junit.framework.Assert.* import junit.framework.Assert.*
import org.junit.Before import org.junit.Before
@ -17,6 +18,7 @@ import org.wikipedia.dataclient.mwapi.MwQueryPage
import org.wikipedia.dataclient.mwapi.MwQueryResponse import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.dataclient.mwapi.MwQueryResult import org.wikipedia.dataclient.mwapi.MwQueryResult
import org.wikipedia.gallery.ImageInfo import org.wikipedia.gallery.ImageInfo
import org.wikipedia.wikidata.Entities
import java.util.* import java.util.*
@ -24,12 +26,14 @@ class MediaClientTest {
@Mock @Mock
internal var mediaInterface: MediaInterface? = null internal var mediaInterface: MediaInterface? = null
@Mock
internal var mediaConverter: MediaConverter? = null
@Mock
internal var mediaDetailInterface: MediaDetailInterface? = null
@Mock @Mock
internal var pageMediaInterface: PageMediaInterface? = null internal var pageMediaInterface: PageMediaInterface? = null
@Mock
internal var mediaDetailInterface: MediaDetailInterface? = null
@InjectMocks @InjectMocks
var mediaClient: MediaClient? = null var mediaClient: MediaClient? = null
@ -51,7 +55,7 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.checkPageExistsUsingTitle(ArgumentMatchers.anyString())) `when`(mediaInterface!!.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Single.just(mockResponse))
val checkPageExistsUsingTitle = val checkPageExistsUsingTitle =
mediaClient!!.checkPageExistsUsingTitle("File:Test.jpg").blockingGet() mediaClient!!.checkPageExistsUsingTitle("File:Test.jpg").blockingGet()
@ -69,7 +73,7 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.checkPageExistsUsingTitle(ArgumentMatchers.anyString())) `when`(mediaInterface!!.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Single.just(mockResponse))
val checkPageExistsUsingTitle = val checkPageExistsUsingTitle =
mediaClient!!.checkPageExistsUsingTitle("File:Test.jpg").blockingGet() mediaClient!!.checkPageExistsUsingTitle("File:Test.jpg").blockingGet()
@ -87,7 +91,7 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.checkFileExistsUsingSha(ArgumentMatchers.anyString())) `when`(mediaInterface!!.checkFileExistsUsingSha(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Single.just(mockResponse))
val checkFileExistsUsingSha = mediaClient!!.checkFileExistsUsingSha("abcde").blockingGet() val checkFileExistsUsingSha = mediaClient!!.checkFileExistsUsingSha("abcde").blockingGet()
assertTrue(checkFileExistsUsingSha) assertTrue(checkFileExistsUsingSha)
@ -104,7 +108,7 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.checkFileExistsUsingSha(ArgumentMatchers.anyString())) `when`(mediaInterface!!.checkFileExistsUsingSha(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Single.just(mockResponse))
val checkFileExistsUsingSha = mediaClient!!.checkFileExistsUsingSha("abcde").blockingGet() val checkFileExistsUsingSha = mediaClient!!.checkFileExistsUsingSha("abcde").blockingGet()
assertFalse(checkFileExistsUsingSha) assertFalse(checkFileExistsUsingSha)
@ -112,21 +116,12 @@ class MediaClientTest {
@Test @Test
fun getMedia() { fun getMedia() {
val imageInfo = ImageInfo() val (mockResponse, media: Media) = expectGetEntitiesAndMediaConversion()
val mwQueryPage = mock(MwQueryPage::class.java)
`when`(mwQueryPage.title()).thenReturn("Test")
`when`(mwQueryPage.imageInfo()).thenReturn(imageInfo)
val mwQueryResult = mock(MwQueryResult::class.java)
`when`(mwQueryResult.firstPage()).thenReturn(mwQueryPage)
val mockResponse = mock(MwQueryResponse::class.java)
`when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString())) `when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Single.just(mockResponse))
assertEquals("Test", mediaClient!!.getMedia("abcde").blockingGet().filename) mediaClient!!.getMedia("abcde").test().assertValue(media)
} }
@Test @Test
@ -143,34 +138,34 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString())) `when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Single.just(mockResponse))
mediaClient!!.getMedia("abcde").test().assertErrorMessage("empty list passed for ids")
assertEquals(Media.EMPTY, mediaClient!!.getMedia("abcde").blockingGet())
} }
@Captor
private val filenameCaptor: ArgumentCaptor<String>? = null
@Test @Test
fun getPictureOfTheDay() { fun getPictureOfTheDay() {
val template = "Template:Potd/" + CommonsDateUtil.getIso8601DateFormatShort().format(Date()) val template = "Template:Potd/" + CommonsDateUtil.getIso8601DateFormatShort().format(Date())
val imageInfo = ImageInfo() val (mockResponse, media: Media) = expectGetEntitiesAndMediaConversion()
`when`(mediaInterface!!.getMediaWithGenerator(template))
.thenReturn(Single.just(mockResponse))
mediaClient!!.getPictureOfTheDay().test().assertValue(media)
}
val mwQueryPage = mock(MwQueryPage::class.java) private fun expectGetEntitiesAndMediaConversion(): Pair<MwQueryResponse, Media> {
`when`(mwQueryPage.title()).thenReturn("Test")
`when`(mwQueryPage.imageInfo()).thenReturn(imageInfo)
val mwQueryResult = mock(MwQueryResult::class.java)
`when`(mwQueryResult.firstPage()).thenReturn(mwQueryPage)
val mockResponse = mock(MwQueryResponse::class.java) val mockResponse = mock(MwQueryResponse::class.java)
`when`(mockResponse.query()).thenReturn(mwQueryResult) val queryResult: MwQueryResult = mock()
whenever(mockResponse.query()).thenReturn(queryResult)
`when`(mediaInterface!!.getMediaWithGenerator(filenameCaptor!!.capture())) val queryPage: MwQueryPage = mock()
.thenReturn(Observable.just(mockResponse)) whenever(queryResult.pages()).thenReturn(listOf(queryPage))
whenever(queryPage.pageId()).thenReturn(0)
assertEquals("Test", mediaClient!!.pictureOfTheDay.blockingGet().filename) val entities: Entities = mock()
assertEquals(template, filenameCaptor.value); whenever(mediaDetailInterface!!.getEntity("M0")).thenReturn(Single.just(entities))
val entity: Entities.Entity = mock()
whenever(entities.entities()).thenReturn(mapOf("id" to entity))
val media: Media = mock()
whenever(mediaConverter!!.convert(queryPage, entity)).thenReturn(media)
return Pair(mockResponse, media)
} }
@Captor @Captor
@ -179,17 +174,8 @@ class MediaClientTest {
@Test @Test
fun getMediaListFromCategoryTwice() { fun getMediaListFromCategoryTwice() {
val mockContinuation = mapOf(Pair("gcmcontinue", "test")) val mockContinuation = mapOf(Pair("gcmcontinue", "test"))
val imageInfo = ImageInfo()
val mwQueryPage = mock(MwQueryPage::class.java) val (mockResponse, media: Media) = expectGetEntitiesAndMediaConversion()
`when`(mwQueryPage.title()).thenReturn("Test")
`when`(mwQueryPage.imageInfo()).thenReturn(imageInfo)
val mwQueryResult = mock(MwQueryResult::class.java)
`when`(mwQueryResult.pages()).thenReturn(listOf(mwQueryPage))
val mockResponse = mock(MwQueryResponse::class.java)
`when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mockResponse.continuation()).thenReturn(mockContinuation) `when`(mockResponse.continuation()).thenReturn(mockContinuation)
`when`( `when`(
@ -198,31 +184,23 @@ class MediaClientTest {
continuationCaptor!!.capture() continuationCaptor!!.capture()
) )
) )
.thenReturn(Observable.just(mockResponse)) .thenReturn(Single.just(mockResponse))
val media1 = mediaClient!!.getMediaListFromCategory("abcde").blockingGet().get(0) val media1 = mediaClient!!.getMediaListFromCategory("abcde").blockingGet().get(0)
val media2 = mediaClient!!.getMediaListFromCategory("abcde").blockingGet().get(0) val media2 = mediaClient!!.getMediaListFromCategory("abcde").blockingGet().get(0)
assertEquals(continuationCaptor.allValues[0], emptyMap<String, String>()) assertEquals(continuationCaptor.allValues[0], emptyMap<String, String>())
assertEquals(continuationCaptor.allValues[1], mockContinuation) assertEquals(continuationCaptor.allValues[1], mockContinuation)
assertEquals(media1.filename, "Test") assertEquals(media1, media)
assertEquals(media2.filename, "Test") assertEquals(media2, media)
} }
@Test @Test
fun getMediaListForUser() { fun getMediaListForUser() {
val mockContinuation = mapOf("gcmcontinue" to "test") val mockContinuation = mapOf("gcmcontinue" to "test")
val imageInfo = ImageInfo()
val mwQueryPage = mock(MwQueryPage::class.java) val (mockResponse, media: Media) = expectGetEntitiesAndMediaConversion()
whenever(mwQueryPage.title()).thenReturn("Test")
whenever(mwQueryPage.imageInfo()).thenReturn(imageInfo)
val mwQueryResult = mock(MwQueryResult::class.java)
whenever(mwQueryResult.pages()).thenReturn(listOf(mwQueryPage))
val mockResponse = mock(MwQueryResponse::class.java)
whenever(mockResponse.query()).thenReturn(mwQueryResult)
whenever(mockResponse.continuation()).thenReturn(mockContinuation) whenever(mockResponse.continuation()).thenReturn(mockContinuation)
whenever( whenever(
@ -231,7 +209,7 @@ class MediaClientTest {
continuationCaptor!!.capture() continuationCaptor!!.capture()
) )
) )
.thenReturn(Observable.just(mockResponse)) .thenReturn(Single.just(mockResponse))
val media1 = mediaClient!!.getMediaListForUser("Test").blockingGet().get(0) val media1 = mediaClient!!.getMediaListForUser("Test").blockingGet().get(0)
val media2 = mediaClient!!.getMediaListForUser("Test").blockingGet().get(0) val media2 = mediaClient!!.getMediaListForUser("Test").blockingGet().get(0)
@ -251,7 +229,7 @@ class MediaClientTest {
mockResponse.setParse(mwParseResult) mockResponse.setParse(mwParseResult)
`when`(mediaInterface!!.getPageHtml(ArgumentMatchers.anyString())) `when`(mediaInterface!!.getPageHtml(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Single.just(mockResponse))
assertEquals("Test", mediaClient!!.getPageHtml("abcde").blockingGet()) assertEquals("Test", mediaClient!!.getPageHtml("abcde").blockingGet())
} }
@ -282,7 +260,7 @@ class MediaClientTest {
mockResponse.setParse(null) mockResponse.setParse(null)
`when`(mediaInterface!!.getPageHtml(ArgumentMatchers.anyString())) `when`(mediaInterface!!.getPageHtml(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Single.just(mockResponse))
assertEquals("", mediaClient!!.getPageHtml("abcde").blockingGet()) assertEquals("", mediaClient!!.getPageHtml("abcde").blockingGet())
} }