Replace remaining AsyncTask with RxAndroid (#2681)

This commit is contained in:
Vivek Maskara 2019-03-29 02:40:47 +05:30 committed by Adam Jones
parent a62aaadf90
commit 0bf63f50b3
28 changed files with 1096 additions and 1153 deletions

View file

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

View file

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

View file

@ -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)));
}
});
}
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.
*/
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);
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 (license != null) {
media.setLicense(license);
}
if (deletionStatus){
if (deletionStatus) {
media.setRequestedDeletion();
}
return media;
});
}
// add author, date, etc fields
/**
* Fetch talk page from the MediaWiki API
* @param filename
* @return
*/
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 "";
});
}
}

View file

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

View file

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

View 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();
}
}

View file

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

View file

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

View file

@ -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) {
alert.setPositiveButton(R.string.ok, (dialog1, whichButton) -> {
String reason = input.getText().toString();
DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
deleteTask.execute();
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();
}
}
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());
}
}
}

View file

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

View file

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

View file

@ -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,31 +305,32 @@ 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 {
public Single<MediaResult> fetchMediaByFilename(String filename) {
return Single.fromCallable(() -> {
CustomApiResult apiResult = api.action("query")
.param("prop", "revisions")
.param("titles", filename)
@ -341,6 +342,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return new MediaResult(
apiResult.getString("/api/query/pages/page/revisions/rev"),
apiResult.getString("/api/query/pages/page/revisions/rev/@parsetree"));
});
}
@Override

View file

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

View file

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

View 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());
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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,19 +134,33 @@ 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 */) {
/**
* 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():" + contribution.getLocalUri());
Timber.d("UploadController/doInBackground, contribution.getLocalUri():%s", contribution.getLocalUri());
AssetFileDescriptor assetFileDescriptor = contentResolver
.openAssetFileDescriptor(Uri.fromFile(new File(contribution.getLocalUri().getPath())), "r");
if (assetFileDescriptor != null) {
@ -158,16 +173,12 @@ public class UploadController {
contribution.setDataLength(length);
}
}
} 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: ");
} catch (IOException | NullPointerException | SecurityException e) {
Timber.e(e, "Exception occurred while uploading image");
}
String mimeType = (String) contribution.getTag("mimeType");
Boolean imagePrefix = false;
boolean imagePrefix = false;
if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
mimeType = contentResolver.getType(contribution.getLocalUri());
@ -180,7 +191,7 @@ public class UploadController {
}
if (imagePrefix && contribution.getDateCreated() == null) {
Timber.d("local uri " + contribution.getLocalUri());
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) {
@ -200,15 +211,16 @@ public class UploadController {
return contribution;
}
@Override
protected void onPostExecute(Contribution contribution) {
super.onPostExecute(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);
}
}.executeOnExecutor(Executors.newFixedThreadPool(1)); // TODO remove this by using a sensible thread handling strategy
}
/**

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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