mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
Replace remaining AsyncTask with RxAndroid (#2681)
This commit is contained in:
parent
a62aaadf90
commit
0bf63f50b3
28 changed files with 1096 additions and 1153 deletions
|
|
@ -1,127 +0,0 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.Resources;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents a list of Licenses
|
||||
*/
|
||||
public class LicenseList {
|
||||
private Map<String, License> licenses = new HashMap<>();
|
||||
private Resources res;
|
||||
|
||||
/**
|
||||
* Constructs new instance of LicenceList
|
||||
*
|
||||
* @param activity License activity
|
||||
*/
|
||||
public LicenseList(Activity activity) {
|
||||
res = activity.getResources();
|
||||
XmlPullParser parser = res.getXml(R.xml.wikimedia_licenses);
|
||||
String namespace = "https://www.mediawiki.org/wiki/Extension:UploadWizard/xmlns/licenses";
|
||||
while (xmlFastForward(parser, namespace, "license")) {
|
||||
String id = parser.getAttributeValue(null, "id");
|
||||
String template = parser.getAttributeValue(null, "template");
|
||||
String url = parser.getAttributeValue(null, "url");
|
||||
String name = nameForTemplate(template);
|
||||
License license = new License(id, template, url, name);
|
||||
licenses.put(id, license);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a collection of licenses
|
||||
* @return License values
|
||||
*/
|
||||
public Collection<License> values() {
|
||||
return licenses.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets license
|
||||
* @param key License key
|
||||
* @return License that matches key
|
||||
*/
|
||||
public License get(String key) {
|
||||
return licenses.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a license from template
|
||||
* @param template License template
|
||||
* @return null
|
||||
*/
|
||||
@Nullable
|
||||
License licenseForTemplate(String template) {
|
||||
String ucTemplate = new PageTitle(template).getDisplayText();
|
||||
for (License license : values()) {
|
||||
if (ucTemplate.equals(new PageTitle(license.getTemplate()).getDisplayText())) {
|
||||
return license;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets template name id
|
||||
* @param template License template
|
||||
* @return name id of template
|
||||
*/
|
||||
private String nameIdForTemplate(String template) {
|
||||
// hack :D (converts dashes and periods to underscores)
|
||||
// cc-by-sa-3.0 -> cc_by_sa_3_0
|
||||
return "license_name_" + template.toLowerCase(Locale.ENGLISH).replace("-",
|
||||
"_").replace(".", "_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets name of given template
|
||||
* @param template License template
|
||||
* @return name of template
|
||||
*/
|
||||
private String nameForTemplate(String template) {
|
||||
int nameId = res.getIdentifier("fr.free.nrw.commons:string/"
|
||||
+ nameIdForTemplate(template), null, null);
|
||||
return (nameId != 0) ? res.getString(nameId) : template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast-forward an XmlPullParser to the next instance of the given element
|
||||
* in the input stream (namespaced).
|
||||
*
|
||||
* @param parser
|
||||
* @param namespace
|
||||
* @param element
|
||||
* @return true on match, false on failure
|
||||
*/
|
||||
private boolean xmlFastForward(XmlPullParser parser, String namespace, String element) {
|
||||
try {
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.getEventType() == XmlPullParser.START_TAG &&
|
||||
parser.getNamespace().equals(namespace) &&
|
||||
parser.getName().equals(element)) {
|
||||
// We found it!
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -50,6 +50,7 @@ public class Media implements Parcelable {
|
|||
protected int width;
|
||||
protected int height;
|
||||
protected String license;
|
||||
protected String licenseUrl;
|
||||
protected String creator;
|
||||
protected ArrayList<String> categories; // as loaded at runtime?
|
||||
protected boolean requestedDeletion;
|
||||
|
|
@ -332,12 +333,70 @@ public class Media implements Parcelable {
|
|||
return license;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating Media object from MWQueryPage.
|
||||
* Earlier only basic details were set for the media object but going forward,
|
||||
* a full media object(with categories, descriptions, coordinates etc) can be constructed using this method
|
||||
*
|
||||
* @param page response from the API
|
||||
* @return Media object
|
||||
*/
|
||||
@Nullable
|
||||
public static Media from(MwQueryPage page) {
|
||||
ImageInfo imageInfo = page.imageInfo();
|
||||
if(imageInfo == null) {
|
||||
return null;
|
||||
}
|
||||
ExtMetadata metadata = imageInfo.getMetadata();
|
||||
if (metadata == null) {
|
||||
return new Media(null, imageInfo.getOriginalUrl(),
|
||||
page.title(), "", 0, null, null, null);
|
||||
}
|
||||
|
||||
Media media = new Media(null,
|
||||
imageInfo.getOriginalUrl(),
|
||||
page.title(),
|
||||
"",
|
||||
0,
|
||||
DateUtils.getDateFromString(metadata.dateTimeOriginal().value()),
|
||||
DateUtils.getDateFromString(metadata.dateTime().value()),
|
||||
StringUtils.getParsedStringFromHtml(metadata.artist().value())
|
||||
);
|
||||
|
||||
String language = Locale.getDefault().getLanguage();
|
||||
if (StringUtils.isNullOrWhiteSpace(language)) {
|
||||
language = "default";
|
||||
}
|
||||
|
||||
media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription().value()));
|
||||
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories().value()));
|
||||
String latitude = metadata.gpsLatitude().value();
|
||||
String longitude = metadata.gpsLongitude().value();
|
||||
|
||||
if(!StringUtils.isNullOrWhiteSpace(latitude) && !StringUtils.isNullOrWhiteSpace(longitude)) {
|
||||
LatLng latLng = new LatLng(Double.parseDouble(latitude), Double.parseDouble(longitude), 0);
|
||||
media.setCoordinates(latLng);
|
||||
}
|
||||
|
||||
media.setLicenseInformation(metadata.licenseShortName().value(), metadata.licenseUrl().value());
|
||||
return media;
|
||||
}
|
||||
|
||||
public String getLicenseUrl() {
|
||||
return licenseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the license name of the file.
|
||||
* @param license license name as a String
|
||||
*/
|
||||
public void setLicense(String license) {
|
||||
public void setLicenseInformation(String license, String licenseUrl) {
|
||||
this.license = license;
|
||||
|
||||
if (!licenseUrl.startsWith("http://") && !licenseUrl.startsWith("https://")) {
|
||||
licenseUrl = "https://" + licenseUrl;
|
||||
}
|
||||
this.licenseUrl = licenseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -457,50 +516,11 @@ public class Media implements Parcelable {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Sets the license name of the file.
|
||||
*
|
||||
* @param page response from the API
|
||||
* @return Media object
|
||||
* @param license license name as a String
|
||||
*/
|
||||
@Nullable
|
||||
public static Media from(MwQueryPage page) {
|
||||
ImageInfo imageInfo = page.imageInfo();
|
||||
if(imageInfo == null) {
|
||||
return null;
|
||||
}
|
||||
ExtMetadata metadata = imageInfo.getMetadata();
|
||||
if (metadata == null) {
|
||||
return new Media(null, imageInfo.getOriginalUrl(),
|
||||
page.title(), "", 0, null, null, null);
|
||||
}
|
||||
|
||||
Media media = new Media(null,
|
||||
imageInfo.getOriginalUrl(),
|
||||
page.title(),
|
||||
"",
|
||||
0,
|
||||
DateUtils.getDateFromString(metadata.dateTimeOriginal().value()),
|
||||
DateUtils.getDateFromString(metadata.dateTime().value()),
|
||||
StringUtils.getParsedStringFromHtml(metadata.artist().value())
|
||||
);
|
||||
|
||||
String language = Locale.getDefault().getLanguage();
|
||||
if (StringUtils.isNullOrWhiteSpace(language)) {
|
||||
language = "default";
|
||||
}
|
||||
media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription().value()));
|
||||
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories().value()));
|
||||
String latitude = metadata.gpsLatitude().value();
|
||||
String longitude = metadata.gpsLongitude().value();
|
||||
|
||||
if(!StringUtils.isNullOrWhiteSpace(latitude) && !StringUtils.isNullOrWhiteSpace(longitude)) {
|
||||
LatLng latLng = new LatLng(Double.parseDouble(latitude), Double.parseDouble(longitude), 0);
|
||||
media.setCoordinates(latLng);
|
||||
}
|
||||
|
||||
media.setLicense(metadata.licenseShortName().value());
|
||||
return media;
|
||||
public void setLicense(String license) {
|
||||
this.license = license;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,14 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.text.Html;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.mwapi.MediaResult;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
import io.reactivex.Single;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
|
|
@ -35,301 +17,49 @@ import timber.log.Timber;
|
|||
* This includes things like category lists and multilingual descriptions,
|
||||
* which are not intrinsic to the media and may change due to editing.
|
||||
*/
|
||||
@Singleton
|
||||
public class MediaDataExtractor {
|
||||
private final MediaWikiApi mediaWikiApi;
|
||||
private boolean fetched;
|
||||
private boolean deletionStatus;
|
||||
private ArrayList<String> categories;
|
||||
private Map<String, String> descriptions;
|
||||
private String discussion;
|
||||
private String license;
|
||||
private @Nullable LatLng coordinates;
|
||||
private final OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
|
||||
@Inject
|
||||
public MediaDataExtractor(MediaWikiApi mwApi) {
|
||||
this.categories = new ArrayList<>();
|
||||
this.descriptions = new HashMap<>();
|
||||
this.fetched = false;
|
||||
public MediaDataExtractor(MediaWikiApi mwApi,
|
||||
OkHttpJsonApiClient okHttpJsonApiClient) {
|
||||
this.okHttpJsonApiClient = okHttpJsonApiClient;
|
||||
this.mediaWikiApi = mwApi;
|
||||
this.discussion = new String();
|
||||
}
|
||||
|
||||
/*
|
||||
* Actually fetch the data over the network.
|
||||
* todo: use local caching?
|
||||
*
|
||||
* Warning: synchronous i/o, call on a background thread
|
||||
*/
|
||||
public void fetch(String filename, LicenseList licenseList) throws IOException {
|
||||
if (fetched) {
|
||||
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
|
||||
}
|
||||
|
||||
try{
|
||||
deletionStatus = mediaWikiApi.pageExists("Commons:Deletion_requests/" + filename);
|
||||
Timber.d("Nominated for deletion: " + deletionStatus);
|
||||
}
|
||||
catch (Exception e){
|
||||
Timber.d(e, "Exception during fetching");
|
||||
}
|
||||
|
||||
MediaResult result = mediaWikiApi.fetchMediaByFilename(filename);
|
||||
MediaResult discussion = mediaWikiApi.fetchMediaByFilename(filename.replace("File", "File talk"));
|
||||
setDiscussion(discussion.getWikiSource());
|
||||
|
||||
|
||||
// In-page category links are extracted from source, as XML doesn't cover [[links]]
|
||||
categories = MediaDataExtractorUtil.extractCategories(result.getWikiSource());
|
||||
|
||||
// Description template info is extracted from preprocessor XML
|
||||
processWikiParseTree(result.getParseTreeXmlSource(), licenseList);
|
||||
fetched = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* We could fetch all category links from API, but we actually only want the ones
|
||||
* directly in the page source so they're editable. In the future this may change.
|
||||
*
|
||||
* @param source wikitext source code
|
||||
* Simplified method to extract all details required to show media details.
|
||||
* It fetches media object, deletion status and talk page for the filename
|
||||
* @param filename for which the details are to be fetched
|
||||
* @return full Media object with all details including deletion status and talk page
|
||||
*/
|
||||
private void extractCategories(String source) {
|
||||
Pattern regex = Pattern.compile("\\[\\[\\s*Category\\s*:([^]]*)\\s*\\]\\]", Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = regex.matcher(source);
|
||||
while (matcher.find()) {
|
||||
String cat = matcher.group(1).trim();
|
||||
categories.add(cat);
|
||||
}
|
||||
}
|
||||
|
||||
private void setDiscussion(String source) {
|
||||
try {
|
||||
discussion = Html.fromHtml(mediaWikiApi.parseWikicode(source)).toString();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void processWikiParseTree(String source, LicenseList licenseList) throws IOException {
|
||||
Document doc;
|
||||
try {
|
||||
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
doc = docBuilder.parse(new ByteArrayInputStream(source.getBytes("UTF-8")));
|
||||
} catch (ParserConfigurationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalStateException | SAXException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
Node templateNode = findTemplate(doc.getDocumentElement(), "information");
|
||||
if (templateNode != null) {
|
||||
Node descriptionNode = findTemplateParameter(templateNode, "description");
|
||||
descriptions = getMultilingualText(descriptionNode);
|
||||
|
||||
Node authorNode = findTemplateParameter(templateNode, "author");
|
||||
}
|
||||
|
||||
Node coordinateTemplateNode = findTemplate(doc.getDocumentElement(), "location");
|
||||
|
||||
if (coordinateTemplateNode != null) {
|
||||
coordinates = getCoordinates(coordinateTemplateNode);
|
||||
} else {
|
||||
coordinates = null;
|
||||
}
|
||||
|
||||
/*
|
||||
Pull up the license data list...
|
||||
look for the templates in two ways:
|
||||
* look for 'self' template and check its first parameter
|
||||
* if none, look for any of the known templates
|
||||
*/
|
||||
Timber.d("MediaDataExtractor searching for license");
|
||||
Node selfLicenseNode = findTemplate(doc.getDocumentElement(), "self");
|
||||
if (selfLicenseNode != null) {
|
||||
Node firstNode = findTemplateParameter(selfLicenseNode, 1);
|
||||
String licenseTemplate = getFlatText(firstNode);
|
||||
License license = licenseList.licenseForTemplate(licenseTemplate);
|
||||
if (license == null) {
|
||||
Timber.d("MediaDataExtractor found no matching license for self parameter: %s; faking it", licenseTemplate);
|
||||
this.license = licenseTemplate; // hack hack! For non-selectable licenses that are still in the system.
|
||||
} else {
|
||||
// fixme: record the self-ness in here too... sigh
|
||||
// all this needs better server-side metadata
|
||||
this.license = license.getKey();
|
||||
Timber.d("MediaDataExtractor found self-license %s", this.license);
|
||||
}
|
||||
} else {
|
||||
for (License license : licenseList.values()) {
|
||||
String templateName = license.getTemplate();
|
||||
Node template = findTemplate(doc.getDocumentElement(), templateName);
|
||||
if (template != null) {
|
||||
// Found!
|
||||
this.license = license.getKey();
|
||||
Timber.d("MediaDataExtractor found non-self license %s", this.license);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Node findTemplate(Element parentNode, String title_) throws IOException {
|
||||
String title = new PageTitle(title_).getDisplayText();
|
||||
NodeList nodes = parentNode.getChildNodes();
|
||||
for (int i = 0, length = nodes.getLength(); i < length; i++) {
|
||||
Node node = nodes.item(i);
|
||||
if (node.getNodeName().equals("template")) {
|
||||
String foundTitle = getTemplateTitle(node);
|
||||
String displayText = new PageTitle(foundTitle).getDisplayText();
|
||||
//replaced equals with contains because multiple sources had multiple formats
|
||||
//say from two sources I had {{Location|12.958117388888889|77.6440805}} & {{Location dec|47.99081|7.845416|heading:255.9}},
|
||||
//So exact string match would show null results for uploads via web
|
||||
if (!(TextUtils.isEmpty(displayText)) && displayText.contains(title)) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getTemplateTitle(Node templateNode) throws IOException {
|
||||
NodeList nodes = templateNode.getChildNodes();
|
||||
for (int i = 0, length = nodes.getLength(); i < length; i++) {
|
||||
Node node = nodes.item(i);
|
||||
if (node.getNodeName().equals("title")) {
|
||||
return node.getTextContent().trim();
|
||||
}
|
||||
}
|
||||
throw new IOException("Template has no title element.");
|
||||
}
|
||||
|
||||
private static abstract class TemplateChildNodeComparator {
|
||||
public abstract boolean match(Node node);
|
||||
}
|
||||
|
||||
private Node findTemplateParameter(Node templateNode, String name) throws IOException {
|
||||
final String theName = name;
|
||||
return findTemplateParameter(templateNode, new TemplateChildNodeComparator() {
|
||||
@Override
|
||||
public boolean match(Node node) {
|
||||
return (Utils.capitalize(node.getTextContent().trim()).equals(Utils.capitalize(theName)));
|
||||
public Single<Media> fetchMediaDetails(String filename) {
|
||||
Single<Media> mediaSingle = okHttpJsonApiClient.getMedia(filename, false);
|
||||
Single<Boolean> pageExistsSingle = mediaWikiApi.pageExists("Commons:Deletion_requests/" + filename);
|
||||
Single<String> discussionSingle = getDiscussion(filename);
|
||||
return Single.zip(mediaSingle, pageExistsSingle, discussionSingle, (media, deletionStatus, discussion) -> {
|
||||
media.setDiscussion(discussion);
|
||||
if (deletionStatus) {
|
||||
media.setRequestedDeletion();
|
||||
}
|
||||
return media;
|
||||
});
|
||||
}
|
||||
|
||||
private Node findTemplateParameter(Node templateNode, int index) throws IOException {
|
||||
final String theIndex = "" + index;
|
||||
return findTemplateParameter(templateNode, new TemplateChildNodeComparator() {
|
||||
@Override
|
||||
public boolean match(Node node) {
|
||||
Element el = (Element)node;
|
||||
if (el.getTextContent().trim().equals(theIndex)) {
|
||||
return true;
|
||||
} else if (el.getAttribute("index") != null && el.getAttribute("index").trim().equals(theIndex)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Node findTemplateParameter(Node templateNode, TemplateChildNodeComparator comparator) throws IOException {
|
||||
NodeList nodes = templateNode.getChildNodes();
|
||||
for (int i = 0, length = nodes.getLength(); i < length; i++) {
|
||||
Node node = nodes.item(i);
|
||||
if (node.getNodeName().equals("part")) {
|
||||
NodeList childNodes = node.getChildNodes();
|
||||
for (int j = 0, childNodesLength = childNodes.getLength(); j < childNodesLength; j++) {
|
||||
Node childNode = childNodes.item(j);
|
||||
if (childNode.getNodeName().equals("name") && comparator.match(childNode)) {
|
||||
// yay! Now fetch the value node.
|
||||
for (int k = j + 1; k < childNodesLength; k++) {
|
||||
Node siblingNode = childNodes.item(k);
|
||||
if (siblingNode.getNodeName().equals("value")) {
|
||||
return siblingNode;
|
||||
}
|
||||
}
|
||||
throw new IOException("No value node found for matched template parameter.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IOException("No matching template parameter node found.");
|
||||
}
|
||||
|
||||
private String getFlatText(Node parentNode) throws IOException {
|
||||
return parentNode.getTextContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the coordinates from the template.
|
||||
* Loops over the children of the coordinate template:
|
||||
* {{Location|47.50111007666667|19.055700301944444}}
|
||||
* and extracts the latitude and longitude.
|
||||
*
|
||||
* @param parentNode The node of the coordinates template.
|
||||
* @return Extracted coordinates.
|
||||
* @throws IOException Parsing failed.
|
||||
* Fetch talk page from the MediaWiki API
|
||||
* @param filename
|
||||
* @return
|
||||
*/
|
||||
private LatLng getCoordinates(Node parentNode) throws IOException {
|
||||
NodeList childNodes = parentNode.getChildNodes();
|
||||
double latitudeText = Double.parseDouble(childNodes.item(1).getTextContent());
|
||||
double longitudeText = Double.parseDouble(childNodes.item(2).getTextContent());
|
||||
return new LatLng(latitudeText, longitudeText, 0);
|
||||
}
|
||||
|
||||
// Extract a dictionary of multilingual texts from a subset of the parse tree.
|
||||
// Texts are wrapped in things like {{en|foo} or {{en|1=foo bar}}.
|
||||
// Text outside those wrappers is stuffed into a 'default' faux language key if present.
|
||||
private Map<String, String> getMultilingualText(Node parentNode) throws IOException {
|
||||
Map<String, String> texts = new HashMap<>();
|
||||
StringBuilder localText = new StringBuilder();
|
||||
|
||||
NodeList nodes = parentNode.getChildNodes();
|
||||
for (int i = 0, length = nodes.getLength(); i < length; i++) {
|
||||
Node node = nodes.item(i);
|
||||
if (node.getNodeName().equals("template")) {
|
||||
// process a template node
|
||||
String title = getTemplateTitle(node);
|
||||
if (title.length() < 3) {
|
||||
// Hopefully a language code. Nasty hack!
|
||||
String lang = title;
|
||||
Node valueNode = findTemplateParameter(node, 1);
|
||||
String value = valueNode.getTextContent(); // hope there's no subtemplates or formatting for now
|
||||
texts.put(lang, value);
|
||||
}
|
||||
} else if (node.getNodeType() == Node.TEXT_NODE) {
|
||||
localText.append(node.getTextContent());
|
||||
}
|
||||
}
|
||||
|
||||
// Some descriptions don't list multilingual variants
|
||||
String defaultText = localText.toString().trim();
|
||||
if (defaultText.length() > 0) {
|
||||
texts.put("default", localText.toString());
|
||||
}
|
||||
return texts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take our metadata and inject it into a live Media object.
|
||||
* Media object might contain stale or cached data, or emptiness.
|
||||
* @param media Media object to inject into
|
||||
*/
|
||||
public void fill(Media media) {
|
||||
if (!fetched) {
|
||||
throw new IllegalStateException("Tried to call MediaDataExtractor.fill() before fetch().");
|
||||
}
|
||||
|
||||
media.setCategories(categories);
|
||||
media.setDescriptions(descriptions);
|
||||
media.setCoordinates(coordinates);
|
||||
media.setDiscussion(discussion);
|
||||
if (license != null) {
|
||||
media.setLicense(license);
|
||||
}
|
||||
if (deletionStatus){
|
||||
media.setRequestedDeletion();
|
||||
}
|
||||
|
||||
// add author, date, etc fields
|
||||
private Single<String> getDiscussion(String filename) {
|
||||
return mediaWikiApi.fetchMediaByFilename(filename.replace("File", "File talk"))
|
||||
.flatMap(mediaResult -> mediaWikiApi.parseWikicode(mediaResult.getWikiSource()))
|
||||
.map(discussion -> HtmlCompat.fromHtml(discussion, HtmlCompat.FROM_HTML_MODE_LEGACY).toString())
|
||||
.onErrorReturn(throwable -> {
|
||||
Timber.e(throwable, "Error occurred while fetching discussion");
|
||||
return "";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
|
||||
class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
|
||||
protected final Media media;
|
||||
private MediaWikiApi mediaWikiApi;
|
||||
|
||||
public MediaThumbnailFetchTask(@NonNull Media media, MediaWikiApi mwApi) {
|
||||
this.media = media;
|
||||
this.mediaWikiApi = mwApi;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
try {
|
||||
return mediaWikiApi.findThumbnailByFilename(params[0]);
|
||||
} catch (Exception e) {
|
||||
// Do something better!
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,32 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
|
||||
import androidx.collection.LruCache;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||
import com.facebook.drawee.view.SimpleDraweeView;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.collection.LruCache;
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.utils.StringUtils;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class MediaWikiImageView extends SimpleDraweeView {
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject LruCache<String, String> thumbnailUrlCache;
|
||||
|
||||
private ThumbnailFetchTask currentThumbnailTask;
|
||||
protected CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
public MediaWikiImageView(Context context) {
|
||||
this(context, null);
|
||||
|
|
@ -44,27 +48,25 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
|||
* @param media the new media
|
||||
*/
|
||||
public void setMedia(Media media) {
|
||||
if (currentThumbnailTask != null) {
|
||||
currentThumbnailTask.cancel(true);
|
||||
}
|
||||
if (media == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (media.getFilename() != null && thumbnailUrlCache.get(media.getFilename()) != null) {
|
||||
setImageUrl(thumbnailUrlCache.get(media.getFilename()));
|
||||
} else {
|
||||
setImageUrl(null);
|
||||
currentThumbnailTask = new ThumbnailFetchTask(media, mwApi);
|
||||
currentThumbnailTask.execute(media.getFilename());
|
||||
}
|
||||
Disposable disposable = fetchMediaThumbnail(media)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(thumbnail -> {
|
||||
if (!StringUtils.isNullOrWhiteSpace(thumbnail)) {
|
||||
setImageUrl(thumbnail);
|
||||
}
|
||||
}, throwable -> Timber.e(throwable, "Error occurred while fetching thumbnail"));
|
||||
|
||||
compositeDisposable.add(disposable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
if (currentThumbnailTask != null) {
|
||||
currentThumbnailTask.cancel(true);
|
||||
}
|
||||
compositeDisposable.clear();
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +88,27 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
|||
.build());
|
||||
}
|
||||
|
||||
//TODO: refactor the logic for thumbnails. ImageInfo API can be used to fetch thumbnail upfront
|
||||
/**
|
||||
* Fetches media thumbnail from the server
|
||||
* @param media
|
||||
* @return
|
||||
*/
|
||||
public Single<String> fetchMediaThumbnail(Media media) {
|
||||
if (media.getFilename() != null && thumbnailUrlCache.get(media.getFilename()) != null) {
|
||||
return Single.just(thumbnailUrlCache.get(media.getFilename()));
|
||||
}
|
||||
return mwApi.findThumbnailByFilename(media.getFilename())
|
||||
.map(result -> {
|
||||
if (TextUtils.isEmpty(result) && media.getLocalUri() != null) {
|
||||
return media.getLocalUri().toString();
|
||||
} else {
|
||||
thumbnailUrlCache.put(media.getFilename(), result);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the image from the URL.
|
||||
* @param url the URL of the image
|
||||
|
|
@ -94,29 +117,4 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
|||
setImageURI(url);
|
||||
}
|
||||
|
||||
private class ThumbnailFetchTask extends MediaThumbnailFetchTask {
|
||||
ThumbnailFetchTask(@NonNull Media media, @NonNull MediaWikiApi mwApi) {
|
||||
super(media, mwApi);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String result) {
|
||||
if (isCancelled()) {
|
||||
return;
|
||||
}
|
||||
if (TextUtils.isEmpty(result) && media.getLocalUri() != null) {
|
||||
result = media.getLocalUri().toString();
|
||||
} else {
|
||||
// only cache meaningful thumbnails received from network.
|
||||
try {
|
||||
thumbnailUrlCache.put(media.getFilename(), result);
|
||||
} catch (NullPointerException npe) {
|
||||
Timber.e("error when adding pic to cache " + npe);
|
||||
|
||||
Toast.makeText(getContext(), R.string.error_while_cache, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
setImageUrl(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
194
app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java
Normal file
194
app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
package fr.free.nrw.commons.delete;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.notification.NotificationHelper;
|
||||
import fr.free.nrw.commons.review.ReviewActivity;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||
import io.reactivex.Single;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_DELETE;
|
||||
|
||||
/**
|
||||
* Refactored async task to Rx
|
||||
*/
|
||||
@Singleton
|
||||
public class DeleteHelper {
|
||||
private final MediaWikiApi mwApi;
|
||||
private final SessionManager sessionManager;
|
||||
private final NotificationHelper notificationHelper;
|
||||
private final ViewUtilWrapper viewUtil;
|
||||
|
||||
@Inject
|
||||
public DeleteHelper(MediaWikiApi mwApi,
|
||||
SessionManager sessionManager,
|
||||
NotificationHelper notificationHelper,
|
||||
ViewUtilWrapper viewUtil) {
|
||||
this.mwApi = mwApi;
|
||||
this.sessionManager = sessionManager;
|
||||
this.notificationHelper = notificationHelper;
|
||||
this.viewUtil = viewUtil;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public interface to nominate a particular media file for deletion
|
||||
* @param context
|
||||
* @param media
|
||||
* @param reason
|
||||
* @return
|
||||
*/
|
||||
public Single<Boolean> makeDeletion(Context context, Media media, String reason) {
|
||||
viewUtil.showShortToast(context, "Trying to nominate " + media.getDisplayTitle() + " for deletion");
|
||||
|
||||
return Single.fromCallable(() -> delete(media, reason))
|
||||
.flatMap(result -> Single.fromCallable(() ->
|
||||
showDeletionNotification(context, media, result)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes several API calls to nominate the file for deletion
|
||||
* @param media
|
||||
* @param reason
|
||||
* @return
|
||||
*/
|
||||
private boolean delete(Media media, String reason) {
|
||||
String editToken;
|
||||
String authCookie;
|
||||
String summary = "Nominating " + media.getFilename() + " for deletion.";
|
||||
|
||||
authCookie = sessionManager.getAuthCookie();
|
||||
mwApi.setAuthCookie(authCookie);
|
||||
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
String fileDeleteString = "{{delete|reason=" + reason +
|
||||
"|subpage=" + media.getFilename() +
|
||||
"|day=" + calendar.get(Calendar.DAY_OF_MONTH) +
|
||||
"|month=" + calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault()) +
|
||||
"|year=" + calendar.get(Calendar.YEAR) +
|
||||
"}}";
|
||||
|
||||
String subpageString = "=== [[:" + media.getFilename() + "]] ===\n" +
|
||||
reason +
|
||||
" ~~~~";
|
||||
|
||||
String logPageString = "\n{{Commons:Deletion requests/" + media.getFilename() +
|
||||
"}}\n";
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault());
|
||||
String date = sdf.format(calendar.getTime());
|
||||
|
||||
String userPageString = "\n{{subst:idw|" + media.getFilename() +
|
||||
"}} ~~~~";
|
||||
|
||||
try {
|
||||
editToken = mwApi.getEditToken();
|
||||
if (editToken.equals("+\\")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mwApi.prependEdit(editToken, fileDeleteString + "\n",
|
||||
media.getFilename(), summary);
|
||||
mwApi.edit(editToken, subpageString + "\n",
|
||||
"Commons:Deletion_requests/" + media.getFilename(), summary);
|
||||
mwApi.appendEdit(editToken, logPageString + "\n",
|
||||
"Commons:Deletion_requests/" + date, summary);
|
||||
mwApi.appendEdit(editToken, userPageString + "\n",
|
||||
"User_Talk:" + sessionManager.getCurrentAccount().name, summary);
|
||||
} catch (Exception e) {
|
||||
Timber.e(e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean showDeletionNotification(Context context, Media media, boolean result) {
|
||||
String message;
|
||||
String title = "Nominating for Deletion";
|
||||
|
||||
if (result) {
|
||||
title += ": Success";
|
||||
message = "Successfully nominated " + media.getDisplayTitle() + " deletion.";
|
||||
} else {
|
||||
title += ": Failed";
|
||||
message = "Could not request deletion.";
|
||||
}
|
||||
|
||||
String urlForDelete = BuildConfig.COMMONS_URL + "/wiki/Commons:Deletion_requests/File:" + media.getFilename();
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForDelete));
|
||||
notificationHelper.showNotification(context, title, message, NOTIFICATION_DELETE, browserIntent);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a reason needs to be asked before nominating for deletion
|
||||
* @param media
|
||||
* @param context
|
||||
* @param question
|
||||
* @param problem
|
||||
*/
|
||||
public void askReasonAndExecute(Media media, Context context, String question, String problem) {
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(context);
|
||||
alert.setTitle(question);
|
||||
|
||||
boolean[] checkedItems = {false, false, false, false};
|
||||
ArrayList<Integer> mUserReason = new ArrayList<>();
|
||||
|
||||
String[] reasonList = {"Reason 1", "Reason 2", "Reason 3", "Reason 4"};
|
||||
|
||||
|
||||
if (problem.equals("spam")) {
|
||||
reasonList[0] = "A selfie";
|
||||
reasonList[1] = "Blurry";
|
||||
reasonList[2] = "Nonsense";
|
||||
reasonList[3] = "Other";
|
||||
} else if (problem.equals("copyRightViolation")) {
|
||||
reasonList[0] = "Press photo";
|
||||
reasonList[1] = "Random photo from internet";
|
||||
reasonList[2] = "Logo";
|
||||
reasonList[3] = "Other";
|
||||
}
|
||||
|
||||
alert.setMultiChoiceItems(reasonList, checkedItems, (dialogInterface, position, isChecked) -> {
|
||||
if (isChecked) {
|
||||
mUserReason.add(position);
|
||||
} else {
|
||||
mUserReason.remove((Integer.valueOf(position)));
|
||||
}
|
||||
});
|
||||
|
||||
alert.setPositiveButton("OK", (dialogInterface, i) -> {
|
||||
|
||||
String reason = "Because it is ";
|
||||
for (int j = 0; j < mUserReason.size(); j++) {
|
||||
reason = reason + reasonList[mUserReason.get(j)];
|
||||
if (j != mUserReason.size() - 1) {
|
||||
reason = reason + ", ";
|
||||
}
|
||||
}
|
||||
|
||||
((ReviewActivity) context).reviewController.swipeToNext();
|
||||
((ReviewActivity) context).runRandomizer();
|
||||
|
||||
makeDeletion(context, media, reason);
|
||||
});
|
||||
alert.setNegativeButton("Cancel", null);
|
||||
AlertDialog d = alert.create();
|
||||
d.show();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
package fr.free.nrw.commons.delete;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.view.Gravity;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationCompat.Builder;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.review.ReviewActivity;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static androidx.core.app.NotificationCompat.DEFAULT_ALL;
|
||||
import static androidx.core.app.NotificationCompat.PRIORITY_HIGH;
|
||||
|
||||
public class DeleteTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject SessionManager sessionManager;
|
||||
|
||||
private static final int NOTIFICATION_DELETE = 1;
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
private Builder notificationBuilder;
|
||||
private Context context;
|
||||
private Media media;
|
||||
private String reason;
|
||||
|
||||
public DeleteTask (Context context, Media media, String reason){
|
||||
this.context = context;
|
||||
this.media = media;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
ApplicationlessInjection
|
||||
.getInstance(context.getApplicationContext())
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
|
||||
notificationManager =
|
||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationBuilder = new NotificationCompat.Builder(
|
||||
context,
|
||||
CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)
|
||||
.setOnlyAlertOnce(true);
|
||||
Toast toast = new Toast(context);
|
||||
toast.setGravity(Gravity.CENTER,0,0);
|
||||
toast = Toast.makeText(context,"Trying to nominate "+media.getDisplayTitle()+ " for deletion", Toast.LENGTH_SHORT);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void ...voids) {
|
||||
publishProgress(0);
|
||||
|
||||
String editToken;
|
||||
String authCookie;
|
||||
String summary = context.getString(R.string.nominating_file_for_deletion, media.getFilename());
|
||||
|
||||
authCookie = sessionManager.getAuthCookie();
|
||||
mwApi.setAuthCookie(authCookie);
|
||||
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
String fileDeleteString = "{{delete|reason=" + reason +
|
||||
"|subpage=" +media.getFilename() +
|
||||
"|day=" + calendar.get(Calendar.DAY_OF_MONTH) +
|
||||
"|month=" + calendar.getDisplayName(Calendar.MONTH,Calendar.LONG, Locale.getDefault()) +
|
||||
"|year=" + calendar.get(Calendar.YEAR) +
|
||||
"}}";
|
||||
|
||||
String subpageString = "=== [[:" + media.getFilename() + "]] ===\n" +
|
||||
reason +
|
||||
" ~~~~";
|
||||
|
||||
String logPageString = "\n{{Commons:Deletion requests/" + media.getFilename() +
|
||||
"}}\n";
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault());
|
||||
String date = sdf.format(calendar.getTime());
|
||||
|
||||
String userPageString = "\n{{subst:idw|" + media.getFilename() +
|
||||
"}} ~~~~";
|
||||
|
||||
try {
|
||||
editToken = mwApi.getEditToken();
|
||||
if (editToken.equals("+\\")) {
|
||||
return false;
|
||||
}
|
||||
publishProgress(1);
|
||||
|
||||
mwApi.prependEdit(editToken,fileDeleteString+"\n",
|
||||
media.getFilename(), summary);
|
||||
publishProgress(2);
|
||||
|
||||
mwApi.edit(editToken,subpageString+"\n",
|
||||
"Commons:Deletion_requests/"+media.getFilename(), summary);
|
||||
publishProgress(3);
|
||||
|
||||
mwApi.appendEdit(editToken,logPageString+"\n",
|
||||
"Commons:Deletion_requests/"+date, summary);
|
||||
publishProgress(4);
|
||||
|
||||
mwApi.appendEdit(editToken,userPageString+"\n",
|
||||
"User_Talk:"+ sessionManager.getCurrentAccount().name,summary);
|
||||
publishProgress(5);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Timber.e(e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate (Integer... values){
|
||||
super.onProgressUpdate(values);
|
||||
|
||||
int[] messages = new int[]{
|
||||
R.string.getting_edit_token,
|
||||
R.string.nominate_for_deletion_edit_file_page,
|
||||
R.string.nominate_for_deletion_create_deletion_request,
|
||||
R.string.nominate_for_deletion_edit_deletion_request_log,
|
||||
R.string.nominate_for_deletion_notify_user,
|
||||
R.string.nominate_for_deletion_done
|
||||
};
|
||||
|
||||
String message = "";
|
||||
if (0 < values[0] && values[0] < messages.length) {
|
||||
message = context.getString(messages[values[0]]);
|
||||
}
|
||||
|
||||
notificationBuilder.setContentTitle(context.getString(R.string.nominating_file_for_deletion, media.getFilename()))
|
||||
.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(message))
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setProgress(5, values[0], false)
|
||||
.setOngoing(true);
|
||||
notificationManager.notify(NOTIFICATION_DELETE, notificationBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
String message;
|
||||
String title = "Nominating for Deletion";
|
||||
|
||||
if (result){
|
||||
title += ": Success";
|
||||
message = "Successfully nominated " + media.getDisplayTitle() + " for deletion.";
|
||||
}
|
||||
else {
|
||||
title += ": Failed";
|
||||
message = "Could not request deletion.";
|
||||
}
|
||||
|
||||
notificationBuilder.setDefaults(DEFAULT_ALL)
|
||||
.setContentTitle(title)
|
||||
.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(message))
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setProgress(0,0,false)
|
||||
.setOngoing(false)
|
||||
.setPriority(PRIORITY_HIGH);
|
||||
String urlForDelete = BuildConfig.COMMONS_URL + "/wiki/Commons:Deletion_requests/File:" + media.getFilename();
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW , Uri.parse(urlForDelete));
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context , 1 , browserIntent , PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
notificationBuilder.setContentIntent(pendingIntent);
|
||||
notificationManager.notify(NOTIFICATION_DELETE, notificationBuilder.build());
|
||||
}
|
||||
|
||||
// TODO: refactor; see MediaDetailsFragment.onDeleteButtonClicked
|
||||
// ReviewActivity will use this
|
||||
public static void askReasonAndExecute(Media media, Context context, String question, String problem) {
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(context);
|
||||
alert.setTitle(question);
|
||||
|
||||
boolean[] checkedItems = {false , false, false, false};
|
||||
ArrayList<Integer> mUserReason = new ArrayList<>();
|
||||
|
||||
String[] reasonList= {"Reason 1","Reason 2","Reason 3","Reason 4"};
|
||||
|
||||
|
||||
if(problem.equals("spam")){
|
||||
reasonList[0] = "A selfie";
|
||||
reasonList[1] = "Blurry";
|
||||
reasonList[2] = "Nonsense";
|
||||
reasonList[3] = "Other";
|
||||
}
|
||||
else if(problem.equals("copyRightViolation")){
|
||||
reasonList[0] = "Press photo";
|
||||
reasonList[1] = "Random photo from internet";
|
||||
reasonList[2] = "Logo";
|
||||
reasonList[3] = "Other";
|
||||
}
|
||||
|
||||
alert.setMultiChoiceItems(reasonList, checkedItems, new DialogInterface.OnMultiChoiceClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int position, boolean isChecked) {
|
||||
if(isChecked){
|
||||
mUserReason.add(position);
|
||||
}else{
|
||||
mUserReason.remove((Integer.valueOf(position)));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
alert.setPositiveButton("OK", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
|
||||
String reason = "Because it is ";
|
||||
for (int j = 0; j < mUserReason.size(); j++) {
|
||||
reason = reason + reasonList[mUserReason.get(j)];
|
||||
if (j != mUserReason.size() - 1) {
|
||||
reason = reason + ", ";
|
||||
}
|
||||
}
|
||||
|
||||
((ReviewActivity)context).swipeToNext();
|
||||
((ReviewActivity)context).runRandomizer();
|
||||
|
||||
DeleteTask deleteTask = new DeleteTask(context, media, reason);
|
||||
deleteTask.execute();
|
||||
}
|
||||
});
|
||||
alert.setNegativeButton("Cancel" , null);
|
||||
|
||||
|
||||
AlertDialog d = alert.create();
|
||||
d.show();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -10,11 +10,10 @@ import fr.free.nrw.commons.CommonsApplication;
|
|||
import fr.free.nrw.commons.MediaWikiImageView;
|
||||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
|
||||
import fr.free.nrw.commons.delete.DeleteTask;
|
||||
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
|
||||
import fr.free.nrw.commons.nearby.PlaceRenderer;
|
||||
import fr.free.nrw.commons.review.ReviewController;
|
||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||
import fr.free.nrw.commons.nearby.PlaceRenderer;
|
||||
import fr.free.nrw.commons.upload.FileProcessor;
|
||||
import fr.free.nrw.commons.widget.PicOfDayAppWidget;
|
||||
|
||||
|
|
@ -41,8 +40,6 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
|
|||
|
||||
void inject(LoginActivity activity);
|
||||
|
||||
void inject(DeleteTask deleteTask);
|
||||
|
||||
void inject(SettingsFragment fragment);
|
||||
|
||||
void inject(ReviewController reviewController);
|
||||
|
|
|
|||
|
|
@ -2,11 +2,9 @@ package fr.free.nrw.commons.media;
|
|||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.database.DataSetObserver;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
|
|
@ -25,37 +23,34 @@ import android.widget.Spinner;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import fr.free.nrw.commons.License;
|
||||
import fr.free.nrw.commons.LicenseList;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.MediaDataExtractor;
|
||||
import fr.free.nrw.commons.MediaWikiImageView;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.category.CategoryDetailsActivity;
|
||||
import fr.free.nrw.commons.contributions.ContributionsFragment;
|
||||
import fr.free.nrw.commons.delete.DeleteTask;
|
||||
import fr.free.nrw.commons.delete.DeleteHelper;
|
||||
import fr.free.nrw.commons.delete.ReasonBuilder;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.ui.widget.CompatTextView;
|
||||
import fr.free.nrw.commons.ui.widget.HtmlTextView;
|
||||
import fr.free.nrw.commons.utils.DateUtils;
|
||||
import fr.free.nrw.commons.utils.StringUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
@ -88,13 +83,13 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
}
|
||||
|
||||
@Inject
|
||||
Provider<MediaDataExtractor> mediaDataExtractorProvider;
|
||||
@Inject
|
||||
MediaWikiApi mwApi;
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
MediaDataExtractor mediaDataExtractor;
|
||||
@Inject
|
||||
ReasonBuilder reasonBuilder;
|
||||
@Inject
|
||||
DeleteHelper deleteHelper;
|
||||
@Inject
|
||||
ViewUtilWrapper viewUtil;
|
||||
|
||||
private int initialListTop = 0;
|
||||
|
||||
|
|
@ -105,7 +100,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
@BindView(R.id.mediaDetailTitle)
|
||||
TextView title;
|
||||
@BindView(R.id.mediaDetailDesc)
|
||||
TextView desc;
|
||||
HtmlTextView desc;
|
||||
@BindView(R.id.mediaDetailAuthor)
|
||||
TextView author;
|
||||
@BindView(R.id.mediaDetailLicense)
|
||||
|
|
@ -135,8 +130,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
|
||||
private ViewTreeObserver.OnScrollChangedListener scrollListener;
|
||||
private DataSetObserver dataObserver;
|
||||
private AsyncTask<Void, Void, Boolean> detailFetchTask;
|
||||
private LicenseList licenseList;
|
||||
|
||||
//Had to make this class variable, to implement various onClicks, which access the media, also I fell why make separate variables when one can serve the purpose
|
||||
private Media media;
|
||||
|
|
@ -198,8 +191,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
authorLayout.setVisibility(GONE);
|
||||
}
|
||||
|
||||
licenseList = new LicenseList(getActivity());
|
||||
|
||||
// Progressively darken the image in the background when we scroll detail pane up
|
||||
scrollListener = this::updateTheDarkness;
|
||||
view.getViewTreeObserver().addOnScrollChangedListener(scrollListener);
|
||||
|
|
@ -269,62 +260,19 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
private void displayMediaDetails() {
|
||||
//Always load image from Internet to allow viewing the desc, license, and cats
|
||||
image.setMedia(media);
|
||||
|
||||
// FIXME: For transparent images
|
||||
// FIXME: keep the spinner going while we load data
|
||||
// FIXME: cache this data
|
||||
// Load image metadata: desc, license, categories
|
||||
detailFetchTask = new AsyncTask<Void, Void, Boolean>() {
|
||||
private MediaDataExtractor extractor;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
extractor = mediaDataExtractorProvider.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
// Local files have no filename yet
|
||||
if (media.getFilename() == null) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
try {
|
||||
extractor.fetch(media.getFilename(), licenseList);
|
||||
return Boolean.TRUE;
|
||||
} catch (IOException e) {
|
||||
Timber.d(e);
|
||||
}
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean success) {
|
||||
detailFetchTask = null;
|
||||
if (!isAdded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
extractor.fill(media);
|
||||
setTextFields(media);
|
||||
} else {
|
||||
Timber.d("Failed to load photo details.");
|
||||
}
|
||||
}
|
||||
};
|
||||
detailFetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
||||
title.setText(media.getDisplayTitle());
|
||||
desc.setText(""); // fill in from network...
|
||||
license.setText(""); // fill in from network...
|
||||
desc.setHtmlText(media.getDescription());
|
||||
license.setText(media.getLicense());
|
||||
|
||||
Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::setTextFields);
|
||||
compositeDisposable.add(disposable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (detailFetchTask != null) {
|
||||
detailFetchTask.cancel(true);
|
||||
detailFetchTask = null;
|
||||
}
|
||||
if (layoutListener != null && getView() != null) {
|
||||
getView().getViewTreeObserver().removeGlobalOnLayoutListener(layoutListener); // old Android was on crack. CRACK IS WHACK
|
||||
layoutListener = null;
|
||||
|
|
@ -341,7 +289,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
}
|
||||
|
||||
private void setTextFields(Media media) {
|
||||
desc.setText(prettyDescription(media));
|
||||
this.media = media;
|
||||
desc.setHtmlText(prettyDescription(media));
|
||||
license.setText(prettyLicense(media));
|
||||
coordinates.setText(prettyCoordinates(media));
|
||||
uploadedDate.setText(prettyUploadedDate(media));
|
||||
|
|
@ -369,16 +318,11 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
|
||||
@OnClick(R.id.mediaDetailLicense)
|
||||
public void onMediaDetailLicenceClicked(){
|
||||
String url = licenseLink(media);
|
||||
String url = media.getLicenseUrl();
|
||||
if (!StringUtils.isNullOrWhiteSpace(url) && getActivity() != null) {
|
||||
Utils.handleWebUrl(getActivity(), Uri.parse(url));
|
||||
} else {
|
||||
if (isCategoryImage) {
|
||||
Timber.d("Unable to fetch license URL for %s", media.getLicense());
|
||||
} else {
|
||||
Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT);
|
||||
toast.show();
|
||||
}
|
||||
viewUtil.showShortToast(getActivity(), getString(R.string.null_url));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -425,18 +369,13 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
final EditText input = new EditText(getActivity());
|
||||
alert.setView(input);
|
||||
input.requestFocus();
|
||||
alert.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
String reason = input.getText().toString();
|
||||
alert.setPositiveButton(R.string.ok, (dialog1, whichButton) -> {
|
||||
String reason = input.getText().toString();
|
||||
|
||||
DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
|
||||
deleteTask.execute();
|
||||
enableDeleteButton(false);
|
||||
}
|
||||
deleteHelper.makeDeletion(getContext(), media, reason);
|
||||
enableDeleteButton(false);
|
||||
});
|
||||
alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
}
|
||||
alert.setNegativeButton(R.string.cancel, (dialog12, whichButton) -> {
|
||||
});
|
||||
AlertDialog d = alert.create();
|
||||
input.addTextChangedListener(new TextWatcher() {
|
||||
|
|
@ -469,13 +408,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
@SuppressLint("CheckResult")
|
||||
private void onDeleteClicked(Spinner spinner) {
|
||||
String reason = spinner.getSelectedItem().toString();
|
||||
Single<String> deletionReason = reasonBuilder.getReason(media, reason);
|
||||
compositeDisposable.add(deletionReason
|
||||
Single<Boolean> resultSingle = reasonBuilder.getReason(media, reason)
|
||||
.flatMap(reasonString -> deleteHelper.makeDeletion(getContext(), media, reason));
|
||||
compositeDisposable.add(resultSingle
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(s -> {
|
||||
DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
|
||||
deleteTask.execute();
|
||||
isDeleted = true;
|
||||
enableDeleteButton(false);
|
||||
}));
|
||||
|
|
@ -570,12 +508,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
if (licenseKey == null || licenseKey.equals("")) {
|
||||
return getString(R.string.detail_license_empty);
|
||||
}
|
||||
License licenseObj = licenseList.get(licenseKey);
|
||||
if (licenseObj == null) {
|
||||
return licenseKey;
|
||||
} else {
|
||||
return licenseObj.getName();
|
||||
}
|
||||
return licenseKey;
|
||||
}
|
||||
|
||||
private String prettyUploadedDate(Media media) {
|
||||
|
|
@ -607,19 +540,4 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
nominatedForDeletion.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable
|
||||
String licenseLink(Media media) {
|
||||
String licenseKey = media.getLicense();
|
||||
if (licenseKey == null || licenseKey.equals("")) {
|
||||
return null;
|
||||
}
|
||||
License licenseObj = licenseList.get(licenseKey);
|
||||
if (licenseObj == null) {
|
||||
return null;
|
||||
} else {
|
||||
return licenseObj.getUrl(Locale.getDefault().getLanguage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import fr.free.nrw.commons.mwapi.model.RecentChange;
|
||||
|
||||
public class RecentChangesImageUtils {
|
||||
|
||||
private static final String[] imageExtensions = new String[]
|
||||
{".jpg", ".jpeg", ".png"};
|
||||
|
||||
@Nullable
|
||||
public static String findImageInRecentChanges(List<RecentChange> recentChanges) {
|
||||
String imageTitle;
|
||||
Random r = new Random();
|
||||
int count = recentChanges.size();
|
||||
// Build a range array
|
||||
int[] randomIndexes = new int[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
randomIndexes[i] = i;
|
||||
}
|
||||
// Then shuffle it
|
||||
for (int i = 0; i < count; i++) {
|
||||
int swapIndex = r.nextInt(count);
|
||||
int temp = randomIndexes[i];
|
||||
randomIndexes[i] = randomIndexes[swapIndex];
|
||||
randomIndexes[swapIndex] = temp;
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
int randomIndex = randomIndexes[i];
|
||||
RecentChange recentChange = recentChanges.get(randomIndex);
|
||||
if (recentChange.getType().equals("log") && !recentChange.getOldRevisionId().equals("0")) {
|
||||
// For log entries, we only want ones where old_revid is zero, indicating a new file
|
||||
continue;
|
||||
}
|
||||
imageTitle = recentChange.getTitle();
|
||||
|
||||
for (String imageExtension : imageExtensions) {
|
||||
if (imageTitle.toLowerCase().endsWith(imageExtension)) {
|
||||
return imageTitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,10 @@ public class ExtMetadata {
|
|||
@SuppressWarnings("unused") @SerializedName("CommonsMetadataExtension") @Nullable private Values commonsMetadataExtension;
|
||||
@SuppressWarnings("unused") @SerializedName("Categories") @Nullable private Values categories;
|
||||
@SuppressWarnings("unused") @SerializedName("Assessments") @Nullable private Values assessments;
|
||||
@SuppressWarnings("unused") @SerializedName("ImageDescription") @Nullable private Values imageDescription;
|
||||
@SuppressWarnings("unused")
|
||||
@SerializedName("ImageDescription")
|
||||
@Nullable
|
||||
private Values imageDescription;
|
||||
@SuppressWarnings("unused") @SerializedName("GPSLatitude") @Nullable private Values gpsLatitude;
|
||||
@SuppressWarnings("unused") @SerializedName("GPSLongitude") @Nullable private Values gpsLongitude;
|
||||
@SuppressWarnings("unused") @SerializedName("DateTimeOriginal") @Nullable private Values dateTimeOriginal;
|
||||
|
|
@ -49,7 +52,8 @@ public class ExtMetadata {
|
|||
return license != null ? license : new Values();
|
||||
}
|
||||
|
||||
@NonNull public Values imageDescription() {
|
||||
@NonNull
|
||||
public Values imageDescription() {
|
||||
return imageDescription != null ? imageDescription : new Values();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -246,11 +246,11 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean pageExists(String pageName) throws IOException {
|
||||
return Double.parseDouble( api.action("query")
|
||||
public Single<Boolean> pageExists(String pageName) {
|
||||
return Single.fromCallable(() -> Double.parseDouble(api.action("query")
|
||||
.param("titles", pageName)
|
||||
.get()
|
||||
.getString("/api/query/pages/page/@_idx")) != -1;
|
||||
.getString("/api/query/pages/page/@_idx")) != -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -305,42 +305,44 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String findThumbnailByFilename(String filename) throws IOException {
|
||||
return api.action("query")
|
||||
public Single<String> findThumbnailByFilename(String filename) {
|
||||
return Single.fromCallable(() -> api.action("query")
|
||||
.param("format", "xml")
|
||||
.param("prop", "imageinfo")
|
||||
.param("iiprop", "url")
|
||||
.param("iiurlwidth", THUMB_SIZE)
|
||||
.param("titles", filename)
|
||||
.get()
|
||||
.getString("/api/query/pages/page/imageinfo/ii/@thumburl");
|
||||
.getString("/api/query/pages/page/imageinfo/ii/@thumburl"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parseWikicode(String source) throws IOException {
|
||||
return api.action("flow-parsoid-utils")
|
||||
public Single<String> parseWikicode(String source) {
|
||||
return Single.fromCallable(() -> api.action("flow-parsoid-utils")
|
||||
.param("from", "wikitext")
|
||||
.param("to", "html")
|
||||
.param("content", source)
|
||||
.param("title", "Main_page")
|
||||
.get()
|
||||
.getString("/api/flow-parsoid-utils/@content");
|
||||
.getString("/api/flow-parsoid-utils/@content"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public MediaResult fetchMediaByFilename(String filename) throws IOException {
|
||||
CustomApiResult apiResult = api.action("query")
|
||||
.param("prop", "revisions")
|
||||
.param("titles", filename)
|
||||
.param("rvprop", "content")
|
||||
.param("rvlimit", 1)
|
||||
.param("rvgeneratexml", 1)
|
||||
.get();
|
||||
public Single<MediaResult> fetchMediaByFilename(String filename) {
|
||||
return Single.fromCallable(() -> {
|
||||
CustomApiResult apiResult = api.action("query")
|
||||
.param("prop", "revisions")
|
||||
.param("titles", filename)
|
||||
.param("rvprop", "content")
|
||||
.param("rvlimit", 1)
|
||||
.param("rvgeneratexml", 1)
|
||||
.get();
|
||||
|
||||
return new MediaResult(
|
||||
apiResult.getString("/api/query/pages/page/revisions/rev"),
|
||||
apiResult.getString("/api/query/pages/page/revisions/rev/@parsetree"));
|
||||
return new MediaResult(
|
||||
apiResult.getString("/api/query/pages/page/revisions/rev"),
|
||||
apiResult.getString("/api/query/pages/page/revisions/rev/@parsetree"));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ public interface MediaWikiApi {
|
|||
|
||||
boolean fileExistsWithName(String fileName) throws IOException;
|
||||
|
||||
boolean pageExists(String pageName) throws IOException;
|
||||
Single<Boolean> pageExists(String pageName);
|
||||
|
||||
String findThumbnailByFilename(String filename) throws IOException;
|
||||
Single<String> findThumbnailByFilename(String filename);
|
||||
|
||||
boolean logEvents(LogBuilder[] logBuilders);
|
||||
|
||||
|
|
@ -69,10 +69,10 @@ public interface MediaWikiApi {
|
|||
@Nullable
|
||||
boolean addWikidataEditTag(String revisionId) throws IOException;
|
||||
|
||||
String parseWikicode(String source) throws IOException;
|
||||
Single<String> parseWikicode(String source);
|
||||
|
||||
@NonNull
|
||||
MediaResult fetchMediaByFilename(String filename) throws IOException;
|
||||
Single<MediaResult> fetchMediaByFilename(String filename);
|
||||
|
||||
@NonNull
|
||||
Observable<String> searchCategories(String filterValue, int searchCatsLimit);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ import okhttp3.Request;
|
|||
import okhttp3.Response;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Test methods in ok http api client
|
||||
*/
|
||||
@Singleton
|
||||
public class OkHttpJsonApiClient {
|
||||
|
||||
|
|
@ -219,18 +222,30 @@ public class OkHttpJsonApiClient {
|
|||
@Nullable
|
||||
public Single<Media> getPictureOfTheDay() {
|
||||
String template = "Template:Potd/" + DateUtils.getCurrentDate();
|
||||
return getMedia(template, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches Media object from the imageInfo API
|
||||
*
|
||||
* @param titles the tiles to be searched for. Can be filename or template name
|
||||
* @param useGenerator specifies if a image generator parameter needs to be passed or not
|
||||
* @return
|
||||
*/
|
||||
public Single<Media> getMedia(String titles, boolean useGenerator) {
|
||||
HttpUrl.Builder urlBuilder = HttpUrl
|
||||
.parse(commonsBaseUrl)
|
||||
.newBuilder()
|
||||
.addQueryParameter("action", "query")
|
||||
.addQueryParameter("generator", "images")
|
||||
.addQueryParameter("format", "json")
|
||||
.addQueryParameter("titles", template)
|
||||
.addQueryParameter("prop", "imageinfo")
|
||||
.addQueryParameter("iiprop", "url|extmetadata");
|
||||
.addQueryParameter("titles", titles);
|
||||
|
||||
if (useGenerator) {
|
||||
urlBuilder.addQueryParameter("generator", "images");
|
||||
}
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(urlBuilder.build())
|
||||
.url(appendMediaProperties(urlBuilder).build())
|
||||
.build();
|
||||
|
||||
return Single.fromCallable(() -> {
|
||||
|
|
@ -238,17 +253,38 @@ public class OkHttpJsonApiClient {
|
|||
if (response.body() != null && response.isSuccessful()) {
|
||||
String json = response.body().string();
|
||||
MwQueryResponse mwQueryPage = gson.fromJson(json, MwQueryResponse.class);
|
||||
return Media.from(mwQueryPage.query().firstPage());
|
||||
if (mwQueryPage.success() && mwQueryPage.query().firstPage() != null) {
|
||||
return Media.from(mwQueryPage.query().firstPage());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever imageInfo is fetched, these common properties can be specified for the API call
|
||||
* https://www.mediawiki.org/wiki/API:Imageinfo
|
||||
* @param builder
|
||||
* @return
|
||||
*/
|
||||
private HttpUrl.Builder appendMediaProperties(HttpUrl.Builder builder) {
|
||||
builder.addQueryParameter("prop", "imageinfo")
|
||||
.addQueryParameter("iiprop", "url|extmetadata")
|
||||
.addQueryParameter("iiextmetadatafilter", "DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl");
|
||||
|
||||
String language = Locale.getDefault().getLanguage();
|
||||
if (!StringUtils.isNullOrWhiteSpace(language)) {
|
||||
builder.addQueryParameter("iiextmetadatalanguage", language);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method takes the keyword and queryType as input and returns a list of Media objects filtered using image generator query
|
||||
* It uses the generator query API to get the images searched using a query, 10 at a time.
|
||||
* @param queryType queryType can be "search" OR "category"
|
||||
* @param keyword
|
||||
* @param keyword the search keyword. Can be either category name or search query
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
|
|
@ -294,29 +330,10 @@ public class OkHttpJsonApiClient {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever imageInfo is fetched, these common properties can be specified for the API call
|
||||
* https://www.mediawiki.org/wiki/API:Imageinfo
|
||||
* @param builder
|
||||
* @return
|
||||
*/
|
||||
private HttpUrl.Builder appendMediaProperties(HttpUrl.Builder builder) {
|
||||
builder.addQueryParameter("prop", "imageinfo")
|
||||
.addQueryParameter("iiprop", "url|extmetadata")
|
||||
.addQueryParameter("iiextmetadatafilter", "DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName");
|
||||
|
||||
String language = Locale.getDefault().getLanguage();
|
||||
if (!StringUtils.isNullOrWhiteSpace(language)) {
|
||||
builder.addQueryParameter("iiextmetadatalanguage", language);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append params for search query.
|
||||
* @param query
|
||||
* @param urlBuilder
|
||||
* @param query the search query to be sent to the API
|
||||
* @param urlBuilder builder for HttpUrl
|
||||
*/
|
||||
private void appendSearchParam(String query, HttpUrl.Builder urlBuilder) {
|
||||
urlBuilder.addQueryParameter("generator", "search")
|
||||
|
|
@ -340,6 +357,11 @@ public class OkHttpJsonApiClient {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append parameters for category image generator
|
||||
* @param categoryName name of the category
|
||||
* @param urlBuilder HttpUrl builder
|
||||
*/
|
||||
private void appendCategoryParams(String categoryName, HttpUrl.Builder urlBuilder) {
|
||||
urlBuilder.addQueryParameter("generator", "categorymembers")
|
||||
.addQueryParameter("gcmtype", "file")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
|
||||
import static androidx.core.app.NotificationCompat.DEFAULT_ALL;
|
||||
import static androidx.core.app.NotificationCompat.PRIORITY_HIGH;
|
||||
|
||||
/**
|
||||
* Helper class that can be used to build a generic notification
|
||||
* Going forward all notifications should be built using this helper class
|
||||
*/
|
||||
@Singleton
|
||||
public class NotificationHelper {
|
||||
|
||||
public static final int NOTIFICATION_DELETE = 1;
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
private NotificationCompat.Builder notificationBuilder;
|
||||
|
||||
@Inject
|
||||
public NotificationHelper(Context context) {
|
||||
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationBuilder = new NotificationCompat
|
||||
.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)
|
||||
.setOnlyAlertOnce(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public interface to build and show a notification in the notification bar
|
||||
* @param context passed context
|
||||
* @param notificationTitle title of the notification
|
||||
* @param notificationMessage message to be displayed in the notification
|
||||
* @param notificationId the notificationID
|
||||
* @param intent the intent to be fired when the notification is clicked
|
||||
*/
|
||||
public void showNotification(Context context,
|
||||
String notificationTitle,
|
||||
String notificationMessage,
|
||||
int notificationId,
|
||||
Intent intent) {
|
||||
|
||||
notificationBuilder.setDefaults(DEFAULT_ALL)
|
||||
.setContentTitle(notificationTitle)
|
||||
.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(notificationMessage))
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setPriority(PRIORITY_HIGH);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
notificationBuilder.setContentIntent(pendingIntent);
|
||||
notificationManager.notify(notificationId, notificationBuilder.build());
|
||||
}
|
||||
}
|
||||
|
|
@ -25,20 +25,18 @@ import fr.free.nrw.commons.Media;
|
|||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.auth.AuthenticatedActivity;
|
||||
import fr.free.nrw.commons.mwapi.MediaResult;
|
||||
import fr.free.nrw.commons.delete.DeleteHelper;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ReviewActivity extends AuthenticatedActivity {
|
||||
|
||||
public ReviewPagerAdapter reviewPagerAdapter;
|
||||
public ReviewController reviewController;
|
||||
@BindView(R.id.reviewPagerIndicator)
|
||||
public CirclePageIndicator pagerIndicator;
|
||||
@BindView(R.id.toolbar)
|
||||
|
|
@ -57,8 +55,16 @@ public class ReviewActivity extends AuthenticatedActivity {
|
|||
ProgressBar progressBar;
|
||||
@BindView(R.id.imageCaption)
|
||||
TextView imageCaption;
|
||||
|
||||
public ReviewPagerAdapter reviewPagerAdapter;
|
||||
public ReviewController reviewController;
|
||||
|
||||
@Inject
|
||||
MediaWikiApi mwApi;
|
||||
@Inject
|
||||
ReviewHelper reviewHelper;
|
||||
@Inject
|
||||
DeleteHelper deleteHelper;
|
||||
|
||||
/**
|
||||
* Consumers should be simply using this method to use this activity.
|
||||
|
|
@ -70,8 +76,7 @@ public class ReviewActivity extends AuthenticatedActivity {
|
|||
Intent reviewActivity = new Intent(context, ReviewActivity.class);
|
||||
context.startActivity(reviewActivity);
|
||||
}
|
||||
@Inject
|
||||
ReviewHelper reviewHelper;
|
||||
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
|
||||
|
|
@ -91,7 +96,7 @@ public class ReviewActivity extends AuthenticatedActivity {
|
|||
ButterKnife.bind(this);
|
||||
initDrawer();
|
||||
|
||||
reviewController = new ReviewController();
|
||||
reviewController = new ReviewController(deleteHelper, this);
|
||||
|
||||
reviewPagerAdapter = new ReviewPagerAdapter(getSupportFragmentManager());
|
||||
reviewPager.setAdapter(reviewPagerAdapter);
|
||||
|
|
@ -134,15 +139,15 @@ public class ReviewActivity extends AuthenticatedActivity {
|
|||
progressBar.setVisibility(View.GONE);
|
||||
}));
|
||||
reviewPager.setCurrentItem(0);
|
||||
compositeDisposable.add(Observable.fromCallable(() -> {
|
||||
MediaResult media = mwApi.fetchMediaByFilename("File:" + fileName);
|
||||
return MediaDataExtractorUtil.extractCategories(media.getWikiSource());
|
||||
})
|
||||
|
||||
Disposable disposable = mwApi.fetchMediaByFilename("File:" + fileName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::updateCategories, this::categoryFetchError));
|
||||
|
||||
|
||||
.subscribe(mediaResult -> {
|
||||
ArrayList<String> categories = MediaDataExtractorUtil.extractCategories(mediaResult.getWikiSource());
|
||||
updateCategories(categories);
|
||||
}, this::categoryFetchError);
|
||||
compositeDisposable.add(disposable);
|
||||
}
|
||||
|
||||
private void categoryFetchError(Throwable throwable) {
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@ import javax.inject.Singleton;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.delete.DeleteTask;
|
||||
import fr.free.nrw.commons.delete.DeleteHelper;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.media.model.MwQueryPage;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
|
|
@ -44,6 +45,17 @@ public class ReviewController {
|
|||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
private final DeleteHelper deleteHelper;
|
||||
|
||||
private ViewPager viewPager;
|
||||
private ReviewActivity reviewActivity;
|
||||
|
||||
ReviewController(DeleteHelper deleteHelper, Context context) {
|
||||
this.deleteHelper = deleteHelper;
|
||||
reviewActivity = (ReviewActivity) context;
|
||||
viewPager = ((ReviewActivity) context).reviewPager;
|
||||
}
|
||||
|
||||
public void onImageRefreshed(String fileName) {
|
||||
this.fileName = fileName;
|
||||
media = new Media("File:" + fileName);
|
||||
|
|
@ -54,15 +66,24 @@ public class ReviewController {
|
|||
ReviewController.categories = categories;
|
||||
}
|
||||
|
||||
public void swipeToNext() {
|
||||
int nextPos = viewPager.getCurrentItem() + 1;
|
||||
if (nextPos <= 3) {
|
||||
viewPager.setCurrentItem(nextPos);
|
||||
} else {
|
||||
reviewActivity.runRandomizer();
|
||||
}
|
||||
}
|
||||
|
||||
public void reportSpam(@NonNull Activity activity) {
|
||||
DeleteTask.askReasonAndExecute(new Media("File:" + fileName),
|
||||
deleteHelper.askReasonAndExecute(new Media("File:" + fileName),
|
||||
activity,
|
||||
activity.getString(R.string.review_spam_report_question),
|
||||
activity.getString(R.string.review_spam_report_problem));
|
||||
activity.getResources().getString(R.string.review_spam_report_question),
|
||||
activity.getResources().getString(R.string.review_spam_report_problem));
|
||||
}
|
||||
|
||||
public void reportPossibleCopyRightViolation(@NonNull Activity activity) {
|
||||
DeleteTask.askReasonAndExecute(new Media("File:" + fileName),
|
||||
deleteHelper.askReasonAndExecute(new Media("File:" + fileName),
|
||||
activity,
|
||||
activity.getResources().getString(R.string.review_c_violation_report_question),
|
||||
activity.getResources().getString(R.string.review_c_violation_report_problem));
|
||||
|
|
|
|||
|
|
@ -1,19 +1,26 @@
|
|||
package fr.free.nrw.commons.review;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import androidx.core.util.Pair;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.media.RecentChangesImageUtils;
|
||||
import fr.free.nrw.commons.media.model.MwQueryPage;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
import fr.free.nrw.commons.mwapi.model.RecentChange;
|
||||
import io.reactivex.Single;
|
||||
|
||||
@Singleton
|
||||
public class ReviewHelper {
|
||||
private static final int MAX_RANDOM_TRIES = 5;
|
||||
|
||||
private static final String[] imageExtensions = new String[]{".jpg", ".jpeg", ".png"};
|
||||
|
||||
private final OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
private final MediaWikiApi mediaWikiApi;
|
||||
|
||||
|
|
@ -23,20 +30,63 @@ public class ReviewHelper {
|
|||
this.mediaWikiApi = mediaWikiApi;
|
||||
}
|
||||
|
||||
public Single<Media> getRandomMedia() {
|
||||
/**
|
||||
* Gets a random media file for review.
|
||||
* - Picks the most recent changes in the last 30 day window
|
||||
* - Picks a random file from those changes
|
||||
* - Checks if the file is nominated for deletion
|
||||
* - Retries upto 5 times for getting a file which is not nominated for deletion
|
||||
* @return
|
||||
*/
|
||||
Single<Media> getRandomMedia() {
|
||||
return okHttpJsonApiClient.getRecentFileChanges()
|
||||
.map(RecentChangesImageUtils::findImageInRecentChanges)
|
||||
.map(title -> {
|
||||
boolean pageExists = mediaWikiApi.pageExists("Commons:Deletion_requests/" + title);
|
||||
if (!pageExists) {
|
||||
title = title.replace("File:", "");
|
||||
return new Media(title);
|
||||
.map(this::findImageInRecentChanges)
|
||||
.flatMap(title -> mediaWikiApi.pageExists("Commons:Deletion_requests/" + title)
|
||||
.map(pageExists -> new Pair<>(title, pageExists)))
|
||||
.map((Pair<String, Boolean> pair) -> {
|
||||
if (pair.second) {
|
||||
return new Media(pair.first.replace("File:", ""));
|
||||
}
|
||||
throw new Exception("Page does not exist");
|
||||
}).retry(MAX_RANDOM_TRIES);
|
||||
}
|
||||
|
||||
public Single<MwQueryPage.Revision> getFirstRevisionOfFile(String fileName) {
|
||||
Single<MwQueryPage.Revision> getFirstRevisionOfFile(String fileName) {
|
||||
return okHttpJsonApiClient.getFirstRevisionOfFile(fileName);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String findImageInRecentChanges(List<RecentChange> recentChanges) {
|
||||
String imageTitle;
|
||||
Random r = new Random();
|
||||
int count = recentChanges.size();
|
||||
// Build a range array
|
||||
int[] randomIndexes = new int[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
randomIndexes[i] = i;
|
||||
}
|
||||
// Then shuffle it
|
||||
for (int i = 0; i < count; i++) {
|
||||
int swapIndex = r.nextInt(count);
|
||||
int temp = randomIndexes[i];
|
||||
randomIndexes[i] = randomIndexes[swapIndex];
|
||||
randomIndexes[swapIndex] = temp;
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
int randomIndex = randomIndexes[i];
|
||||
RecentChange recentChange = recentChanges.get(randomIndex);
|
||||
if (recentChange.getType().equals("log") && !recentChange.getOldRevisionId().equals("0")) {
|
||||
// For log entries, we only want ones where old_revid is zero, indicating a new file
|
||||
continue;
|
||||
}
|
||||
imageTitle = recentChange.getTitle();
|
||||
|
||||
for (String imageExtension : imageExtensions) {
|
||||
if (imageTitle.toLowerCase().endsWith(imageExtension)) {
|
||||
return imageTitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import fr.free.nrw.commons.R;
|
|||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.logging.CommonsLogSender;
|
||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import android.content.ServiceConnection;
|
|||
import android.content.res.AssetFileDescriptor;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.IBinder;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.TextUtils;
|
||||
|
|
@ -20,7 +19,6 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
|
@ -32,6 +30,10 @@ import fr.free.nrw.commons.contributions.Contribution;
|
|||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
@Singleton
|
||||
|
|
@ -45,7 +47,6 @@ public class UploadController {
|
|||
void onUploadStarted(Contribution contribution);
|
||||
}
|
||||
|
||||
|
||||
@Inject
|
||||
public UploadController(SessionManager sessionManager,
|
||||
Context context,
|
||||
|
|
@ -133,81 +134,92 @@ public class UploadController {
|
|||
String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
|
||||
contribution.setLicense(license);
|
||||
|
||||
//FIXME: Add permission request here. Only executeAsyncTask if permission has been granted
|
||||
new AsyncTask<Void, Void, Contribution>() {
|
||||
uploadTask(contribution, onComplete);
|
||||
}
|
||||
|
||||
// Fills up missing information about Contributions
|
||||
// Only does things that involve some form of IO
|
||||
// Runs in background thread
|
||||
@Override
|
||||
protected Contribution doInBackground(Void... voids /* stare into you */) {
|
||||
long length;
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
try {
|
||||
if (contribution.getDataLength() <= 0) {
|
||||
Timber.d("UploadController/doInBackground, contribution.getLocalUri():" + contribution.getLocalUri());
|
||||
AssetFileDescriptor assetFileDescriptor = contentResolver
|
||||
.openAssetFileDescriptor(Uri.fromFile(new File(contribution.getLocalUri().getPath())), "r");
|
||||
if (assetFileDescriptor != null) {
|
||||
length = assetFileDescriptor.getLength();
|
||||
if (length == -1) {
|
||||
// Let us find out the long way!
|
||||
length = countBytes(contentResolver
|
||||
.openInputStream(contribution.getLocalUri()));
|
||||
}
|
||||
contribution.setDataLength(length);
|
||||
}
|
||||
/**
|
||||
* Initiates the upload task
|
||||
* @param contribution
|
||||
* @param onComplete
|
||||
* @return
|
||||
*/
|
||||
private Disposable uploadTask(Contribution contribution, ContributionUploadProgress onComplete) {
|
||||
return Single.fromCallable(() -> makeUpload(contribution))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(finalContribution -> onUploadCompleted(finalContribution, onComplete));
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the Contribution object ready to be uploaded
|
||||
* @param contribution
|
||||
* @return
|
||||
*/
|
||||
private Contribution makeUpload(Contribution contribution) {
|
||||
long length;
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
try {
|
||||
if (contribution.getDataLength() <= 0) {
|
||||
Timber.d("UploadController/doInBackground, contribution.getLocalUri():%s", contribution.getLocalUri());
|
||||
AssetFileDescriptor assetFileDescriptor = contentResolver
|
||||
.openAssetFileDescriptor(Uri.fromFile(new File(contribution.getLocalUri().getPath())), "r");
|
||||
if (assetFileDescriptor != null) {
|
||||
length = assetFileDescriptor.getLength();
|
||||
if (length == -1) {
|
||||
// Let us find out the long way!
|
||||
length = countBytes(contentResolver
|
||||
.openInputStream(contribution.getLocalUri()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "IO Exception: ");
|
||||
} catch (NullPointerException e) {
|
||||
Timber.e(e, "Null Pointer Exception: ");
|
||||
} catch (SecurityException e) {
|
||||
Timber.e(e, "Security Exception: ");
|
||||
contribution.setDataLength(length);
|
||||
}
|
||||
|
||||
String mimeType = (String) contribution.getTag("mimeType");
|
||||
Boolean imagePrefix = false;
|
||||
|
||||
if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
|
||||
mimeType = contentResolver.getType(contribution.getLocalUri());
|
||||
}
|
||||
|
||||
if (mimeType != null) {
|
||||
contribution.setTag("mimeType", mimeType);
|
||||
imagePrefix = mimeType.startsWith("image/");
|
||||
Timber.d("MimeType is: %s", mimeType);
|
||||
}
|
||||
|
||||
if (imagePrefix && contribution.getDateCreated() == null) {
|
||||
Timber.d("local uri " + contribution.getLocalUri());
|
||||
Cursor cursor = contentResolver.query(contribution.getLocalUri(),
|
||||
new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null);
|
||||
if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) {
|
||||
cursor.moveToFirst();
|
||||
Date dateCreated = new Date(cursor.getLong(0));
|
||||
Date epochStart = new Date(0);
|
||||
if (dateCreated.equals(epochStart) || dateCreated.before(epochStart)) {
|
||||
// If date is incorrect (1st second of unix time) then set it to the current date
|
||||
dateCreated = new Date();
|
||||
}
|
||||
contribution.setDateCreated(dateCreated);
|
||||
cursor.close();
|
||||
} else {
|
||||
contribution.setDateCreated(new Date());
|
||||
}
|
||||
}
|
||||
return contribution;
|
||||
}
|
||||
} catch (IOException | NullPointerException | SecurityException e) {
|
||||
Timber.e(e, "Exception occurred while uploading image");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Contribution contribution) {
|
||||
super.onPostExecute(contribution);
|
||||
//Starts the upload. If commented out, user can proceed to next Fragment but upload doesn't happen
|
||||
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, contribution);
|
||||
onComplete.onUploadStarted(contribution);
|
||||
String mimeType = (String) contribution.getTag("mimeType");
|
||||
boolean imagePrefix = false;
|
||||
|
||||
if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
|
||||
mimeType = contentResolver.getType(contribution.getLocalUri());
|
||||
}
|
||||
|
||||
if (mimeType != null) {
|
||||
contribution.setTag("mimeType", mimeType);
|
||||
imagePrefix = mimeType.startsWith("image/");
|
||||
Timber.d("MimeType is: %s", mimeType);
|
||||
}
|
||||
|
||||
if (imagePrefix && contribution.getDateCreated() == null) {
|
||||
Timber.d("local uri %s", contribution.getLocalUri());
|
||||
Cursor cursor = contentResolver.query(contribution.getLocalUri(),
|
||||
new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null);
|
||||
if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) {
|
||||
cursor.moveToFirst();
|
||||
Date dateCreated = new Date(cursor.getLong(0));
|
||||
Date epochStart = new Date(0);
|
||||
if (dateCreated.equals(epochStart) || dateCreated.before(epochStart)) {
|
||||
// If date is incorrect (1st second of unix time) then set it to the current date
|
||||
dateCreated = new Date();
|
||||
}
|
||||
contribution.setDateCreated(dateCreated);
|
||||
cursor.close();
|
||||
} else {
|
||||
contribution.setDateCreated(new Date());
|
||||
}
|
||||
}.executeOnExecutor(Executors.newFixedThreadPool(1)); // TODO remove this by using a sensible thread handling strategy
|
||||
}
|
||||
return contribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the contribution object is completely formed, the item is queued to the upload service
|
||||
* @param contribution
|
||||
* @param onComplete
|
||||
*/
|
||||
private void onUploadCompleted(Contribution contribution, ContributionUploadProgress onComplete) {
|
||||
//Starts the upload. If commented out, user can proceed to next Fragment but upload doesn't happen
|
||||
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, contribution);
|
||||
onComplete.onUploadStarted(contribution);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,19 +2,16 @@ package fr.free.nrw.commons.utils;
|
|||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.StringRes;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import android.view.Display;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
public class ViewUtil {
|
||||
|
||||
public static final String SHOWCASE_VIEW_ID_1 = "SHOWCASE_VIEW_ID_1";
|
||||
public static final String SHOWCASE_VIEW_ID_2 = "SHOWCASE_VIEW_ID_2";
|
||||
public static final String SHOWCASE_VIEW_ID_3 = "SHOWCASE_VIEW_ID_3";
|
||||
|
||||
/**
|
||||
* Utility function to show short snack bar
|
||||
* @param view
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package fr.free.nrw.commons.utils;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class ViewUtilWrapper {
|
||||
|
||||
@Inject
|
||||
public ViewUtilWrapper() {
|
||||
|
||||
}
|
||||
|
||||
public void showShortToast(Context context, String text) {
|
||||
ViewUtil.showShortToast(context, text);
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +133,7 @@
|
|||
android:textSize="@dimen/normal_text"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
<fr.free.nrw.commons.ui.widget.HtmlTextView
|
||||
android:id="@+id/mediaDetailDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import fr.free.nrw.commons.mwapi.MediaResult
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||
import io.reactivex.Single
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.InjectMocks
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
/**
|
||||
* Test methods in media data extractor
|
||||
*/
|
||||
class MediaDataExtractorTest {
|
||||
|
||||
@Mock
|
||||
internal var mwApi: MediaWikiApi? = null
|
||||
|
||||
@Mock
|
||||
internal var okHttpJsonApiClient: OkHttpJsonApiClient? = null
|
||||
|
||||
@InjectMocks
|
||||
var mediaDataExtractor: MediaDataExtractor? = null
|
||||
|
||||
/**
|
||||
* Init mocks for test
|
||||
*/
|
||||
@Before
|
||||
@Throws(Exception::class)
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* test method to fetch media details
|
||||
*/
|
||||
@Test
|
||||
fun fetchMediaDetails() {
|
||||
`when`(okHttpJsonApiClient?.getMedia(ArgumentMatchers.anyString(), ArgumentMatchers.anyBoolean()))
|
||||
.thenReturn(Single.just(mock(Media::class.java)))
|
||||
|
||||
`when`(mwApi?.pageExists(ArgumentMatchers.anyString()))
|
||||
.thenReturn(Single.just(true))
|
||||
|
||||
val mediaResult = mock(MediaResult::class.java)
|
||||
`when`(mediaResult.wikiSource).thenReturn("some wiki source")
|
||||
`when`(mwApi?.fetchMediaByFilename(ArgumentMatchers.anyString()))
|
||||
.thenReturn(Single.just(mediaResult))
|
||||
|
||||
`when`(mwApi?.parseWikicode(ArgumentMatchers.anyString()))
|
||||
.thenReturn(Single.just("discussion text"))
|
||||
|
||||
val fetchMediaDetails = mediaDataExtractor?.fetchMediaDetails("test.jpg")?.blockingGet()
|
||||
|
||||
assertTrue(fetchMediaDetails is Media)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package fr.free.nrw.commons.delete
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.Context
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi
|
||||
import fr.free.nrw.commons.notification.NotificationHelper
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper
|
||||
import junit.framework.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.InjectMocks
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
/**
|
||||
* Tests for delete helper
|
||||
*/
|
||||
class DeleteHelperTest {
|
||||
|
||||
@Mock
|
||||
internal var mwApi: MediaWikiApi? = null
|
||||
|
||||
@Mock
|
||||
internal var sessionManager: SessionManager? = null
|
||||
|
||||
@Mock
|
||||
internal var notificationHelper: NotificationHelper? = null
|
||||
|
||||
@Mock
|
||||
internal var context: Context? = null
|
||||
|
||||
@Mock
|
||||
internal var viewUtil: ViewUtilWrapper? = null
|
||||
|
||||
@Mock
|
||||
internal var media: Media? = null
|
||||
|
||||
@InjectMocks
|
||||
var deleteHelper: DeleteHelper? = null
|
||||
|
||||
/**
|
||||
* Init mocks for test
|
||||
*/
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a successful deletion
|
||||
*/
|
||||
@Test
|
||||
fun makeDeletion() {
|
||||
`when`(mwApi?.editToken).thenReturn("token")
|
||||
`when`(sessionManager?.authCookie).thenReturn("Mock cookie")
|
||||
`when`(sessionManager?.currentAccount).thenReturn(Account("TestUser", "Test"))
|
||||
`when`(media?.displayTitle).thenReturn("Test file")
|
||||
`when`(media?.filename).thenReturn("Test file.jpg")
|
||||
|
||||
val makeDeletion = deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet()
|
||||
assertNotNull(makeDeletion)
|
||||
assertTrue(makeDeletion!!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a failed deletion
|
||||
*/
|
||||
@Test
|
||||
fun makeDeletionForNullToken() {
|
||||
`when`(mwApi?.editToken).thenReturn(null)
|
||||
`when`(sessionManager?.authCookie).thenReturn("Mock cookie")
|
||||
`when`(sessionManager?.currentAccount).thenReturn(Account("TestUser", "Test"))
|
||||
`when`(media?.displayTitle).thenReturn("Test file")
|
||||
`when`(media?.filename).thenReturn("Test file.jpg")
|
||||
|
||||
val makeDeletion = deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet()
|
||||
assertNotNull(makeDeletion)
|
||||
assertFalse(makeDeletion!!)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,11 @@ package fr.free.nrw.commons.mwapi
|
|||
|
||||
import com.google.gson.Gson
|
||||
import fr.free.nrw.commons.BuildConfig
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.TestCommonsApplication
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient.mapType
|
||||
import fr.free.nrw.commons.utils.DateUtils
|
||||
import junit.framework.Assert.assertEquals
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
|
|
@ -21,7 +23,11 @@ import org.mockito.Mockito.`when`
|
|||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import java.net.URLDecoder
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Mock web server based tests for ok http json api client
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(constants = BuildConfig::class, sdk = [23], application = TestCommonsApplication::class)
|
||||
class OkHttpJsonApiClientTest {
|
||||
|
|
@ -34,6 +40,10 @@ class OkHttpJsonApiClientTest {
|
|||
private lateinit var sharedPreferences: JsonKvStore
|
||||
private lateinit var okHttpClient: OkHttpClient
|
||||
|
||||
/**
|
||||
* - make instances of mock web server
|
||||
* - create instance of OkHttpJsonApiClient
|
||||
*/
|
||||
@Before
|
||||
fun setUp() {
|
||||
server = MockWebServer()
|
||||
|
|
@ -49,17 +59,29 @@ class OkHttpJsonApiClientTest {
|
|||
testObject = OkHttpJsonApiClient(okHttpClient, HttpUrl.get(toolsForgeUrl), sparqlUrl, campaignsUrl, serverUrl, sharedPreferences, Gson())
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown server after tests
|
||||
*/
|
||||
@After
|
||||
fun teardown() {
|
||||
server.shutdown()
|
||||
toolsForgeServer.shutdown()
|
||||
sparqlServer.shutdown()
|
||||
campaignsServer.shutdown()
|
||||
}
|
||||
|
||||
/**
|
||||
* Test response for category images
|
||||
*/
|
||||
@Test
|
||||
fun getCategoryImages() {
|
||||
server.enqueue(getFirstPageOfImages())
|
||||
testFirstPageQuery()
|
||||
}
|
||||
|
||||
/**
|
||||
* test paginated response for category images
|
||||
*/
|
||||
@Test
|
||||
fun getCategoryImagesWithContinue() {
|
||||
server.enqueue(getFirstPageOfImages())
|
||||
|
|
@ -85,19 +107,25 @@ class OkHttpJsonApiClientTest {
|
|||
Assert.assertEquals("gcmcontinue||", body["continue"])
|
||||
Assert.assertEquals("imageinfo", body["prop"])
|
||||
Assert.assertEquals("url|extmetadata", body["iiprop"])
|
||||
Assert.assertEquals("DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName", body["iiextmetadatafilter"])
|
||||
Assert.assertEquals("DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl", body["iiextmetadatafilter"])
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(categoryImagesContinued.size, 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test response for search images
|
||||
*/
|
||||
@Test
|
||||
fun getSearchImages() {
|
||||
server.enqueue(getFirstPageOfImages())
|
||||
testFirstPageSearchQuery()
|
||||
}
|
||||
|
||||
/**
|
||||
* Test response for paginated search
|
||||
*/
|
||||
@Test
|
||||
fun getSearchImagesWithContinue() {
|
||||
server.enqueue(getFirstPageOfSearchImages())
|
||||
|
|
@ -123,13 +151,87 @@ class OkHttpJsonApiClientTest {
|
|||
Assert.assertEquals("gsroffset||", body["continue"])
|
||||
Assert.assertEquals("imageinfo", body["prop"])
|
||||
Assert.assertEquals("url|extmetadata", body["iiprop"])
|
||||
Assert.assertEquals("DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName", body["iiextmetadatafilter"])
|
||||
Assert.assertEquals("DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl", body["iiextmetadatafilter"])
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(categoryImagesContinued.size, 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test response for getting media without generator
|
||||
*/
|
||||
@Test
|
||||
fun getMedia() {
|
||||
server.enqueue(getMediaList("", "", "", 1))
|
||||
|
||||
val media = testObject.getMedia("Test.jpg", false)!!.blockingGet()
|
||||
|
||||
assertBasicRequestParameters(server, "GET").let { request ->
|
||||
parseQueryParams(request).let { body ->
|
||||
Assert.assertEquals("json", body["format"])
|
||||
Assert.assertEquals("query", body["action"])
|
||||
Assert.assertEquals("Test.jpg", body["titles"])
|
||||
Assert.assertEquals("imageinfo", body["prop"])
|
||||
Assert.assertEquals("url|extmetadata", body["iiprop"])
|
||||
Assert.assertEquals("DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl", body["iiextmetadatafilter"])
|
||||
}
|
||||
}
|
||||
|
||||
assert(media is Media)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test response for getting media with generator
|
||||
* Equivalent of testing POTD
|
||||
*/
|
||||
@Test
|
||||
fun getImageWithGenerator() {
|
||||
val template = "Template:Potd/" + DateUtils.getCurrentDate()
|
||||
server.enqueue(getMediaList("", "", "", 1))
|
||||
|
||||
val media = testObject.getMedia(template, true)!!.blockingGet()
|
||||
|
||||
assertBasicRequestParameters(server, "GET").let { request ->
|
||||
parseQueryParams(request).let { body ->
|
||||
Assert.assertEquals("json", body["format"])
|
||||
Assert.assertEquals("query", body["action"])
|
||||
Assert.assertEquals(template, body["titles"])
|
||||
Assert.assertEquals("images", body["generator"])
|
||||
Assert.assertEquals("imageinfo", body["prop"])
|
||||
Assert.assertEquals("url|extmetadata", body["iiprop"])
|
||||
Assert.assertEquals("DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl", body["iiextmetadatafilter"])
|
||||
}
|
||||
}
|
||||
|
||||
assert(media is Media)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test response for getting picture of the day
|
||||
*/
|
||||
@Test
|
||||
fun getPictureOfTheDay() {
|
||||
val template = "Template:Potd/" + DateUtils.getCurrentDate()
|
||||
server.enqueue(getMediaList("", "", "", 1))
|
||||
|
||||
val media = testObject.pictureOfTheDay?.blockingGet()
|
||||
|
||||
assertBasicRequestParameters(server, "GET").let { request ->
|
||||
parseQueryParams(request).let { body ->
|
||||
Assert.assertEquals("json", body["format"])
|
||||
Assert.assertEquals("query", body["action"])
|
||||
Assert.assertEquals(template, body["titles"])
|
||||
Assert.assertEquals("images", body["generator"])
|
||||
Assert.assertEquals("imageinfo", body["prop"])
|
||||
Assert.assertEquals("url|extmetadata", body["iiprop"])
|
||||
Assert.assertEquals("DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl", body["iiextmetadatafilter"])
|
||||
}
|
||||
}
|
||||
|
||||
assert(media is Media)
|
||||
}
|
||||
|
||||
private fun testFirstPageSearchQuery() {
|
||||
val categoryImages = testObject.getMediaList("search", "Watercraft moored off shore")!!.blockingGet()
|
||||
|
||||
|
|
@ -144,14 +246,14 @@ class OkHttpJsonApiClientTest {
|
|||
Assert.assertEquals("Watercraft moored off shore", body["gsrsearch"])
|
||||
Assert.assertEquals("imageinfo", body["prop"])
|
||||
Assert.assertEquals("url|extmetadata", body["iiprop"])
|
||||
Assert.assertEquals("DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName", body["iiextmetadatafilter"])
|
||||
Assert.assertEquals("DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl", body["iiextmetadatafilter"])
|
||||
}
|
||||
}
|
||||
assertEquals(categoryImages.size, 2)
|
||||
}
|
||||
|
||||
private fun testFirstPageQuery() {
|
||||
val categoryImages = testObject.getMediaList("category", "Watercraft moored off shore")!!.blockingGet()
|
||||
val categoryImages = testObject.getMediaList("category", "Watercraft moored off shore")?.blockingGet()
|
||||
|
||||
assertBasicRequestParameters(server, "GET").let { request ->
|
||||
parseQueryParams(request).let { body ->
|
||||
|
|
@ -164,56 +266,85 @@ class OkHttpJsonApiClientTest {
|
|||
Assert.assertEquals("desc", body["gcmdir"])
|
||||
Assert.assertEquals("imageinfo", body["prop"])
|
||||
Assert.assertEquals("url|extmetadata", body["iiprop"])
|
||||
Assert.assertEquals("DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName", body["iiextmetadatafilter"])
|
||||
Assert.assertEquals("DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl", body["iiextmetadatafilter"])
|
||||
}
|
||||
}
|
||||
assertEquals(categoryImages.size, 2)
|
||||
assertEquals(categoryImages?.size, 2)
|
||||
}
|
||||
|
||||
private fun getFirstPageOfImages(): MockResponse {
|
||||
val mockResponse = MockResponse()
|
||||
mockResponse.setResponseCode(200)
|
||||
mockResponse.setBody("{\"batchcomplete\":\"\",\"continue\":{\"gcmcontinue\":\"testvalue\",\"continue\":\"gcmcontinue||\"},\"query\":{\"pages\":{\"4406048\":{\"pageid\":4406048,\"ns\":6,\"title\":\"File:test1.jpg\",\"imagerepository\":\"local\",\"imageinfo\":[{\"url\":\"https://upload.wikimedia.org/test1.jpg\",\"descriptionurl\":\"https://commons.wikimedia.org/wiki/File:test1.jpg\",\"descriptionshorturl\":\"https://commons.wikimedia.org/w/index.php?curid=4406048\",\"extmetadata\":{\"DateTime\":{\"value\":\"2013-04-13 15:12:11\",\"source\":\"mediawiki-metadata\",\"hidden\":\"\"},\"Categories\":{\"value\":\"cat1|cat2\",\"source\":\"commons-categories\",\"hidden\":\"\"},\"Artist\":{\"value\":\"<bdi><a href=\\\"https://en.wikipedia.org/wiki/en:Raphael\\\" class=\\\"extiw\\\" title=\\\"w:en:Raphael\\\">Raphael</a>\\n</bdi>\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511<div style=\\\"display: none;\\\">date QS:P571,+1511-00-00T00:00:00Z/9</div>\",\"source\":\"commons-desc-page\"},\"LicenseShortName\":{\"value\":\"Public domain\",\"source\":\"commons-desc-page\",\"hidden\":\"\"}}}]},\"24259710\":{\"pageid\":24259710,\"ns\":6,\"title\":\"File:test2.jpg\",\"imagerepository\":\"local\",\"imageinfo\":[{\"url\":\"https://upload.wikimedia.org/test2.jpg\",\"descriptionurl\":\"https://commons.wikimedia.org/wiki/File:test2.jpg\",\"descriptionshorturl\":\"https://commons.wikimedia.org/w/index.php?curid=4406048\",\"extmetadata\":{\"DateTime\":{\"value\":\"2013-04-13 15:12:11\",\"source\":\"mediawiki-metadata\",\"hidden\":\"\"},\"Categories\":{\"value\":\"cat3|cat4\",\"source\":\"commons-categories\",\"hidden\":\"\"},\"Artist\":{\"value\":\"<bdi><a href=\\\"https://en.wikipedia.org/wiki/en:Raphael\\\" class=\\\"extiw\\\" title=\\\"w:en:Raphael\\\">Raphael</a>\\n</bdi>\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511<div style=\\\"display: none;\\\">date QS:P571,+1511-00-00T00:00:00Z/9</div>\",\"source\":\"commons-desc-page\"},\"LicenseShortName\":{\"value\":\"Public domain\",\"source\":\"commons-desc-page\",\"hidden\":\"\"}}}]}}}}")
|
||||
return mockResponse
|
||||
return getMediaList("gcmcontinue", "testvalue", "gcmcontinue||", 2)
|
||||
}
|
||||
|
||||
private fun getSecondPageOfImages(): MockResponse {
|
||||
val mockResponse = MockResponse()
|
||||
mockResponse.setResponseCode(200)
|
||||
mockResponse.setBody("{\"batchcomplete\":\"\",\"continue\":{\"gcmcontinue\":\"testvalue2\",\"continue\":\"gcmcontinue||\"},\"query\":{\"pages\":{\"4406048\":{\"pageid\":4406048,\"ns\":6,\"title\":\"File:test3.jpg\",\"imagerepository\":\"local\",\"imageinfo\":[{\"url\":\"https://upload.wikimedia.org/test3.jpg\",\"descriptionurl\":\"https://commons.wikimedia.org/wiki/File:test3.jpg\",\"descriptionshorturl\":\"https://commons.wikimedia.org/w/index.php?curid=4406048\",\"extmetadata\":{\"DateTime\":{\"value\":\"2013-04-13 15:12:11\",\"source\":\"mediawiki-metadata\",\"hidden\":\"\"},\"Categories\":{\"value\":\"cat5|cat6\",\"source\":\"commons-categories\",\"hidden\":\"\"},\"Artist\":{\"value\":\"<bdi><a href=\\\"https://en.wikipedia.org/wiki/en:Raphael\\\" class=\\\"extiw\\\" title=\\\"w:en:Raphael\\\">Raphael</a>\\n</bdi>\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511<div style=\\\"display: none;\\\">date QS:P571,+1511-00-00T00:00:00Z/9</div>\",\"source\":\"commons-desc-page\"},\"LicenseShortName\":{\"value\":\"Public domain\",\"source\":\"commons-desc-page\",\"hidden\":\"\"}}}]},\"24259710\":{\"pageid\":24259710,\"ns\":6,\"title\":\"File:test4.jpg\",\"imagerepository\":\"local\",\"imageinfo\":[{\"url\":\"https://upload.wikimedia.org/test4.jpg\",\"descriptionurl\":\"https://commons.wikimedia.org/wiki/File:test4.jpg\",\"descriptionshorturl\":\"https://commons.wikimedia.org/w/index.php?curid=4406048\",\"extmetadata\":{\"DateTime\":{\"value\":\"2013-04-13 15:12:11\",\"source\":\"mediawiki-metadata\",\"hidden\":\"\"},\"Categories\":{\"value\":\"cat7\",\"source\":\"commons-categories\",\"hidden\":\"\"},\"Artist\":{\"value\":\"<bdi><a href=\\\"https://en.wikipedia.org/wiki/en:Raphael\\\" class=\\\"extiw\\\" title=\\\"w:en:Raphael\\\">Raphael</a>\\n</bdi>\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511<div style=\\\"display: none;\\\">date QS:P571,+1511-00-00T00:00:00Z/9</div>\",\"source\":\"commons-desc-page\"},\"LicenseShortName\":{\"value\":\"Public domain\",\"source\":\"commons-desc-page\",\"hidden\":\"\"}}}]}}}}")
|
||||
return mockResponse
|
||||
return getMediaList("gcmcontinue", "testvalue2", "gcmcontinue||", 2)
|
||||
}
|
||||
|
||||
private fun getFirstPageOfSearchImages(): MockResponse {
|
||||
val mockResponse = MockResponse()
|
||||
mockResponse.setResponseCode(200)
|
||||
mockResponse.setBody("{\"batchcomplete\":\"\",\"continue\":{\"continue\":\"gsroffset||\",\"gsroffset\":\"25\"},\"query\":{\"pages\":{\"4406048\":{\"pageid\":4406048,\"ns\":6,\"title\":\"File:test1.jpg\",\"imagerepository\":\"local\",\"imageinfo\":[{\"url\":\"https://upload.wikimedia.org/test1.jpg\",\"descriptionurl\":\"https://commons.wikimedia.org/wiki/File:test1.jpg\",\"descriptionshorturl\":\"https://commons.wikimedia.org/w/index.php?curid=4406048\",\"extmetadata\":{\"DateTime\":{\"value\":\"2013-04-13 15:12:11\",\"source\":\"mediawiki-metadata\",\"hidden\":\"\"},\"Categories\":{\"value\":\"cat1|cat2\",\"source\":\"commons-categories\",\"hidden\":\"\"},\"Artist\":{\"value\":\"<bdi><a href=\\\"https://en.wikipedia.org/wiki/en:Raphael\\\" class=\\\"extiw\\\" title=\\\"w:en:Raphael\\\">Raphael</a>\\n</bdi>\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511<div style=\\\"display: none;\\\">date QS:P571,+1511-00-00T00:00:00Z/9</div>\",\"source\":\"commons-desc-page\"},\"LicenseShortName\":{\"value\":\"Public domain\",\"source\":\"commons-desc-page\",\"hidden\":\"\"}}}]},\"24259710\":{\"pageid\":24259710,\"ns\":6,\"title\":\"File:test2.jpg\",\"imagerepository\":\"local\",\"imageinfo\":[{\"url\":\"https://upload.wikimedia.org/test2.jpg\",\"descriptionurl\":\"https://commons.wikimedia.org/wiki/File:test2.jpg\",\"descriptionshorturl\":\"https://commons.wikimedia.org/w/index.php?curid=4406048\",\"extmetadata\":{\"DateTime\":{\"value\":\"2013-04-13 15:12:11\",\"source\":\"mediawiki-metadata\",\"hidden\":\"\"},\"Categories\":{\"value\":\"cat3|cat4\",\"source\":\"commons-categories\",\"hidden\":\"\"},\"Artist\":{\"value\":\"<bdi><a href=\\\"https://en.wikipedia.org/wiki/en:Raphael\\\" class=\\\"extiw\\\" title=\\\"w:en:Raphael\\\">Raphael</a>\\n</bdi>\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511<div style=\\\"display: none;\\\">date QS:P571,+1511-00-00T00:00:00Z/9</div>\",\"source\":\"commons-desc-page\"},\"LicenseShortName\":{\"value\":\"Public domain\",\"source\":\"commons-desc-page\",\"hidden\":\"\"}}}]}}}}")
|
||||
return mockResponse
|
||||
return getMediaList("gsroffset", "25", "gsroffset||", 2)
|
||||
}
|
||||
|
||||
private fun getSecondPageOfSearchImages(): MockResponse {
|
||||
return getMediaList("gsroffset", "25", "gsroffset||", 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a MockResponse object which contains a list of media pages
|
||||
*/
|
||||
private fun getMediaList(queryContinueType: String,
|
||||
queryContinueValue: String,
|
||||
continueVal: String,
|
||||
numberOfPages: Int): MockResponse {
|
||||
val mockResponse = MockResponse()
|
||||
mockResponse.setResponseCode(200)
|
||||
mockResponse.setBody("{\"batchcomplete\":\"\",\"continue\":{\"continue\":\"gsroffset||\",\"gsroffset\":\"50\"},\"query\":{\"pages\":{\"4406048\":{\"pageid\":4406048,\"ns\":6,\"title\":\"File:test3.jpg\",\"imagerepository\":\"local\",\"imageinfo\":[{\"url\":\"https://upload.wikimedia.org/test3.jpg\",\"descriptionurl\":\"https://commons.wikimedia.org/wiki/File:test3.jpg\",\"descriptionshorturl\":\"https://commons.wikimedia.org/w/index.php?curid=4406048\",\"extmetadata\":{\"DateTime\":{\"value\":\"2013-04-13 15:12:11\",\"source\":\"mediawiki-metadata\",\"hidden\":\"\"},\"Categories\":{\"value\":\"cat5|cat6\",\"source\":\"commons-categories\",\"hidden\":\"\"},\"Artist\":{\"value\":\"<bdi><a href=\\\"https://en.wikipedia.org/wiki/en:Raphael\\\" class=\\\"extiw\\\" title=\\\"w:en:Raphael\\\">Raphael</a>\\n</bdi>\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511<div style=\\\"display: none;\\\">date QS:P571,+1511-00-00T00:00:00Z/9</div>\",\"source\":\"commons-desc-page\"},\"LicenseShortName\":{\"value\":\"Public domain\",\"source\":\"commons-desc-page\",\"hidden\":\"\"}}}]},\"24259710\":{\"pageid\":24259710,\"ns\":6,\"title\":\"File:test4.jpg\",\"imagerepository\":\"local\",\"imageinfo\":[{\"url\":\"https://upload.wikimedia.org/test4.jpg\",\"descriptionurl\":\"https://commons.wikimedia.org/wiki/File:test4.jpg\",\"descriptionshorturl\":\"https://commons.wikimedia.org/w/index.php?curid=4406048\",\"extmetadata\":{\"DateTime\":{\"value\":\"2013-04-13 15:12:11\",\"source\":\"mediawiki-metadata\",\"hidden\":\"\"},\"Categories\":{\"value\":\"cat7\",\"source\":\"commons-categories\",\"hidden\":\"\"},\"Artist\":{\"value\":\"<bdi><a href=\\\"https://en.wikipedia.org/wiki/en:Raphael\\\" class=\\\"extiw\\\" title=\\\"w:en:Raphael\\\">Raphael</a>\\n</bdi>\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511<div style=\\\"display: none;\\\">date QS:P571,+1511-00-00T00:00:00Z/9</div>\",\"source\":\"commons-desc-page\"},\"LicenseShortName\":{\"value\":\"Public domain\",\"source\":\"commons-desc-page\",\"hidden\":\"\"}}}]}}}}")
|
||||
var continueJson = ""
|
||||
|
||||
if (queryContinueType != "" && queryContinueValue != "" && continueVal != "") {
|
||||
continueJson = ",\"continue\":{\"$queryContinueType\":\"$queryContinueValue\",\"continue\":\"$continueVal\"}"
|
||||
}
|
||||
|
||||
val mediaList = mutableListOf<String>()
|
||||
val random = Random(1000)
|
||||
for (i in 0 until numberOfPages) {
|
||||
mediaList.add(getMediaPage(random))
|
||||
}
|
||||
|
||||
val pagesString = mediaList.joinToString()
|
||||
val responseBody = "{\"batchcomplete\":\"\"$continueJson,\"query\":{\"pages\":{$pagesString}}}"
|
||||
mockResponse.setBody(responseBody)
|
||||
return mockResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate test media json object
|
||||
*/
|
||||
private fun getMediaPage(random: Random): String {
|
||||
val pageID = random.nextInt()
|
||||
val id = random.nextInt()
|
||||
val fileName = "Test$id"
|
||||
val id1 = random.nextInt()
|
||||
val id2 = random.nextInt()
|
||||
val categories = "cat$id1|cat$id2"
|
||||
return "\"$pageID\":{\"pageid\":$pageID,\"ns\":6,\"title\":\"File:$fileName\",\"imagerepository\":\"local\",\"imageinfo\":[{\"url\":\"https://upload.wikimedia.org/$fileName\",\"descriptionurl\":\"https://commons.wikimedia.org/wiki/File:$fileName\",\"descriptionshorturl\":\"https://commons.wikimedia.org/w/index.php?curid=4406048\",\"extmetadata\":{\"DateTime\":{\"value\":\"2013-04-13 15:12:11\",\"source\":\"mediawiki-metadata\",\"hidden\":\"\"},\"Categories\":{\"value\":\"$categories\",\"source\":\"commons-categories\",\"hidden\":\"\"},\"Artist\":{\"value\":\"<bdi><a href=\\\"https://en.wikipedia.org/wiki/en:Raphael\\\" class=\\\"extiw\\\" title=\\\"w:en:Raphael\\\">Raphael</a>\\n</bdi>\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511<div style=\\\"display: none;\\\">date QS:P571,+1511-00-00T00:00:00Z/9</div>\",\"source\":\"commons-desc-page\"},\"LicenseShortName\":{\"value\":\"Public domain\",\"source\":\"commons-desc-page\",\"hidden\":\"\"}}}]}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Check request params
|
||||
*/
|
||||
private fun assertBasicRequestParameters(server: MockWebServer, method: String): RecordedRequest = server.takeRequest().let {
|
||||
Assert.assertEquals("/", it.requestUrl.encodedPath())
|
||||
Assert.assertEquals(method, it.method)
|
||||
return it
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse query params
|
||||
*/
|
||||
private fun parseQueryParams(request: RecordedRequest) = HashMap<String, String?>().apply {
|
||||
request.requestUrl.let {
|
||||
it.queryParameterNames().forEach { name -> put(name, it.queryParameter(name)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseBody(body: String): Map<String, String> = HashMap<String, String>().apply {
|
||||
body.split("&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray().forEach { prop ->
|
||||
val pair = prop.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
put(pair[0], URLDecoder.decode(pair[1], "utf-8"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package fr.free.nrw.commons.review
|
||||
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.media.model.MwQueryPage
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||
import fr.free.nrw.commons.mwapi.model.RecentChange
|
||||
import io.reactivex.Single
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.InjectMocks
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
/**
|
||||
* Test class for ReviewHelper
|
||||
*/
|
||||
class ReviewHelperTest {
|
||||
|
||||
@Mock
|
||||
internal var okHttpJsonApiClient: OkHttpJsonApiClient? = null
|
||||
@Mock
|
||||
internal var mediaWikiApi: MediaWikiApi? = null
|
||||
|
||||
@InjectMocks
|
||||
var reviewHelper: ReviewHelper? = null
|
||||
|
||||
/**
|
||||
* Init mocks
|
||||
*/
|
||||
@Before
|
||||
@Throws(Exception::class)
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for getting random media
|
||||
*/
|
||||
@Test
|
||||
fun getRandomMedia() {
|
||||
`when`(okHttpJsonApiClient?.recentFileChanges)
|
||||
.thenReturn(Single.just(listOf(RecentChange("test", "File:Test1.jpeg", "0"),
|
||||
RecentChange("test", "File:Test2.png", "0"),
|
||||
RecentChange("test", "File:Test3.jpg", "0"))))
|
||||
|
||||
`when`(mediaWikiApi?.pageExists(ArgumentMatchers.anyString()))
|
||||
.thenReturn(Single.just(true))
|
||||
val randomMedia = reviewHelper?.randomMedia?.blockingGet()
|
||||
|
||||
assertTrue(randomMedia is Media)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for getting first revision of file
|
||||
*/
|
||||
@Test
|
||||
fun getFirstRevisionOfFile() {
|
||||
`when`(okHttpJsonApiClient?.getFirstRevisionOfFile(ArgumentMatchers.anyString()))
|
||||
.thenReturn(Single.just(mock(MwQueryPage.Revision::class.java)))
|
||||
val firstRevisionOfFile = reviewHelper?.getFirstRevisionOfFile("Test.jpg")?.blockingGet()
|
||||
|
||||
assertTrue(firstRevisionOfFile is MwQueryPage.Revision)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import android.app.Application
|
|||
import android.content.Context
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile
|
||||
import fr.free.nrw.commons.kvstore.BasicKvStore
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi
|
||||
import fr.free.nrw.commons.nearby.Place
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue