From 7b3625a45c281c9c434f9fb78b6bbdde6f5bcc35 Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Wed, 20 Mar 2019 09:38:55 +0530 Subject: [PATCH 01/14] Use rxjava instead of async task for deletion Use rxjava for upload task Use rxjava for thumbnail fetcher Use rxjava for media details fetch Fix build more fixes --- .../main/java/fr/free/nrw/commons/Media.java | 31 +- .../free/nrw/commons/MediaDataExtractor.java | 329 ++---------------- .../nrw/commons/MediaThumbnailFetchTask.java | 26 -- .../free/nrw/commons/MediaWikiImageView.java | 80 ++--- .../free/nrw/commons/delete/DeleteHelper.java | 187 ++++++++++ .../free/nrw/commons/delete/DeleteTask.java | 253 -------------- .../di/CommonsApplicationComponent.java | 5 +- .../commons/media/MediaDetailFragment.java | 93 +---- .../nrw/commons/media/model/ExtMetadata.java | 38 +- .../mwapi/ApacheHttpClientMediaWikiApi.java | 44 +-- .../free/nrw/commons/mwapi/MediaWikiApi.java | 8 +- .../commons/mwapi/OkHttpJsonApiClient.java | 39 ++- .../nrw/commons/review/ReviewActivity.java | 22 +- .../nrw/commons/review/ReviewController.java | 31 +- .../free/nrw/commons/review/ReviewHelper.java | 16 +- .../nrw/commons/upload/UploadController.java | 133 ++++--- .../commons/utils/MediaDataExtractorUtil.java | 6 + 17 files changed, 496 insertions(+), 845 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java create mode 100644 app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 0e90ba3adb..725281d186 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -3,8 +3,11 @@ import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.Nullable; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -13,14 +16,20 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import androidx.annotation.Nullable; import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.media.model.ExtMetadata; import fr.free.nrw.commons.media.model.ImageInfo; import fr.free.nrw.commons.media.model.MwQueryPage; import fr.free.nrw.commons.utils.DateUtils; +import fr.free.nrw.commons.utils.MediaDataExtractorUtil; import fr.free.nrw.commons.utils.StringUtils; public class Media implements Parcelable { + private static final Type MAP_TYPE = new TypeToken>() { + }.getType(); + public static Creator CREATOR = new Creator() { @Override public Media createFromParcel(Parcel parcel) { @@ -450,21 +459,37 @@ public boolean getRequestedDeletion(){ return requestedDeletion; } - public static Media from(MwQueryPage page) { + public static Media from(MwQueryPage page, Gson gson) { ImageInfo imageInfo = page.imageInfo(); if(imageInfo == null) { return null; } + ExtMetadata metadata = imageInfo.getMetadata(); + + String categories = metadata.categories().value(); + Media media = new Media(null, imageInfo.getOriginalUrl(), page.title(), - imageInfo.getMetadata().imageDescription().value(), + "", 0, DateUtils.getDateFromString(imageInfo.getMetadata().dateTimeOriginal().value()), DateUtils.getDateFromString(imageInfo.getMetadata().dateTime().value()), StringUtils.getParsedStringFromHtml(imageInfo.getMetadata().artist().value()) ); + media.setDescriptions(metadata.imageDescription().value()); + + media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(categories)); + + 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(imageInfo.getMetadata().licenseShortName().value()); return media; diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index f849148a00..1d9fea1830 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -1,33 +1,13 @@ 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 fr.free.nrw.commons.mwapi.MediaWikiApi; -import fr.free.nrw.commons.utils.MediaDataExtractorUtil; -import timber.log.Timber; +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; +import io.reactivex.Single; /** * Fetch additional media data from the network that we don't store locally. @@ -35,301 +15,34 @@ * 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 categories; - private Map 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 - */ - 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; - } + public Single fetchMediaDetails(String filename) { + Single mediaSingle = okHttpJsonApiClient.getMedia(filename, false); + Single pageExistsSingle = mediaWikiApi.pageExists("Commons:Deletion_requests/" + filename); + Single discussionSingle = getDiscussion(filename); + return Single.zip(mediaSingle, pageExistsSingle, discussionSingle, (media, deletionStatus, discussion) -> { + media.setDiscussion(discussion); + if (deletionStatus) { + media.setRequestedDeletion(); } + return media; }); } - private Node findTemplateParameter(Node templateNode, 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 getMultilingualText(Node parentNode) throws IOException { - Map texts = new HashMap<>(); - StringBuilder localText = new StringBuilder(); - - NodeList nodes = parentNode.getChildNodes(); - for (int i = 0, length = nodes.getLength(); i < length; i++) { - Node node = nodes.item(i); - if (node.getNodeName().equals("template")) { - // process a template node - String title = getTemplateTitle(node); - if (title.length() < 3) { - // Hopefully a language code. Nasty hack! - String lang = title; - Node valueNode = findTemplateParameter(node, 1); - String value = valueNode.getTextContent(); // hope there's no subtemplates or formatting for now - texts.put(lang, value); - } - } else if (node.getNodeType() == Node.TEXT_NODE) { - localText.append(node.getTextContent()); - } - } - - // Some descriptions don't list multilingual variants - String defaultText = localText.toString().trim(); - if (defaultText.length() > 0) { - texts.put("default", localText.toString()); - } - return texts; - } - - /** - * Take our metadata and inject it into a live Media object. - * Media object might contain stale or cached data, or emptiness. - * @param media Media object to inject into - */ - public void fill(Media media) { - if (!fetched) { - throw new IllegalStateException("Tried to call MediaDataExtractor.fill() before fetch()."); - } - - media.setCategories(categories); - media.setDescriptions(descriptions); - media.setCoordinates(coordinates); - media.setDiscussion(discussion); - if (license != null) { - media.setLicense(license); - } - if (deletionStatus){ - media.setRequestedDeletion(); - } - - // add author, date, etc fields + private Single getDiscussion(String filename) { + return mediaWikiApi.fetchMediaByFilename(filename.replace("File", "File talk")) + .flatMap(mediaResult -> mediaWikiApi.parseWikicode(mediaResult.getWikiSource())) + .map(discussion -> Html.fromHtml(discussion).toString()); } } diff --git a/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java b/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java deleted file mode 100644 index e56e16a133..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java +++ /dev/null @@ -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 { - 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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java index 460ee60083..42dbcde448 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java @@ -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 thumbnailUrlCache; - private ThumbnailFetchTask currentThumbnailTask; + protected CompositeDisposable compositeDisposable = new CompositeDisposable(); public MediaWikiImageView(Context context) { this(context, null); @@ -44,27 +48,25 @@ public MediaWikiImageView(Context context, AttributeSet attrs, int defStyle) { * @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,21 @@ R.drawable.ic_image_black_24dp, getContext().getTheme())) .build()); } + public Single 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 +111,4 @@ private void setImageUrl(@Nullable String url) { 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); - } - } } diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java new file mode 100644 index 0000000000..202a59a7d0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java @@ -0,0 +1,187 @@ +package fr.free.nrw.commons.delete; + +import android.app.NotificationManager; +import android.app.PendingIntent; +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 androidx.core.app.NotificationCompat; +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.mwapi.MediaWikiApi; +import fr.free.nrw.commons.review.ReviewActivity; +import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.Single; +import timber.log.Timber; + +import static androidx.core.app.NotificationCompat.DEFAULT_ALL; +import static androidx.core.app.NotificationCompat.PRIORITY_HIGH; + +@Singleton +public class DeleteHelper { + private static final int NOTIFICATION_DELETE = 1; + + private final MediaWikiApi mwApi; + private final SessionManager sessionManager; + + private NotificationManager notificationManager; + private NotificationCompat.Builder notificationBuilder; + + @Inject + public DeleteHelper(MediaWikiApi mwApi, SessionManager sessionManager) { + this.mwApi = mwApi; + this.sessionManager = sessionManager; + } + + public Single makeDeletion(Context context, Media media, String reason) { + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL) + .setOnlyAlertOnce(true); + ViewUtil.showShortToast(context, "Trying to nominate " + media.getDisplayTitle() + " for deletion"); + + return Single.fromCallable(() -> delete(media, reason)) + .flatMap(result -> Single.fromCallable(() -> + showDeletionNotification(context, media, result))); + } + + 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; + } + + public 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."; + } + + 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()); + return result; + } + + 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 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(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java deleted file mode 100644 index 7629fb3044..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java +++ /dev/null @@ -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 { - - @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 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(); - - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java index dcb8bc4bac..d0b0c8ab4d 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -10,11 +10,10 @@ 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 mediaDataExtractorProvider; - @Inject - MediaWikiApi mwApi; - @Inject - SessionManager sessionManager; + MediaDataExtractor mediaDataExtractor; @Inject ReasonBuilder reasonBuilder; + @Inject + DeleteHelper deleteHelper; private int initialListTop = 0; @@ -137,7 +132,6 @@ public static MediaDetailFragment forMedia(int index, boolean editable, boolean private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once! private ViewTreeObserver.OnScrollChangedListener scrollListener; private DataSetObserver dataObserver; - private AsyncTask 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 @@ -271,62 +265,19 @@ public void onChanged() { 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() { - 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... + + 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; @@ -426,18 +377,13 @@ public void onDeleteButtonClicked(){ final EditText input = new EditText(getActivity()); alert.setView(input); input.requestFocus(); - alert.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - String reason = input.getText().toString(); + alert.setPositiveButton(R.string.ok, (dialog1, whichButton) -> { + String reason = input.getText().toString(); - DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason); - deleteTask.execute(); - enableDeleteButton(false); - } + deleteHelper.makeDeletion(getContext(), media, reason); + enableDeleteButton(false); }); - alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - } + alert.setNegativeButton(R.string.cancel, (dialog12, whichButton) -> { }); AlertDialog d = alert.create(); input.addTextChangedListener(new TextWatcher() { @@ -470,13 +416,12 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { @SuppressLint("CheckResult") private void onDeleteClicked(Spinner spinner) { String reason = spinner.getSelectedItem().toString(); - Single deletionReason = reasonBuilder.getReason(media, reason); - compositeDisposable.add(deletionReason + Single 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); })); diff --git a/app/src/main/java/fr/free/nrw/commons/media/model/ExtMetadata.java b/app/src/main/java/fr/free/nrw/commons/media/model/ExtMetadata.java index c5eb2d90c4..bbc6eb51c9 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/model/ExtMetadata.java +++ b/app/src/main/java/fr/free/nrw/commons/media/model/ExtMetadata.java @@ -2,6 +2,8 @@ import com.google.gson.annotations.SerializedName; +import java.util.Map; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import fr.free.nrw.commons.utils.StringUtils; @@ -13,7 +15,9 @@ 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("GPSLatitude") @Nullable private Values gpsLatitude; + @SuppressWarnings("unused") @SerializedName("GPSLongitude") @Nullable private Values gpsLongitude; + @SuppressWarnings("unused") @SerializedName("ImageDescription") @Nullable private MapValues imageDescription; @SuppressWarnings("unused") @SerializedName("DateTimeOriginal") @Nullable private Values dateTimeOriginal; @SuppressWarnings("unused") @SerializedName("Artist") @Nullable private Values artist; @SuppressWarnings("unused") @SerializedName("Credit") @Nullable private Values credit; @@ -47,8 +51,23 @@ public class ExtMetadata { return license != null ? license : new Values(); } - @NonNull public Values imageDescription() { - return imageDescription != null ? imageDescription : new Values(); + @NonNull public MapValues imageDescription() { + return imageDescription != null ? imageDescription : new MapValues(); + } + + @NonNull + public Values categories() { + return categories != null ? categories : new Values(); + } + + @NonNull + public Values gpsLatitude() { + return gpsLatitude != null ? gpsLatitude : new Values(); + } + + @NonNull + public Values gpsLongitude() { + return gpsLongitude != null ? gpsLongitude : new Values(); } @NonNull public Values objectName() { @@ -75,4 +94,17 @@ public class Values { return StringUtils.defaultString(source); } } + + public class MapValues { + @SuppressWarnings("unused,NullableProblems") @Nullable private Map value; + @SuppressWarnings("unused,NullableProblems") @Nullable private String source; + + @NonNull public Map value() { + return value; + } + + @NonNull public String source() { + return StringUtils.defaultString(source); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 3a3ad173a5..cec992b858 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -249,11 +249,11 @@ public boolean fileExistsWithName(String fileName) throws IOException { } @Override - public boolean pageExists(String pageName) throws IOException { - return Double.parseDouble( api.action("query") + public Single 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 @@ -308,42 +308,44 @@ public String prependEdit(String editToken, String processedPageContent, String } @Override - public String findThumbnailByFilename(String filename) throws IOException { - return api.action("query") + public Single 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 parseWikicode(String source) { + return Single.fromCallable(() -> api.action("flow-parsoid-utils") .param("from", "wikitext") .param("to", "html") .param("content", source) .param("title", "Main_page") .get() - .getString("/api/flow-parsoid-utils/@content"); + .getString("/api/flow-parsoid-utils/@content")); } @Override @NonNull - public MediaResult fetchMediaByFilename(String filename) throws IOException { - CustomApiResult apiResult = api.action("query") - .param("prop", "revisions") - .param("titles", filename) - .param("rvprop", "content") - .param("rvlimit", 1) - .param("rvgeneratexml", 1) - .get(); - - return new MediaResult( - apiResult.getString("/api/query/pages/page/revisions/rev"), - apiResult.getString("/api/query/pages/page/revisions/rev/@parsetree")); + public Single fetchMediaByFilename(String filename) { + return Single.fromCallable(() -> { + CustomApiResult apiResult = api.action("query") + .param("prop", "revisions") + .param("titles", filename) + .param("rvprop", "content") + .param("rvlimit", 1) + .param("rvgeneratexml", 1) + .get(); + + return new MediaResult( + apiResult.getString("/api/query/pages/page/revisions/rev"), + apiResult.getString("/api/query/pages/page/revisions/rev/@parsetree")); + }); } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index 362113de4e..206f4e1dde 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -34,9 +34,9 @@ public interface MediaWikiApi { boolean fileExistsWithName(String fileName) throws IOException; - boolean pageExists(String pageName) throws IOException; + Single pageExists(String pageName); - String findThumbnailByFilename(String filename) throws IOException; + Single findThumbnailByFilename(String filename); boolean logEvents(LogBuilder[] logBuilders); @@ -72,10 +72,10 @@ Single uploadFileFinalize(String filename, String filekey, @Nullable boolean addWikidataEditTag(String revisionId) throws IOException; - String parseWikicode(String source) throws IOException; + Single parseWikicode(String source); @NonNull - MediaResult fetchMediaByFilename(String filename) throws IOException; + Single fetchMediaByFilename(String filename); @NonNull Observable searchCategories(String filterValue, int searchCatsLimit); diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java index 47d89b11da..3204a792a3 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -208,34 +208,45 @@ public Single getCampaigns() { @Nullable public Single getPictureOfTheDay() { String template = "Template:Potd/" + DateUtils.getCurrentDate(); + return getMedia(template, true); + } + + public Single 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(() -> { Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.body() != null && response.isSuccessful()) { + if (response.body() != null && response.isSuccessful()) { String json = response.body().string(); - if (json == null) { - return null; - } MwQueryResponse mwQueryPage = gson.fromJson(json, MwQueryResponse.class); - return Media.from(mwQueryPage.query().firstPage()); + if (mwQueryPage.success() && mwQueryPage.query().firstPage() != null) { + return Media.from(mwQueryPage.query().firstPage(), gson); + } } return null; }); } + private HttpUrl.Builder appendMediaProperties(HttpUrl.Builder builder) { + builder.addQueryParameter("prop", "imageinfo") + .addQueryParameter("iiprop", "url|extmetadata") + .addQueryParameter("iiextmetadatamultilang", String.valueOf(true)); + return builder; + } + /** * This method takes search keyword as input and returns a list of Media objects filtered using search query * It uses the generator query API to get the images searched using a query, 25 at a time. @@ -254,12 +265,10 @@ public Single> searchImages(String query, int offset) { .addQueryParameter("gsrnamespace", "6") .addQueryParameter("gsrlimit", "25") .addQueryParameter("gsroffset", String.valueOf(offset)) - .addQueryParameter("gsrsearch", query) - .addQueryParameter("prop", "imageinfo") - .addQueryParameter("iiprop", "url|extmetadata"); + .addQueryParameter("gsrsearch", query); Request request = new Request.Builder() - .url(urlBuilder.build()) + .url(appendMediaProperties(urlBuilder).build()) .build(); return Single.fromCallable(() -> { @@ -273,7 +282,7 @@ public Single> searchImages(String query, int offset) { MwQueryResponse mwQueryResponse = gson.fromJson(json, MwQueryResponse.class); List pages = mwQueryResponse.query().pages(); for (MwQueryPage page : pages) { - mediaList.add(Media.from(page)); + mediaList.add(Media.from(page, gson)); } } return mediaList; diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java index 54d0891bcb..f5194b95d0 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java @@ -21,13 +21,13 @@ import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; 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; @@ -49,6 +49,8 @@ public class ReviewActivity extends AuthenticatedActivity { @Inject MediaWikiApi mwApi; @Inject ReviewHelper reviewHelper; + @Inject + DeleteHelper deleteHelper; public ReviewPagerAdapter reviewPagerAdapter; @@ -76,7 +78,7 @@ protected void onCreate(Bundle savedInstanceState) { ButterKnife.bind(this); initDrawer(); - reviewController = new ReviewController(); + reviewController = new ReviewController(deleteHelper, this); reviewPagerAdapter = new ReviewPagerAdapter(getSupportFragmentManager()); reviewPager.setAdapter(reviewPagerAdapter); @@ -119,15 +121,15 @@ private void updateImage(String fileName) { reviewPagerAdapter.updateFileInformation(fileName, revision); })); 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 categories = MediaDataExtractorUtil.extractCategories(mediaResult.getWikiSource()); + updateCategories(categories); + }, this::categoryFetchError); + compositeDisposable.add(disposable); } private void categoryFetchError(Throwable throwable) { diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java index 4d236a96cb..9f89f548a8 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java @@ -15,10 +15,11 @@ 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 void onCategoriesRefreshed(ArrayList categories) { 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)); diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java index 34e2d7074a..5789afd988 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java @@ -1,5 +1,7 @@ package fr.free.nrw.commons.review; +import android.util.Pair; + import javax.inject.Inject; import javax.inject.Singleton; @@ -23,20 +25,20 @@ public ReviewHelper(OkHttpJsonApiClient okHttpJsonApiClient, MediaWikiApi mediaW this.mediaWikiApi = mediaWikiApi; } - public Single getRandomMedia() { + Single 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); + .flatMap(title -> mediaWikiApi.pageExists("Commons:Deletion_requests/" + title) + .map(pageExists -> new Pair<>(title, pageExists))) + .map((Pair pair) -> { + if (!pair.second) { + return new Media(pair.first.replace("File:", "")); } throw new Exception("Page does not exist"); }).retry(MAX_RANDOM_TRIES); } - public Single getFirstRevisionOfFile(String fileName) { + Single getFirstRevisionOfFile(String fileName) { return okHttpJsonApiClient.getFirstRevisionOfFile(fileName); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index b0d7375a25..e8da010489 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -10,7 +10,6 @@ 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.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.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 @@ -133,81 +135,76 @@ private void startUpload(final Contribution contribution, final ContributionUplo 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() { - - // Fills up missing information about Contributions - // Only does things that involve some form of IO - // Runs in background thread - @Override - protected Contribution doInBackground(Void... voids /* stare into you */) { - long length; - ContentResolver contentResolver = context.getContentResolver(); - try { - if (contribution.getDataLength() <= 0) { - Timber.d("UploadController/doInBackground, contribution.getLocalUri():" + contribution.getLocalUri()); - AssetFileDescriptor assetFileDescriptor = contentResolver - .openAssetFileDescriptor(Uri.fromFile(new File(contribution.getLocalUri().getPath())), "r"); - if (assetFileDescriptor != null) { - length = assetFileDescriptor.getLength(); - if (length == -1) { - // Let us find out the long way! - length = countBytes(contentResolver - .openInputStream(contribution.getLocalUri())); - } - contribution.setDataLength(length); - } + uploadTask(contribution, onComplete); + } + + private Disposable uploadTask(Contribution contribution, ContributionUploadProgress onComplete) { + return Single.fromCallable(() -> makeUpload(contribution)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(finalContribution -> onUploadCompleted(finalContribution, onComplete)); + } + + private Contribution makeUpload(Contribution contribution) { + long length; + ContentResolver contentResolver = context.getContentResolver(); + try { + if (contribution.getDataLength() <= 0) { + Timber.d("UploadController/doInBackground, contribution.getLocalUri():%s", contribution.getLocalUri()); + AssetFileDescriptor assetFileDescriptor = contentResolver + .openAssetFileDescriptor(Uri.fromFile(new File(contribution.getLocalUri().getPath())), "r"); + if (assetFileDescriptor != null) { + length = assetFileDescriptor.getLength(); + if (length == -1) { + // Let us find out the long way! + length = countBytes(contentResolver + .openInputStream(contribution.getLocalUri())); } - } catch (IOException e) { - Timber.e(e, "IO Exception: "); - } catch (NullPointerException e) { - Timber.e(e, "Null Pointer Exception: "); - } catch (SecurityException e) { - Timber.e(e, "Security Exception: "); + contribution.setDataLength(length); } + } + } catch (IOException | NullPointerException | SecurityException e) { + Timber.e(e, "Exception occurred while uploading image"); + } - String mimeType = (String) contribution.getTag("mimeType"); - Boolean imagePrefix = false; + String mimeType = (String) contribution.getTag("mimeType"); + boolean imagePrefix = false; - if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) { - mimeType = contentResolver.getType(contribution.getLocalUri()); - } + if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) { + mimeType = contentResolver.getType(contribution.getLocalUri()); + } - if (mimeType != null) { - contribution.setTag("mimeType", mimeType); - imagePrefix = mimeType.startsWith("image/"); - Timber.d("MimeType is: %s", mimeType); - } + if (mimeType != null) { + contribution.setTag("mimeType", mimeType); + imagePrefix = mimeType.startsWith("image/"); + Timber.d("MimeType is: %s", mimeType); + } - if (imagePrefix && contribution.getDateCreated() == null) { - Timber.d("local uri " + contribution.getLocalUri()); - Cursor cursor = contentResolver.query(contribution.getLocalUri(), - new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null); - if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) { - cursor.moveToFirst(); - Date dateCreated = new Date(cursor.getLong(0)); - Date epochStart = new Date(0); - if (dateCreated.equals(epochStart) || dateCreated.before(epochStart)) { - // If date is incorrect (1st second of unix time) then set it to the current date - dateCreated = new Date(); - } - contribution.setDateCreated(dateCreated); - cursor.close(); - } else { - contribution.setDateCreated(new Date()); - } + if (imagePrefix && contribution.getDateCreated() == null) { + Timber.d("local uri %s", contribution.getLocalUri()); + Cursor cursor = contentResolver.query(contribution.getLocalUri(), + new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null); + if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) { + cursor.moveToFirst(); + Date dateCreated = new Date(cursor.getLong(0)); + Date epochStart = new Date(0); + if (dateCreated.equals(epochStart) || dateCreated.before(epochStart)) { + // If date is incorrect (1st second of unix time) then set it to the current date + dateCreated = new Date(); } - return contribution; + contribution.setDateCreated(dateCreated); + cursor.close(); + } else { + contribution.setDateCreated(new Date()); } + } + return contribution; + } - @Override - protected void onPostExecute(Contribution contribution) { - super.onPostExecute(contribution); - //Starts the upload. If commented out, user can proceed to next Fragment but upload doesn't happen - uploadService.queue(UploadService.ACTION_UPLOAD_FILE, contribution); - onComplete.onUploadStarted(contribution); - } - }.executeOnExecutor(Executors.newFixedThreadPool(1)); // TODO remove this by using a sensible thread handling strategy + 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); } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java index 63421a8e4f..656db534cc 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java @@ -1,6 +1,8 @@ package fr.free.nrw.commons.utils; import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -24,5 +26,9 @@ public static ArrayList extractCategories(String source) { return categories; } + public static List extractCategoriesFromList(String source) { + String[] categories = source.split("\\|"); + return Arrays.asList(categories); + } } From 2fdb45974f9306c04df55fa177a4bf9b03f906f5 Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Sat, 23 Mar 2019 17:20:54 +0530 Subject: [PATCH 02/14] Add javadocs --- .../main/java/fr/free/nrw/commons/Media.java | 30 +++++++++++-------- .../free/nrw/commons/MediaDataExtractor.java | 6 ++++ .../free/nrw/commons/MediaWikiImageView.java | 6 ++++ .../free/nrw/commons/delete/DeleteHelper.java | 23 ++++++++++++++ .../commons/mwapi/OkHttpJsonApiClient.java | 18 +++++++++-- .../free/nrw/commons/review/ReviewHelper.java | 8 +++++ .../nrw/commons/upload/UploadController.java | 17 ++++++++++- .../commons/utils/MediaDataExtractorUtil.java | 5 ++++ 8 files changed, 97 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 725281d186..f399758a7f 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -4,10 +4,6 @@ import android.os.Parcel; import android.os.Parcelable; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; - -import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -27,9 +23,6 @@ public class Media implements Parcelable { - private static final Type MAP_TYPE = new TypeToken>() { - }.getType(); - public static Creator CREATOR = new Creator() { @Override public Media createFromParcel(Parcel parcel) { @@ -459,12 +452,24 @@ public boolean getRequestedDeletion(){ return requestedDeletion; } - public static Media from(MwQueryPage page, Gson gson) { + /** + * 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 + */ + 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); + } String categories = metadata.categories().value(); @@ -473,13 +478,12 @@ public static Media from(MwQueryPage page, Gson gson) { page.title(), "", 0, - DateUtils.getDateFromString(imageInfo.getMetadata().dateTimeOriginal().value()), - DateUtils.getDateFromString(imageInfo.getMetadata().dateTime().value()), - StringUtils.getParsedStringFromHtml(imageInfo.getMetadata().artist().value()) + DateUtils.getDateFromString(metadata.dateTimeOriginal().value()), + DateUtils.getDateFromString(metadata.dateTime().value()), + StringUtils.getParsedStringFromHtml(metadata.artist().value()) ); media.setDescriptions(metadata.imageDescription().value()); - media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(categories)); String latitude = metadata.gpsLatitude().value(); @@ -490,7 +494,7 @@ public static Media from(MwQueryPage page, Gson gson) { media.setCoordinates(latLng); } - media.setLicense(imageInfo.getMetadata().licenseShortName().value()); + media.setLicense(metadata.licenseShortName().value()); return media; } diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index 1d9fea1830..1daabb328a 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -27,6 +27,12 @@ public MediaDataExtractor(MediaWikiApi mwApi, this.mediaWikiApi = mwApi; } + /** + * 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 + */ public Single fetchMediaDetails(String filename) { Single mediaSingle = okHttpJsonApiClient.getMedia(filename, false); Single pageExistsSingle = mediaWikiApi.pageExists("Commons:Deletion_requests/" + filename); diff --git a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java index 42dbcde448..c78c550bb1 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java @@ -88,6 +88,12 @@ R.drawable.ic_image_black_24dp, getContext().getTheme())) .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 fetchMediaThumbnail(Media media) { if (media.getFilename() != null && thumbnailUrlCache.get(media.getFilename()) != null) { return Single.just(thumbnailUrlCache.get(media.getFilename())); diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java index 202a59a7d0..bdd8df16bb 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java @@ -30,6 +30,9 @@ import static androidx.core.app.NotificationCompat.DEFAULT_ALL; import static androidx.core.app.NotificationCompat.PRIORITY_HIGH; +/** + * Refactored async task to Rx + */ @Singleton public class DeleteHelper { private static final int NOTIFICATION_DELETE = 1; @@ -46,6 +49,13 @@ public DeleteHelper(MediaWikiApi mwApi, SessionManager sessionManager) { this.sessionManager = sessionManager; } + /** + * Public interface to nominate a particular media file for deletion + * @param context + * @param media + * @param reason + * @return + */ public Single makeDeletion(Context context, Media media, String reason) { notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationBuilder = new NotificationCompat.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL) @@ -57,6 +67,12 @@ public Single makeDeletion(Context context, Media media, String reason) 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; @@ -135,6 +151,13 @@ public boolean showDeletionNotification(Context context, Media media, boolean re 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); diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java index 3204a792a3..3ecdcfb51f 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -211,6 +211,13 @@ public Single getPictureOfTheDay() { return getMedia(template, true); } + /** + * Fetches Media object from the imageInfo API + * + * @param titles + * @param useGenerator + * @return + */ public Single getMedia(String titles, boolean useGenerator) { HttpUrl.Builder urlBuilder = HttpUrl .parse(commonsBaseUrl) @@ -233,16 +240,23 @@ public Single getMedia(String titles, boolean useGenerator) { 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(), gson); + 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") .addQueryParameter("iiextmetadatamultilang", String.valueOf(true)); return builder; } @@ -282,7 +296,7 @@ public Single> searchImages(String query, int offset) { MwQueryResponse mwQueryResponse = gson.fromJson(json, MwQueryResponse.class); List pages = mwQueryResponse.query().pages(); for (MwQueryPage page : pages) { - mediaList.add(Media.from(page, gson)); + mediaList.add(Media.from(page)); } } return mediaList; diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java index 5789afd988..ed6bb73e28 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java @@ -25,6 +25,14 @@ public ReviewHelper(OkHttpJsonApiClient okHttpJsonApiClient, MediaWikiApi mediaW this.mediaWikiApi = mediaWikiApi; } + /** + * 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 getRandomMedia() { return okHttpJsonApiClient.getRecentFileChanges() .map(RecentChangesImageUtils::findImageInRecentChanges) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index e8da010489..34603f0b8a 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -47,7 +47,6 @@ public interface ContributionUploadProgress { void onUploadStarted(Contribution contribution); } - @Inject public UploadController(SessionManager sessionManager, Context context, @@ -138,6 +137,12 @@ private void startUpload(final Contribution contribution, final ContributionUplo uploadTask(contribution, onComplete); } + /** + * Initiates the upload task + * @param contribution + * @param onComplete + * @return + */ private Disposable uploadTask(Contribution contribution, ContributionUploadProgress onComplete) { return Single.fromCallable(() -> makeUpload(contribution)) .subscribeOn(Schedulers.io()) @@ -145,6 +150,11 @@ private Disposable uploadTask(Contribution contribution, ContributionUploadProgr .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(); @@ -201,6 +211,11 @@ private Contribution makeUpload(Contribution contribution) { return contribution; } + /** + * When the contribution object is completely formed, the item is queued to the upload service + * @param contribution + * @param onComplete + */ private void onUploadCompleted(Contribution contribution, ContributionUploadProgress onComplete) { //Starts the upload. If commented out, user can proceed to next Fragment but upload doesn't happen uploadService.queue(UploadService.ACTION_UPLOAD_FILE, contribution); diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java index 656db534cc..8623ddfa09 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java @@ -26,6 +26,11 @@ public static ArrayList extractCategories(String source) { return categories; } + /** + * Extracts a list of categories from | separated category string + * @param source + * @return + */ public static List extractCategoriesFromList(String source) { String[] categories = source.split("\\|"); return Arrays.asList(categories); From 170258c49dc0631055f6220201ed36ee464418ce Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Wed, 20 Mar 2019 09:38:55 +0530 Subject: [PATCH 03/14] Use rxjava instead of async task for deletion Use rxjava for upload task Use rxjava for thumbnail fetcher Use rxjava for media details fetch Fix build more fixes --- .../main/java/fr/free/nrw/commons/Media.java | 31 +- .../free/nrw/commons/MediaDataExtractor.java | 329 ++---------------- .../nrw/commons/MediaThumbnailFetchTask.java | 26 -- .../free/nrw/commons/MediaWikiImageView.java | 80 ++--- .../free/nrw/commons/delete/DeleteHelper.java | 187 ++++++++++ .../free/nrw/commons/delete/DeleteTask.java | 253 -------------- .../di/CommonsApplicationComponent.java | 5 +- .../commons/media/MediaDetailFragment.java | 93 +---- .../nrw/commons/media/model/ExtMetadata.java | 38 +- .../mwapi/ApacheHttpClientMediaWikiApi.java | 44 +-- .../free/nrw/commons/mwapi/MediaWikiApi.java | 8 +- .../commons/mwapi/OkHttpJsonApiClient.java | 39 ++- .../nrw/commons/review/ReviewActivity.java | 22 +- .../nrw/commons/review/ReviewController.java | 31 +- .../free/nrw/commons/review/ReviewHelper.java | 16 +- .../nrw/commons/upload/UploadController.java | 133 ++++--- .../commons/utils/MediaDataExtractorUtil.java | 6 + 17 files changed, 496 insertions(+), 845 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java create mode 100644 app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 0e90ba3adb..725281d186 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -3,8 +3,11 @@ import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.Nullable; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -13,14 +16,20 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import androidx.annotation.Nullable; import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.media.model.ExtMetadata; import fr.free.nrw.commons.media.model.ImageInfo; import fr.free.nrw.commons.media.model.MwQueryPage; import fr.free.nrw.commons.utils.DateUtils; +import fr.free.nrw.commons.utils.MediaDataExtractorUtil; import fr.free.nrw.commons.utils.StringUtils; public class Media implements Parcelable { + private static final Type MAP_TYPE = new TypeToken>() { + }.getType(); + public static Creator CREATOR = new Creator() { @Override public Media createFromParcel(Parcel parcel) { @@ -450,21 +459,37 @@ public boolean getRequestedDeletion(){ return requestedDeletion; } - public static Media from(MwQueryPage page) { + public static Media from(MwQueryPage page, Gson gson) { ImageInfo imageInfo = page.imageInfo(); if(imageInfo == null) { return null; } + ExtMetadata metadata = imageInfo.getMetadata(); + + String categories = metadata.categories().value(); + Media media = new Media(null, imageInfo.getOriginalUrl(), page.title(), - imageInfo.getMetadata().imageDescription().value(), + "", 0, DateUtils.getDateFromString(imageInfo.getMetadata().dateTimeOriginal().value()), DateUtils.getDateFromString(imageInfo.getMetadata().dateTime().value()), StringUtils.getParsedStringFromHtml(imageInfo.getMetadata().artist().value()) ); + media.setDescriptions(metadata.imageDescription().value()); + + media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(categories)); + + 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(imageInfo.getMetadata().licenseShortName().value()); return media; diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index f849148a00..1d9fea1830 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -1,33 +1,13 @@ 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 fr.free.nrw.commons.mwapi.MediaWikiApi; -import fr.free.nrw.commons.utils.MediaDataExtractorUtil; -import timber.log.Timber; +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; +import io.reactivex.Single; /** * Fetch additional media data from the network that we don't store locally. @@ -35,301 +15,34 @@ * 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 categories; - private Map 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 - */ - 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; - } + public Single fetchMediaDetails(String filename) { + Single mediaSingle = okHttpJsonApiClient.getMedia(filename, false); + Single pageExistsSingle = mediaWikiApi.pageExists("Commons:Deletion_requests/" + filename); + Single discussionSingle = getDiscussion(filename); + return Single.zip(mediaSingle, pageExistsSingle, discussionSingle, (media, deletionStatus, discussion) -> { + media.setDiscussion(discussion); + if (deletionStatus) { + media.setRequestedDeletion(); } + return media; }); } - private Node findTemplateParameter(Node templateNode, 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 getMultilingualText(Node parentNode) throws IOException { - Map texts = new HashMap<>(); - StringBuilder localText = new StringBuilder(); - - NodeList nodes = parentNode.getChildNodes(); - for (int i = 0, length = nodes.getLength(); i < length; i++) { - Node node = nodes.item(i); - if (node.getNodeName().equals("template")) { - // process a template node - String title = getTemplateTitle(node); - if (title.length() < 3) { - // Hopefully a language code. Nasty hack! - String lang = title; - Node valueNode = findTemplateParameter(node, 1); - String value = valueNode.getTextContent(); // hope there's no subtemplates or formatting for now - texts.put(lang, value); - } - } else if (node.getNodeType() == Node.TEXT_NODE) { - localText.append(node.getTextContent()); - } - } - - // Some descriptions don't list multilingual variants - String defaultText = localText.toString().trim(); - if (defaultText.length() > 0) { - texts.put("default", localText.toString()); - } - return texts; - } - - /** - * Take our metadata and inject it into a live Media object. - * Media object might contain stale or cached data, or emptiness. - * @param media Media object to inject into - */ - public void fill(Media media) { - if (!fetched) { - throw new IllegalStateException("Tried to call MediaDataExtractor.fill() before fetch()."); - } - - media.setCategories(categories); - media.setDescriptions(descriptions); - media.setCoordinates(coordinates); - media.setDiscussion(discussion); - if (license != null) { - media.setLicense(license); - } - if (deletionStatus){ - media.setRequestedDeletion(); - } - - // add author, date, etc fields + private Single getDiscussion(String filename) { + return mediaWikiApi.fetchMediaByFilename(filename.replace("File", "File talk")) + .flatMap(mediaResult -> mediaWikiApi.parseWikicode(mediaResult.getWikiSource())) + .map(discussion -> Html.fromHtml(discussion).toString()); } } diff --git a/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java b/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java deleted file mode 100644 index e56e16a133..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java +++ /dev/null @@ -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 { - 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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java index 460ee60083..42dbcde448 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java @@ -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 thumbnailUrlCache; - private ThumbnailFetchTask currentThumbnailTask; + protected CompositeDisposable compositeDisposable = new CompositeDisposable(); public MediaWikiImageView(Context context) { this(context, null); @@ -44,27 +48,25 @@ public MediaWikiImageView(Context context, AttributeSet attrs, int defStyle) { * @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,21 @@ R.drawable.ic_image_black_24dp, getContext().getTheme())) .build()); } + public Single 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 +111,4 @@ private void setImageUrl(@Nullable String url) { 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); - } - } } diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java new file mode 100644 index 0000000000..202a59a7d0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java @@ -0,0 +1,187 @@ +package fr.free.nrw.commons.delete; + +import android.app.NotificationManager; +import android.app.PendingIntent; +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 androidx.core.app.NotificationCompat; +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.mwapi.MediaWikiApi; +import fr.free.nrw.commons.review.ReviewActivity; +import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.Single; +import timber.log.Timber; + +import static androidx.core.app.NotificationCompat.DEFAULT_ALL; +import static androidx.core.app.NotificationCompat.PRIORITY_HIGH; + +@Singleton +public class DeleteHelper { + private static final int NOTIFICATION_DELETE = 1; + + private final MediaWikiApi mwApi; + private final SessionManager sessionManager; + + private NotificationManager notificationManager; + private NotificationCompat.Builder notificationBuilder; + + @Inject + public DeleteHelper(MediaWikiApi mwApi, SessionManager sessionManager) { + this.mwApi = mwApi; + this.sessionManager = sessionManager; + } + + public Single makeDeletion(Context context, Media media, String reason) { + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL) + .setOnlyAlertOnce(true); + ViewUtil.showShortToast(context, "Trying to nominate " + media.getDisplayTitle() + " for deletion"); + + return Single.fromCallable(() -> delete(media, reason)) + .flatMap(result -> Single.fromCallable(() -> + showDeletionNotification(context, media, result))); + } + + 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; + } + + public 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."; + } + + 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()); + return result; + } + + 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 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(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java deleted file mode 100644 index 7629fb3044..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java +++ /dev/null @@ -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 { - - @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 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(); - - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java index dcb8bc4bac..d0b0c8ab4d 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -10,11 +10,10 @@ 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 mediaDataExtractorProvider; - @Inject - MediaWikiApi mwApi; - @Inject - SessionManager sessionManager; + MediaDataExtractor mediaDataExtractor; @Inject ReasonBuilder reasonBuilder; + @Inject + DeleteHelper deleteHelper; private int initialListTop = 0; @@ -137,7 +132,6 @@ public static MediaDetailFragment forMedia(int index, boolean editable, boolean private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once! private ViewTreeObserver.OnScrollChangedListener scrollListener; private DataSetObserver dataObserver; - private AsyncTask 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 @@ -271,62 +265,19 @@ public void onChanged() { 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() { - 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... + + 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; @@ -426,18 +377,13 @@ public void onDeleteButtonClicked(){ final EditText input = new EditText(getActivity()); alert.setView(input); input.requestFocus(); - alert.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - String reason = input.getText().toString(); + alert.setPositiveButton(R.string.ok, (dialog1, whichButton) -> { + String reason = input.getText().toString(); - DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason); - deleteTask.execute(); - enableDeleteButton(false); - } + deleteHelper.makeDeletion(getContext(), media, reason); + enableDeleteButton(false); }); - alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - } + alert.setNegativeButton(R.string.cancel, (dialog12, whichButton) -> { }); AlertDialog d = alert.create(); input.addTextChangedListener(new TextWatcher() { @@ -470,13 +416,12 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { @SuppressLint("CheckResult") private void onDeleteClicked(Spinner spinner) { String reason = spinner.getSelectedItem().toString(); - Single deletionReason = reasonBuilder.getReason(media, reason); - compositeDisposable.add(deletionReason + Single 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); })); diff --git a/app/src/main/java/fr/free/nrw/commons/media/model/ExtMetadata.java b/app/src/main/java/fr/free/nrw/commons/media/model/ExtMetadata.java index c5eb2d90c4..bbc6eb51c9 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/model/ExtMetadata.java +++ b/app/src/main/java/fr/free/nrw/commons/media/model/ExtMetadata.java @@ -2,6 +2,8 @@ import com.google.gson.annotations.SerializedName; +import java.util.Map; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import fr.free.nrw.commons.utils.StringUtils; @@ -13,7 +15,9 @@ 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("GPSLatitude") @Nullable private Values gpsLatitude; + @SuppressWarnings("unused") @SerializedName("GPSLongitude") @Nullable private Values gpsLongitude; + @SuppressWarnings("unused") @SerializedName("ImageDescription") @Nullable private MapValues imageDescription; @SuppressWarnings("unused") @SerializedName("DateTimeOriginal") @Nullable private Values dateTimeOriginal; @SuppressWarnings("unused") @SerializedName("Artist") @Nullable private Values artist; @SuppressWarnings("unused") @SerializedName("Credit") @Nullable private Values credit; @@ -47,8 +51,23 @@ public class ExtMetadata { return license != null ? license : new Values(); } - @NonNull public Values imageDescription() { - return imageDescription != null ? imageDescription : new Values(); + @NonNull public MapValues imageDescription() { + return imageDescription != null ? imageDescription : new MapValues(); + } + + @NonNull + public Values categories() { + return categories != null ? categories : new Values(); + } + + @NonNull + public Values gpsLatitude() { + return gpsLatitude != null ? gpsLatitude : new Values(); + } + + @NonNull + public Values gpsLongitude() { + return gpsLongitude != null ? gpsLongitude : new Values(); } @NonNull public Values objectName() { @@ -75,4 +94,17 @@ public class Values { return StringUtils.defaultString(source); } } + + public class MapValues { + @SuppressWarnings("unused,NullableProblems") @Nullable private Map value; + @SuppressWarnings("unused,NullableProblems") @Nullable private String source; + + @NonNull public Map value() { + return value; + } + + @NonNull public String source() { + return StringUtils.defaultString(source); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 95117cb46e..466aaec471 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -249,11 +249,11 @@ public boolean fileExistsWithName(String fileName) throws IOException { } @Override - public boolean pageExists(String pageName) throws IOException { - return Double.parseDouble( api.action("query") + public Single 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 @@ -308,42 +308,44 @@ public String prependEdit(String editToken, String processedPageContent, String } @Override - public String findThumbnailByFilename(String filename) throws IOException { - return api.action("query") + public Single 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 parseWikicode(String source) { + return Single.fromCallable(() -> api.action("flow-parsoid-utils") .param("from", "wikitext") .param("to", "html") .param("content", source) .param("title", "Main_page") .get() - .getString("/api/flow-parsoid-utils/@content"); + .getString("/api/flow-parsoid-utils/@content")); } @Override @NonNull - public MediaResult fetchMediaByFilename(String filename) throws IOException { - CustomApiResult apiResult = api.action("query") - .param("prop", "revisions") - .param("titles", filename) - .param("rvprop", "content") - .param("rvlimit", 1) - .param("rvgeneratexml", 1) - .get(); - - return new MediaResult( - apiResult.getString("/api/query/pages/page/revisions/rev"), - apiResult.getString("/api/query/pages/page/revisions/rev/@parsetree")); + public Single fetchMediaByFilename(String filename) { + return Single.fromCallable(() -> { + CustomApiResult apiResult = api.action("query") + .param("prop", "revisions") + .param("titles", filename) + .param("rvprop", "content") + .param("rvlimit", 1) + .param("rvgeneratexml", 1) + .get(); + + return new MediaResult( + apiResult.getString("/api/query/pages/page/revisions/rev"), + apiResult.getString("/api/query/pages/page/revisions/rev/@parsetree")); + }); } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index 362113de4e..206f4e1dde 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -34,9 +34,9 @@ public interface MediaWikiApi { boolean fileExistsWithName(String fileName) throws IOException; - boolean pageExists(String pageName) throws IOException; + Single pageExists(String pageName); - String findThumbnailByFilename(String filename) throws IOException; + Single findThumbnailByFilename(String filename); boolean logEvents(LogBuilder[] logBuilders); @@ -72,10 +72,10 @@ Single uploadFileFinalize(String filename, String filekey, @Nullable boolean addWikidataEditTag(String revisionId) throws IOException; - String parseWikicode(String source) throws IOException; + Single parseWikicode(String source); @NonNull - MediaResult fetchMediaByFilename(String filename) throws IOException; + Single fetchMediaByFilename(String filename); @NonNull Observable searchCategories(String filterValue, int searchCatsLimit); diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java index 47d89b11da..3204a792a3 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -208,34 +208,45 @@ public Single getCampaigns() { @Nullable public Single getPictureOfTheDay() { String template = "Template:Potd/" + DateUtils.getCurrentDate(); + return getMedia(template, true); + } + + public Single 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(() -> { Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.body() != null && response.isSuccessful()) { + if (response.body() != null && response.isSuccessful()) { String json = response.body().string(); - if (json == null) { - return null; - } MwQueryResponse mwQueryPage = gson.fromJson(json, MwQueryResponse.class); - return Media.from(mwQueryPage.query().firstPage()); + if (mwQueryPage.success() && mwQueryPage.query().firstPage() != null) { + return Media.from(mwQueryPage.query().firstPage(), gson); + } } return null; }); } + private HttpUrl.Builder appendMediaProperties(HttpUrl.Builder builder) { + builder.addQueryParameter("prop", "imageinfo") + .addQueryParameter("iiprop", "url|extmetadata") + .addQueryParameter("iiextmetadatamultilang", String.valueOf(true)); + return builder; + } + /** * This method takes search keyword as input and returns a list of Media objects filtered using search query * It uses the generator query API to get the images searched using a query, 25 at a time. @@ -254,12 +265,10 @@ public Single> searchImages(String query, int offset) { .addQueryParameter("gsrnamespace", "6") .addQueryParameter("gsrlimit", "25") .addQueryParameter("gsroffset", String.valueOf(offset)) - .addQueryParameter("gsrsearch", query) - .addQueryParameter("prop", "imageinfo") - .addQueryParameter("iiprop", "url|extmetadata"); + .addQueryParameter("gsrsearch", query); Request request = new Request.Builder() - .url(urlBuilder.build()) + .url(appendMediaProperties(urlBuilder).build()) .build(); return Single.fromCallable(() -> { @@ -273,7 +282,7 @@ public Single> searchImages(String query, int offset) { MwQueryResponse mwQueryResponse = gson.fromJson(json, MwQueryResponse.class); List pages = mwQueryResponse.query().pages(); for (MwQueryPage page : pages) { - mediaList.add(Media.from(page)); + mediaList.add(Media.from(page, gson)); } } return mediaList; diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java index 54d0891bcb..f5194b95d0 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java @@ -21,13 +21,13 @@ import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; 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; @@ -49,6 +49,8 @@ public class ReviewActivity extends AuthenticatedActivity { @Inject MediaWikiApi mwApi; @Inject ReviewHelper reviewHelper; + @Inject + DeleteHelper deleteHelper; public ReviewPagerAdapter reviewPagerAdapter; @@ -76,7 +78,7 @@ protected void onCreate(Bundle savedInstanceState) { ButterKnife.bind(this); initDrawer(); - reviewController = new ReviewController(); + reviewController = new ReviewController(deleteHelper, this); reviewPagerAdapter = new ReviewPagerAdapter(getSupportFragmentManager()); reviewPager.setAdapter(reviewPagerAdapter); @@ -119,15 +121,15 @@ private void updateImage(String fileName) { reviewPagerAdapter.updateFileInformation(fileName, revision); })); 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 categories = MediaDataExtractorUtil.extractCategories(mediaResult.getWikiSource()); + updateCategories(categories); + }, this::categoryFetchError); + compositeDisposable.add(disposable); } private void categoryFetchError(Throwable throwable) { diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java index 4d236a96cb..9f89f548a8 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java @@ -15,10 +15,11 @@ 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 void onCategoriesRefreshed(ArrayList categories) { 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)); diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java index 34e2d7074a..5789afd988 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java @@ -1,5 +1,7 @@ package fr.free.nrw.commons.review; +import android.util.Pair; + import javax.inject.Inject; import javax.inject.Singleton; @@ -23,20 +25,20 @@ public ReviewHelper(OkHttpJsonApiClient okHttpJsonApiClient, MediaWikiApi mediaW this.mediaWikiApi = mediaWikiApi; } - public Single getRandomMedia() { + Single 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); + .flatMap(title -> mediaWikiApi.pageExists("Commons:Deletion_requests/" + title) + .map(pageExists -> new Pair<>(title, pageExists))) + .map((Pair pair) -> { + if (!pair.second) { + return new Media(pair.first.replace("File:", "")); } throw new Exception("Page does not exist"); }).retry(MAX_RANDOM_TRIES); } - public Single getFirstRevisionOfFile(String fileName) { + Single getFirstRevisionOfFile(String fileName) { return okHttpJsonApiClient.getFirstRevisionOfFile(fileName); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index b0d7375a25..e8da010489 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -10,7 +10,6 @@ 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.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.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 @@ -133,81 +135,76 @@ private void startUpload(final Contribution contribution, final ContributionUplo 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() { - - // Fills up missing information about Contributions - // Only does things that involve some form of IO - // Runs in background thread - @Override - protected Contribution doInBackground(Void... voids /* stare into you */) { - long length; - ContentResolver contentResolver = context.getContentResolver(); - try { - if (contribution.getDataLength() <= 0) { - Timber.d("UploadController/doInBackground, contribution.getLocalUri():" + contribution.getLocalUri()); - AssetFileDescriptor assetFileDescriptor = contentResolver - .openAssetFileDescriptor(Uri.fromFile(new File(contribution.getLocalUri().getPath())), "r"); - if (assetFileDescriptor != null) { - length = assetFileDescriptor.getLength(); - if (length == -1) { - // Let us find out the long way! - length = countBytes(contentResolver - .openInputStream(contribution.getLocalUri())); - } - contribution.setDataLength(length); - } + uploadTask(contribution, onComplete); + } + + private Disposable uploadTask(Contribution contribution, ContributionUploadProgress onComplete) { + return Single.fromCallable(() -> makeUpload(contribution)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(finalContribution -> onUploadCompleted(finalContribution, onComplete)); + } + + private Contribution makeUpload(Contribution contribution) { + long length; + ContentResolver contentResolver = context.getContentResolver(); + try { + if (contribution.getDataLength() <= 0) { + Timber.d("UploadController/doInBackground, contribution.getLocalUri():%s", contribution.getLocalUri()); + AssetFileDescriptor assetFileDescriptor = contentResolver + .openAssetFileDescriptor(Uri.fromFile(new File(contribution.getLocalUri().getPath())), "r"); + if (assetFileDescriptor != null) { + length = assetFileDescriptor.getLength(); + if (length == -1) { + // Let us find out the long way! + length = countBytes(contentResolver + .openInputStream(contribution.getLocalUri())); } - } catch (IOException e) { - Timber.e(e, "IO Exception: "); - } catch (NullPointerException e) { - Timber.e(e, "Null Pointer Exception: "); - } catch (SecurityException e) { - Timber.e(e, "Security Exception: "); + contribution.setDataLength(length); } + } + } catch (IOException | NullPointerException | SecurityException e) { + Timber.e(e, "Exception occurred while uploading image"); + } - String mimeType = (String) contribution.getTag("mimeType"); - Boolean imagePrefix = false; + String mimeType = (String) contribution.getTag("mimeType"); + boolean imagePrefix = false; - if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) { - mimeType = contentResolver.getType(contribution.getLocalUri()); - } + if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) { + mimeType = contentResolver.getType(contribution.getLocalUri()); + } - if (mimeType != null) { - contribution.setTag("mimeType", mimeType); - imagePrefix = mimeType.startsWith("image/"); - Timber.d("MimeType is: %s", mimeType); - } + if (mimeType != null) { + contribution.setTag("mimeType", mimeType); + imagePrefix = mimeType.startsWith("image/"); + Timber.d("MimeType is: %s", mimeType); + } - if (imagePrefix && contribution.getDateCreated() == null) { - Timber.d("local uri " + contribution.getLocalUri()); - Cursor cursor = contentResolver.query(contribution.getLocalUri(), - new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null); - if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) { - cursor.moveToFirst(); - Date dateCreated = new Date(cursor.getLong(0)); - Date epochStart = new Date(0); - if (dateCreated.equals(epochStart) || dateCreated.before(epochStart)) { - // If date is incorrect (1st second of unix time) then set it to the current date - dateCreated = new Date(); - } - contribution.setDateCreated(dateCreated); - cursor.close(); - } else { - contribution.setDateCreated(new Date()); - } + if (imagePrefix && contribution.getDateCreated() == null) { + Timber.d("local uri %s", contribution.getLocalUri()); + Cursor cursor = contentResolver.query(contribution.getLocalUri(), + new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null); + if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) { + cursor.moveToFirst(); + Date dateCreated = new Date(cursor.getLong(0)); + Date epochStart = new Date(0); + if (dateCreated.equals(epochStart) || dateCreated.before(epochStart)) { + // If date is incorrect (1st second of unix time) then set it to the current date + dateCreated = new Date(); } - return contribution; + contribution.setDateCreated(dateCreated); + cursor.close(); + } else { + contribution.setDateCreated(new Date()); } + } + return contribution; + } - @Override - protected void onPostExecute(Contribution contribution) { - super.onPostExecute(contribution); - //Starts the upload. If commented out, user can proceed to next Fragment but upload doesn't happen - uploadService.queue(UploadService.ACTION_UPLOAD_FILE, contribution); - onComplete.onUploadStarted(contribution); - } - }.executeOnExecutor(Executors.newFixedThreadPool(1)); // TODO remove this by using a sensible thread handling strategy + 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); } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java index 63421a8e4f..656db534cc 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java @@ -1,6 +1,8 @@ package fr.free.nrw.commons.utils; import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -24,5 +26,9 @@ public static ArrayList extractCategories(String source) { return categories; } + public static List extractCategoriesFromList(String source) { + String[] categories = source.split("\\|"); + return Arrays.asList(categories); + } } From bbcd16b63c5ad044c059a47df89dc223659e4526 Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Sat, 23 Mar 2019 17:20:54 +0530 Subject: [PATCH 04/14] Add javadocs --- .../main/java/fr/free/nrw/commons/Media.java | 30 +++++++++++-------- .../free/nrw/commons/MediaDataExtractor.java | 6 ++++ .../free/nrw/commons/MediaWikiImageView.java | 6 ++++ .../free/nrw/commons/delete/DeleteHelper.java | 23 ++++++++++++++ .../commons/mwapi/OkHttpJsonApiClient.java | 18 +++++++++-- .../free/nrw/commons/review/ReviewHelper.java | 8 +++++ .../nrw/commons/upload/UploadController.java | 17 ++++++++++- .../commons/utils/MediaDataExtractorUtil.java | 5 ++++ 8 files changed, 97 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 725281d186..f399758a7f 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -4,10 +4,6 @@ import android.os.Parcel; import android.os.Parcelable; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; - -import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -27,9 +23,6 @@ public class Media implements Parcelable { - private static final Type MAP_TYPE = new TypeToken>() { - }.getType(); - public static Creator CREATOR = new Creator() { @Override public Media createFromParcel(Parcel parcel) { @@ -459,12 +452,24 @@ public boolean getRequestedDeletion(){ return requestedDeletion; } - public static Media from(MwQueryPage page, Gson gson) { + /** + * 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 + */ + 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); + } String categories = metadata.categories().value(); @@ -473,13 +478,12 @@ public static Media from(MwQueryPage page, Gson gson) { page.title(), "", 0, - DateUtils.getDateFromString(imageInfo.getMetadata().dateTimeOriginal().value()), - DateUtils.getDateFromString(imageInfo.getMetadata().dateTime().value()), - StringUtils.getParsedStringFromHtml(imageInfo.getMetadata().artist().value()) + DateUtils.getDateFromString(metadata.dateTimeOriginal().value()), + DateUtils.getDateFromString(metadata.dateTime().value()), + StringUtils.getParsedStringFromHtml(metadata.artist().value()) ); media.setDescriptions(metadata.imageDescription().value()); - media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(categories)); String latitude = metadata.gpsLatitude().value(); @@ -490,7 +494,7 @@ public static Media from(MwQueryPage page, Gson gson) { media.setCoordinates(latLng); } - media.setLicense(imageInfo.getMetadata().licenseShortName().value()); + media.setLicense(metadata.licenseShortName().value()); return media; } diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index 1d9fea1830..1daabb328a 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -27,6 +27,12 @@ public MediaDataExtractor(MediaWikiApi mwApi, this.mediaWikiApi = mwApi; } + /** + * 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 + */ public Single fetchMediaDetails(String filename) { Single mediaSingle = okHttpJsonApiClient.getMedia(filename, false); Single pageExistsSingle = mediaWikiApi.pageExists("Commons:Deletion_requests/" + filename); diff --git a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java index 42dbcde448..c78c550bb1 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java @@ -88,6 +88,12 @@ R.drawable.ic_image_black_24dp, getContext().getTheme())) .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 fetchMediaThumbnail(Media media) { if (media.getFilename() != null && thumbnailUrlCache.get(media.getFilename()) != null) { return Single.just(thumbnailUrlCache.get(media.getFilename())); diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java index 202a59a7d0..bdd8df16bb 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java @@ -30,6 +30,9 @@ import static androidx.core.app.NotificationCompat.DEFAULT_ALL; import static androidx.core.app.NotificationCompat.PRIORITY_HIGH; +/** + * Refactored async task to Rx + */ @Singleton public class DeleteHelper { private static final int NOTIFICATION_DELETE = 1; @@ -46,6 +49,13 @@ public DeleteHelper(MediaWikiApi mwApi, SessionManager sessionManager) { this.sessionManager = sessionManager; } + /** + * Public interface to nominate a particular media file for deletion + * @param context + * @param media + * @param reason + * @return + */ public Single makeDeletion(Context context, Media media, String reason) { notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationBuilder = new NotificationCompat.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL) @@ -57,6 +67,12 @@ public Single makeDeletion(Context context, Media media, String reason) 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; @@ -135,6 +151,13 @@ public boolean showDeletionNotification(Context context, Media media, boolean re 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); diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java index 3204a792a3..3ecdcfb51f 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -211,6 +211,13 @@ public Single getPictureOfTheDay() { return getMedia(template, true); } + /** + * Fetches Media object from the imageInfo API + * + * @param titles + * @param useGenerator + * @return + */ public Single getMedia(String titles, boolean useGenerator) { HttpUrl.Builder urlBuilder = HttpUrl .parse(commonsBaseUrl) @@ -233,16 +240,23 @@ public Single getMedia(String titles, boolean useGenerator) { 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(), gson); + 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") .addQueryParameter("iiextmetadatamultilang", String.valueOf(true)); return builder; } @@ -282,7 +296,7 @@ public Single> searchImages(String query, int offset) { MwQueryResponse mwQueryResponse = gson.fromJson(json, MwQueryResponse.class); List pages = mwQueryResponse.query().pages(); for (MwQueryPage page : pages) { - mediaList.add(Media.from(page, gson)); + mediaList.add(Media.from(page)); } } return mediaList; diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java index 5789afd988..ed6bb73e28 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java @@ -25,6 +25,14 @@ public ReviewHelper(OkHttpJsonApiClient okHttpJsonApiClient, MediaWikiApi mediaW this.mediaWikiApi = mediaWikiApi; } + /** + * 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 getRandomMedia() { return okHttpJsonApiClient.getRecentFileChanges() .map(RecentChangesImageUtils::findImageInRecentChanges) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index e8da010489..34603f0b8a 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -47,7 +47,6 @@ public interface ContributionUploadProgress { void onUploadStarted(Contribution contribution); } - @Inject public UploadController(SessionManager sessionManager, Context context, @@ -138,6 +137,12 @@ private void startUpload(final Contribution contribution, final ContributionUplo uploadTask(contribution, onComplete); } + /** + * Initiates the upload task + * @param contribution + * @param onComplete + * @return + */ private Disposable uploadTask(Contribution contribution, ContributionUploadProgress onComplete) { return Single.fromCallable(() -> makeUpload(contribution)) .subscribeOn(Schedulers.io()) @@ -145,6 +150,11 @@ private Disposable uploadTask(Contribution contribution, ContributionUploadProgr .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(); @@ -201,6 +211,11 @@ private Contribution makeUpload(Contribution contribution) { return contribution; } + /** + * When the contribution object is completely formed, the item is queued to the upload service + * @param contribution + * @param onComplete + */ private void onUploadCompleted(Contribution contribution, ContributionUploadProgress onComplete) { //Starts the upload. If commented out, user can proceed to next Fragment but upload doesn't happen uploadService.queue(UploadService.ACTION_UPLOAD_FILE, contribution); diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java index 656db534cc..8623ddfa09 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java @@ -26,6 +26,11 @@ public static ArrayList extractCategories(String source) { return categories; } + /** + * Extracts a list of categories from | separated category string + * @param source + * @return + */ public static List extractCategoriesFromList(String source) { String[] categories = source.split("\\|"); return Arrays.asList(categories); From 81116e882365ff5c98ab42e02a11794f5576c5c1 Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Mon, 25 Mar 2019 11:04:27 +0530 Subject: [PATCH 05/14] Fix build --- .../free/nrw/commons/MediaDataExtractor.java | 7 +++++- .../commons/media/MediaDetailFragment.java | 4 ++-- .../nrw/commons/review/ReviewActivity.java | 24 +++++++------------ 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index 1daabb328a..108dcdd34f 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -8,6 +8,7 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; import io.reactivex.Single; +import timber.log.Timber; /** * Fetch additional media data from the network that we don't store locally. @@ -49,6 +50,10 @@ public Single fetchMediaDetails(String filename) { private Single getDiscussion(String filename) { return mediaWikiApi.fetchMediaByFilename(filename.replace("File", "File talk")) .flatMap(mediaResult -> mediaWikiApi.parseWikicode(mediaResult.getWikiSource())) - .map(discussion -> Html.fromHtml(discussion).toString()); + .map(discussion -> Html.fromHtml(discussion).toString()) + .onErrorReturn(throwable -> { + Timber.e(throwable, "Error occurred while fetching discussion"); + return ""; + }); } } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 42b4c35d18..d297ffc167 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -266,8 +266,8 @@ private void displayMediaDetails() { //Always load image from Internet to allow viewing the desc, license, and cats image.setMedia(media); title.setText(media.getDisplayTitle()); - desc.setText(""); // fill in from network... - license.setText(""); // fill in from network... + desc.setText(media.getDescription()); + license.setText(media.getLicense()); Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename()) .subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java index 347be928f7..114c72736d 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java @@ -37,8 +37,6 @@ 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,20 +76,6 @@ public static void startYourself(Context context, String title) { Intent reviewActivity = new Intent(context, ReviewActivity.class); context.startActivity(reviewActivity); } - @Inject - ReviewHelper reviewHelper; - @Inject - DeleteHelper deleteHelper; - - @Inject - DeleteHelper deleteHelper; - - public ReviewPagerAdapter reviewPagerAdapter; - - public ReviewController reviewController; - - @BindView(R.id.reviewPagerIndicator) - public CirclePageIndicator pagerIndicator; private CompositeDisposable compositeDisposable = new CompositeDisposable(); From a53846e87b0300c28bd1165d149f0ba3fb1c659e Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Tue, 26 Mar 2019 23:18:22 +0530 Subject: [PATCH 06/14] Fix build --- app/src/main/java/fr/free/nrw/commons/Media.java | 7 +------ .../fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java | 9 +++++++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 512937f6a9..3ca6920b9e 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -9,7 +9,6 @@ import java.util.Date; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -486,11 +485,7 @@ public static Media from(MwQueryPage page) { 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.setDescriptions(Collections.singletonMap("default", metadata.imageDescription().value())); media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories().value())); String latitude = metadata.gpsLatitude().value(); String longitude = metadata.gpsLongitude().value(); diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java index 51ff48647d..cc32b9ac8c 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -267,8 +267,13 @@ public Single getMedia(String titles, boolean useGenerator) { 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") - .addQueryParameter("iiextmetadatamultilang", String.valueOf(true)); + .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; } From 78c019bc3e73685bfe4f8c48811f0edbc26d642b Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Wed, 27 Mar 2019 00:27:16 +0530 Subject: [PATCH 07/14] With tests --- app/build.gradle | 5 + .../free/nrw/commons/MediaDataExtractor.java | 8 +- .../free/nrw/commons/delete/DeleteHelper.java | 38 ++------ .../notification/NotificationHelper.java | 53 +++++++++++ .../nrw/commons/MediaDataExtractorTest.kt | 54 +++++++++++ .../nrw/commons/delete/DeleteHelperTest.kt | 75 +++++++++++++++ .../commons/mwapi/OkHttpJsonApiClientTest.kt | 93 ++++++++++++++++--- 7 files changed, 283 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/MediaDataExtractorTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt diff --git a/app/build.gradle b/app/build.gradle index 84f929c361..d28963f557 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,6 +64,11 @@ dependencies { testImplementation 'com.nhaarman:mockito-kotlin:1.5.0' testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0' + testImplementation 'org.powermock:powermock-api-mockito:1.6.2' + testImplementation 'org.powermock:powermock-module-junit4-rule-agent:1.6.2' + testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.2' + testImplementation 'org.powermock:powermock-module-junit4:1.6.2' + // Android testing androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index 108dcdd34f..6bde0b6de9 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -5,6 +5,7 @@ import javax.inject.Inject; import javax.inject.Singleton; +import androidx.core.text.HtmlCompat; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; import io.reactivex.Single; @@ -47,10 +48,15 @@ public Single fetchMediaDetails(String filename) { }); } + /** + * Fetch talk page from the MediaWiki API + * @param filename + * @return + */ private Single getDiscussion(String filename) { return mediaWikiApi.fetchMediaByFilename(filename.replace("File", "File talk")) .flatMap(mediaResult -> mediaWikiApi.parseWikicode(mediaResult.getWikiSource())) - .map(discussion -> Html.fromHtml(discussion).toString()) + .map(discussion -> HtmlCompat.fromHtml(discussion, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()) .onErrorReturn(throwable -> { Timber.e(throwable, "Error occurred while fetching discussion"); return ""; diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java index bdd8df16bb..28d2249896 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java @@ -1,7 +1,5 @@ package fr.free.nrw.commons.delete; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; @@ -15,38 +13,34 @@ import javax.inject.Singleton; import androidx.appcompat.app.AlertDialog; -import androidx.core.app.NotificationCompat; 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.mwapi.MediaWikiApi; +import fr.free.nrw.commons.notification.NotificationHelper; import fr.free.nrw.commons.review.ReviewActivity; import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Single; import timber.log.Timber; -import static androidx.core.app.NotificationCompat.DEFAULT_ALL; -import static androidx.core.app.NotificationCompat.PRIORITY_HIGH; +import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_DELETE; /** * Refactored async task to Rx */ @Singleton public class DeleteHelper { - private static final int NOTIFICATION_DELETE = 1; - private final MediaWikiApi mwApi; private final SessionManager sessionManager; - - private NotificationManager notificationManager; - private NotificationCompat.Builder notificationBuilder; + private final NotificationHelper notificationHelper; @Inject - public DeleteHelper(MediaWikiApi mwApi, SessionManager sessionManager) { + public DeleteHelper(MediaWikiApi mwApi, + SessionManager sessionManager, + NotificationHelper notificationHelper) { this.mwApi = mwApi; this.sessionManager = sessionManager; + this.notificationHelper = notificationHelper; } /** @@ -57,9 +51,6 @@ public DeleteHelper(MediaWikiApi mwApi, SessionManager sessionManager) { * @return */ public Single makeDeletion(Context context, Media media, String reason) { - notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationBuilder = new NotificationCompat.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL) - .setOnlyAlertOnce(true); ViewUtil.showShortToast(context, "Trying to nominate " + media.getDisplayTitle() + " for deletion"); return Single.fromCallable(() -> delete(media, reason)) @@ -122,7 +113,7 @@ private boolean delete(Media media, String reason) { return true; } - public boolean showDeletionNotification(Context context, Media media, boolean result) { + private boolean showDeletionNotification(Context context, Media media, boolean result) { String message; String title = "Nominating for Deletion"; @@ -134,20 +125,9 @@ public boolean showDeletionNotification(Context context, Media media, boolean re 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()); + notificationHelper.showNotification(context, title, message, NOTIFICATION_DELETE, browserIntent); return result; } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java new file mode 100644 index 0000000000..b5d53a8de3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java @@ -0,0 +1,53 @@ +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; + +@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 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()); + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/MediaDataExtractorTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/MediaDataExtractorTest.kt new file mode 100644 index 0000000000..214151f9a0 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/MediaDataExtractorTest.kt @@ -0,0 +1,54 @@ +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 + +class MediaDataExtractorTest { + + @Mock + internal var mwApi: MediaWikiApi? = null + + @Mock + internal var okHttpJsonApiClient: OkHttpJsonApiClient? = null + + @InjectMocks + var mediaDataExtractor: MediaDataExtractor? = null + + @Before + @Throws(Exception::class) + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @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) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt new file mode 100644 index 0000000000..24de836d6d --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt @@ -0,0 +1,75 @@ +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.ViewUtil +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.powermock.api.mockito.PowerMockito +import org.powermock.core.classloader.annotations.PrepareForTest +import org.powermock.modules.junit4.PowerMockRunner + +@RunWith(PowerMockRunner::class) +@PrepareForTest(ViewUtil::class) +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 media: Media? = null + + @InjectMocks + var deleteHelper: DeleteHelper? = null + + @Test + fun makeDeletion() { + `when`(mwApi!!.editToken).thenReturn("token") + `when`(sessionManager!!.authCookie).thenReturn("Mock cookie") + `when`(sessionManager!!.currentAccount).thenReturn(Account("TestUser", "Test")) + + MockitoAnnotations.initMocks(this) + PowerMockito.mockStatic(ViewUtil::class.java) + + `when`(media!!.displayTitle).thenReturn("Test file") + `when`(media!!.filename).thenReturn("Test file.jpg") + + val makeDeletion = deleteHelper!!.makeDeletion(context, media, "Test reason").blockingGet() + assertTrue(makeDeletion) + } + + @Test + fun makeDeletionForNullToken() { + `when`(mwApi!!.editToken).thenReturn(null) + `when`(sessionManager!!.authCookie).thenReturn("Mock cookie") + `when`(sessionManager!!.currentAccount).thenReturn(Account("TestUser", "Test")) + + MockitoAnnotations.initMocks(this) + PowerMockito.mockStatic(ViewUtil::class.java) + + `when`(media!!.displayTitle).thenReturn("Test file") + `when`(media!!.filename).thenReturn("Test file.jpg") + + val makeDeletion = deleteHelper!!.makeDeletion(context, media, "Test reason").blockingGet() + assertFalse(makeDeletion) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt index 9d2a468f5d..b32ae953d9 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt @@ -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,6 +23,7 @@ import org.mockito.Mockito.`when` import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import java.net.URLDecoder +import kotlin.random.Random @RunWith(RobolectricTestRunner::class) @Config(constants = BuildConfig::class, sdk = [23], application = TestCommonsApplication::class) @@ -130,6 +133,48 @@ class OkHttpJsonApiClientTest { assertEquals(categoryImagesContinued.size, 2) } + @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", body["iiextmetadatafilter"]) + } + } + + assert(media is Media) + } + + @Test + fun getPictureOfTheDay() { + 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", body["iiextmetadatafilter"]) + } + } + + assert(media is Media) + } + private fun testFirstPageSearchQuery() { val categoryImages = testObject.getMediaList("search", "Watercraft moored off shore")!!.blockingGet() @@ -171,33 +216,55 @@ class OkHttpJsonApiClientTest { } 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\":\"Raphael\\n\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511
date QS:P571,+1511-00-00T00:00:00Z/9
\",\"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\":\"Raphael\\n\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511
date QS:P571,+1511-00-00T00:00:00Z/9
\",\"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\":\"Raphael\\n\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511
date QS:P571,+1511-00-00T00:00:00Z/9
\",\"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\":\"Raphael\\n\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511
date QS:P571,+1511-00-00T00:00:00Z/9
\",\"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\":\"Raphael\\n\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511
date QS:P571,+1511-00-00T00:00:00Z/9
\",\"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\":\"Raphael\\n\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511
date QS:P571,+1511-00-00T00:00:00Z/9
\",\"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) + } + + 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\":\"Raphael\\n\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511
date QS:P571,+1511-00-00T00:00:00Z/9
\",\"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\":\"Raphael\\n\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511
date QS:P571,+1511-00-00T00:00:00Z/9
\",\"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() + 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 } + 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\":\"Raphael\\n\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511
date QS:P571,+1511-00-00T00:00:00Z/9
\",\"source\":\"commons-desc-page\"},\"LicenseShortName\":{\"value\":\"Public domain\",\"source\":\"commons-desc-page\",\"hidden\":\"\"}}}]}" + } + private fun assertBasicRequestParameters(server: MockWebServer, method: String): RecordedRequest = server.takeRequest().let { Assert.assertEquals("/", it.requestUrl.encodedPath()) Assert.assertEquals(method, it.method) From 095de86d1a8e46b82d364566f17b85f9ccf5d1b8 Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Wed, 27 Mar 2019 12:01:46 +0530 Subject: [PATCH 08/14] Add more javadocs --- .../nrw/commons/notification/NotificationHelper.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java index b5d53a8de3..f558df9918 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java @@ -15,6 +15,10 @@ 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 { @@ -31,6 +35,14 @@ public NotificationHelper(Context context) { .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, From 791f57fbe0057e50cb9b172440501b3459a50dbe Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Wed, 27 Mar 2019 23:12:43 +0530 Subject: [PATCH 09/14] With more tests --- app/build.gradle | 5 ---- .../main/java/fr/free/nrw/commons/Media.java | 8 ++++++- .../free/nrw/commons/delete/DeleteHelper.java | 7 ++++-- .../fr/free/nrw/commons/utils/ViewUtil.java | 19 ++++++++++----- .../nrw/commons/delete/DeleteHelperTest.kt | 23 ++++++++----------- .../nrw/commons/upload/UploadModelTest.kt | 1 - 6 files changed, 34 insertions(+), 29 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d28963f557..84f929c361 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,11 +64,6 @@ dependencies { testImplementation 'com.nhaarman:mockito-kotlin:1.5.0' testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0' - testImplementation 'org.powermock:powermock-api-mockito:1.6.2' - testImplementation 'org.powermock:powermock-module-junit4-rule-agent:1.6.2' - testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.2' - testImplementation 'org.powermock:powermock-module-junit4:1.6.2' - // Android testing androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 3ca6920b9e..ac863120f7 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -9,6 +9,7 @@ import java.util.Date; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -485,7 +486,12 @@ public static Media from(MwQueryPage page) { StringUtils.getParsedStringFromHtml(metadata.artist().value()) ); - media.setDescriptions(Collections.singletonMap("default", metadata.imageDescription().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(); diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java index 28d2249896..f666759e12 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java @@ -33,14 +33,17 @@ public class DeleteHelper { private final MediaWikiApi mwApi; private final SessionManager sessionManager; private final NotificationHelper notificationHelper; + private final ViewUtil viewUtil; @Inject public DeleteHelper(MediaWikiApi mwApi, SessionManager sessionManager, - NotificationHelper notificationHelper) { + NotificationHelper notificationHelper, + ViewUtil viewUtil) { this.mwApi = mwApi; this.sessionManager = sessionManager; this.notificationHelper = notificationHelper; + this.viewUtil = viewUtil; } /** @@ -51,7 +54,7 @@ public DeleteHelper(MediaWikiApi mwApi, * @return */ public Single makeDeletion(Context context, Media media, String reason) { - ViewUtil.showShortToast(context, "Trying to nominate " + media.getDisplayTitle() + " for deletion"); + viewUtil.showShortToast(context, "Trying to nominate " + media.getDisplayTitle() + " for deletion"); return Single.fromCallable(() -> delete(media, reason)) .flatMap(result -> Single.fromCallable(() -> diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java index f9a246a8b4..096e436137 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java @@ -2,18 +2,25 @@ 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 javax.inject.Inject; +import javax.inject.Singleton; + +import androidx.annotation.StringRes; + +@Singleton 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"; + @Inject + public ViewUtil() { + + } /** * Utility function to show short snack bar @@ -44,7 +51,7 @@ public static void showLongToast(Context context, @StringRes int stringResourceI ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_LONG).show()); } - public static void showShortToast(Context context, String text) { + public void showShortToast(Context context, String text) { if (context == null) { return; } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt index 24de836d6d..c3bbc0f948 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt @@ -9,18 +9,13 @@ import fr.free.nrw.commons.notification.NotificationHelper import fr.free.nrw.commons.utils.ViewUtil import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue +import org.junit.Before import org.junit.Test -import org.junit.runner.RunWith import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -import org.powermock.api.mockito.PowerMockito -import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.modules.junit4.PowerMockRunner -@RunWith(PowerMockRunner::class) -@PrepareForTest(ViewUtil::class) class DeleteHelperTest { @Mock @@ -35,21 +30,25 @@ class DeleteHelperTest { @Mock internal var context: Context? = null + @Mock + internal var viewUtil: ViewUtil? = null + @Mock internal var media: Media? = null @InjectMocks var deleteHelper: DeleteHelper? = null + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + @Test fun makeDeletion() { `when`(mwApi!!.editToken).thenReturn("token") `when`(sessionManager!!.authCookie).thenReturn("Mock cookie") `when`(sessionManager!!.currentAccount).thenReturn(Account("TestUser", "Test")) - - MockitoAnnotations.initMocks(this) - PowerMockito.mockStatic(ViewUtil::class.java) - `when`(media!!.displayTitle).thenReturn("Test file") `when`(media!!.filename).thenReturn("Test file.jpg") @@ -62,10 +61,6 @@ class DeleteHelperTest { `when`(mwApi!!.editToken).thenReturn(null) `when`(sessionManager!!.authCookie).thenReturn("Mock cookie") `when`(sessionManager!!.currentAccount).thenReturn(Account("TestUser", "Test")) - - MockitoAnnotations.initMocks(this) - PowerMockito.mockStatic(ViewUtil::class.java) - `when`(media!!.displayTitle).thenReturn("Test file") `when`(media!!.filename).thenReturn("Test file.jpg") diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadModelTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadModelTest.kt index 3bb29fbb22..ca7a562e61 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadModelTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadModelTest.kt @@ -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 From f3b170087ce7f65f6233e20a40be46bcba4a2018 Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Wed, 27 Mar 2019 23:46:11 +0530 Subject: [PATCH 10/14] With review helper test --- .../media/RecentChangesImageUtils.java | 49 ---------------- .../free/nrw/commons/review/ReviewHelper.java | 48 ++++++++++++++-- .../nrw/commons/review/ReviewHelperTest.kt | 57 +++++++++++++++++++ 3 files changed, 101 insertions(+), 53 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/media/RecentChangesImageUtils.java create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt diff --git a/app/src/main/java/fr/free/nrw/commons/media/RecentChangesImageUtils.java b/app/src/main/java/fr/free/nrw/commons/media/RecentChangesImageUtils.java deleted file mode 100644 index 4a00abc3fb..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/RecentChangesImageUtils.java +++ /dev/null @@ -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 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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java index ed6bb73e28..4aa1ecf22e 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java @@ -1,21 +1,26 @@ package fr.free.nrw.commons.review; -import android.util.Pair; +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; @@ -35,11 +40,11 @@ public ReviewHelper(OkHttpJsonApiClient okHttpJsonApiClient, MediaWikiApi mediaW */ Single getRandomMedia() { return okHttpJsonApiClient.getRecentFileChanges() - .map(RecentChangesImageUtils::findImageInRecentChanges) + .map(this::findImageInRecentChanges) .flatMap(title -> mediaWikiApi.pageExists("Commons:Deletion_requests/" + title) .map(pageExists -> new Pair<>(title, pageExists))) .map((Pair pair) -> { - if (!pair.second) { + if (pair.second) { return new Media(pair.first.replace("File:", "")); } throw new Exception("Page does not exist"); @@ -49,4 +54,39 @@ Single getRandomMedia() { Single getFirstRevisionOfFile(String fileName) { return okHttpJsonApiClient.getFirstRevisionOfFile(fileName); } + + @Nullable + public String findImageInRecentChanges(List 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; + } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt new file mode 100644 index 0000000000..d967b8fca9 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt @@ -0,0 +1,57 @@ +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 + +class ReviewHelperTest { + + @Mock + internal var okHttpJsonApiClient: OkHttpJsonApiClient? = null + @Mock + internal var mediaWikiApi: MediaWikiApi? = null + + @InjectMocks + var reviewHelper: ReviewHelper? = null + + @Before + @Throws(Exception::class) + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @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 + 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) + } +} \ No newline at end of file From 7dc244b09998446084a3458752c88eaa4e812052 Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Thu, 28 Mar 2019 11:50:21 +0530 Subject: [PATCH 11/14] With suggested changes and java docs --- .../java/fr/free/nrw/commons/LicenseList.java | 127 ------------------ .../main/java/fr/free/nrw/commons/Media.java | 105 ++++++++------- .../commons/media/MediaDetailFragment.java | 52 ++----- .../commons/mwapi/OkHttpJsonApiClient.java | 20 ++- .../commons/settings/SettingsFragment.java | 1 - .../main/res/layout/fragment_media_detail.xml | 2 +- .../nrw/commons/MediaDataExtractorTest.kt | 19 ++- .../nrw/commons/delete/DeleteHelperTest.kt | 44 +++--- .../commons/mwapi/OkHttpJsonApiClientTest.kt | 80 +++++++++-- 9 files changed, 202 insertions(+), 248 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/LicenseList.java diff --git a/app/src/main/java/fr/free/nrw/commons/LicenseList.java b/app/src/main/java/fr/free/nrw/commons/LicenseList.java deleted file mode 100644 index a6c6f9cad6..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/LicenseList.java +++ /dev/null @@ -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 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 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; - } - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index ac863120f7..46b0bbbabb 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -50,6 +50,7 @@ public Media[] newArray(int i) { protected int width; protected int height; protected String license; + protected String licenseUrl; protected String creator; protected ArrayList categories; // as loaded at runtime? protected boolean requestedDeletion; @@ -332,12 +333,66 @@ public String getLicense() { 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; + this.licenseUrl = licenseUrl; } /** @@ -457,51 +512,11 @@ public boolean getRequestedDeletion(){ } /** - * 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; } } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index d297ffc167..d3f2174f4a 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -8,7 +8,6 @@ import android.os.Bundle; import android.text.Editable; import android.text.Html; -import android.text.TextUtils; import android.text.TextWatcher; import android.util.TypedValue; import android.view.LayoutInflater; @@ -30,27 +29,25 @@ import javax.inject.Inject; -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.DeleteHelper; import fr.free.nrw.commons.delete.ReasonBuilder; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.location.LatLng; -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 io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -92,6 +89,8 @@ public static MediaDetailFragment forMedia(int index, boolean editable, boolean ReasonBuilder reasonBuilder; @Inject DeleteHelper deleteHelper; + @Inject + ViewUtil viewUtil; private int initialListTop = 0; @@ -102,7 +101,7 @@ public static MediaDetailFragment forMedia(int index, boolean editable, boolean @BindView(R.id.mediaDetailTitle) TextView title; @BindView(R.id.mediaDetailDesc) - TextView desc; + HtmlTextView desc; @BindView(R.id.mediaDetailAuthor) TextView author; @BindView(R.id.mediaDetailLicense) @@ -132,7 +131,6 @@ public static MediaDetailFragment forMedia(int index, boolean editable, boolean private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once! private ViewTreeObserver.OnScrollChangedListener scrollListener; private DataSetObserver dataObserver; - 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; @@ -194,8 +192,6 @@ && getParentFragment() instanceof MediaDetailPagerFragment) { 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); @@ -266,7 +262,7 @@ private void displayMediaDetails() { //Always load image from Internet to allow viewing the desc, license, and cats image.setMedia(media); title.setText(media.getDisplayTitle()); - desc.setText(media.getDescription()); + desc.setHtmlText(media.getDescription()); license.setText(media.getLicense()); Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename()) @@ -294,7 +290,7 @@ public void onDestroyView() { } private void setTextFields(Media media) { - desc.setText(prettyDescription(media)); + desc.setHtmlText(prettyDescription(media)); license.setText(prettyLicense(media)); coordinates.setText(prettyCoordinates(media)); uploadedDate.setText(prettyUploadedDate(media)); @@ -322,15 +318,10 @@ private void setTextFields(Media media) { @OnClick(R.id.mediaDetailLicense) public void onMediaDetailLicenceClicked(){ - if (!TextUtils.isEmpty(licenseLink(media))) { - openWebBrowser(licenseLink(media)); + if (!StringUtils.isNullOrWhiteSpace(media.getLicenseUrl())) { + openWebBrowser(media.getLicenseUrl()); } 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)); } } @@ -516,12 +507,7 @@ private String prettyLicense(Media media) { if (licenseKey == null || licenseKey.equals("")) { return getString(R.string.detail_license_empty); } - License licenseObj = licenseList.get(licenseKey); - if (licenseObj == null) { - return licenseKey; - } else { - return licenseObj.getName(); - } + return licenseKey; } private String prettyUploadedDate(Media media) { @@ -554,20 +540,6 @@ private void checkDeletion(Media media){ } } - 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()); - } - } - private void openWebBrowser(String url) { Intent browser = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); //check if web browser available diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java index cc32b9ac8c..91e0cd2657 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -42,6 +42,9 @@ import okhttp3.Response; import timber.log.Timber; +/** + * Test methods in ok http api client + */ @Singleton public class OkHttpJsonApiClient { @@ -225,8 +228,8 @@ public Single getPictureOfTheDay() { /** * Fetches Media object from the imageInfo API * - * @param titles - * @param useGenerator + * @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 getMedia(String titles, boolean useGenerator) { @@ -267,7 +270,7 @@ public Single getMedia(String titles, boolean useGenerator) { 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"); + .addQueryParameter("iiextmetadatafilter", "DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl"); String language = Locale.getDefault().getLanguage(); if (!StringUtils.isNullOrWhiteSpace(language)) { @@ -281,7 +284,7 @@ private HttpUrl.Builder appendMediaProperties(HttpUrl.Builder 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 @@ -329,8 +332,8 @@ public Single> getMediaList(String queryType, String keyword) { /** * 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") @@ -354,6 +357,11 @@ private void appendQueryContinueValues(String query, HttpUrl.Builder urlBuilder) } } + /** + * 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") diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index fd1ffc2919..abd54158b4 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -21,7 +21,6 @@ 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; diff --git a/app/src/main/res/layout/fragment_media_detail.xml b/app/src/main/res/layout/fragment_media_detail.xml index 6ac2d737bf..bd1c6c89e6 100644 --- a/app/src/main/res/layout/fragment_media_detail.xml +++ b/app/src/main/res/layout/fragment_media_detail.xml @@ -133,7 +133,7 @@ android:textSize="@dimen/normal_text" android:textStyle="bold" /> - + 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", body["iiextmetadatafilter"]) + } + } + + assert(media is Media) + } + private fun testFirstPageSearchQuery() { val categoryImages = testObject.getMediaList("search", "Watercraft moored off shore")!!.blockingGet() @@ -209,7 +266,7 @@ 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) @@ -231,6 +288,9 @@ class OkHttpJsonApiClientTest { 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, @@ -255,6 +315,9 @@ class OkHttpJsonApiClientTest { return mockResponse } + /** + * Generate test media json object + */ private fun getMediaPage(random: Random): String { val pageID = random.nextInt() val id = random.nextInt() @@ -265,22 +328,23 @@ class OkHttpJsonApiClientTest { 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\":\"Raphael\\n\",\"source\":\"commons-desc-page\"},\"ImageDescription\":{\"value\":\"test desc\",\"source\":\"commons-desc-page\"},\"DateTimeOriginal\":{\"value\":\"1511
date QS:P571,+1511-00-00T00:00:00Z/9
\",\"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().apply { request.requestUrl.let { it.queryParameterNames().forEach { name -> put(name, it.queryParameter(name)) } } } - private fun parseBody(body: String): Map = HashMap().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")) - } - } } \ No newline at end of file From e3e00a3be1a58e9b4987816b5da3d34215eb73f6 Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Thu, 28 Mar 2019 15:20:50 +0530 Subject: [PATCH 12/14] Fix tests --- .../nrw/commons/mwapi/OkHttpJsonApiClientTest.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt index a1970ce928..0a7bad2d50 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt @@ -107,7 +107,7 @@ 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"]) } } @@ -151,7 +151,7 @@ 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"]) } } @@ -174,7 +174,7 @@ class OkHttpJsonApiClientTest { 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", body["iiextmetadatafilter"]) + Assert.assertEquals("DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl", body["iiextmetadatafilter"]) } } @@ -200,7 +200,7 @@ class OkHttpJsonApiClientTest { 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", body["iiextmetadatafilter"]) + Assert.assertEquals("DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl", body["iiextmetadatafilter"]) } } @@ -225,7 +225,7 @@ class OkHttpJsonApiClientTest { 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", body["iiextmetadatafilter"]) + Assert.assertEquals("DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl", body["iiextmetadatafilter"]) } } @@ -246,7 +246,7 @@ 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) From 091a475422bb86ce5fa6f990fa290355618511fb Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Thu, 28 Mar 2019 20:42:33 +0530 Subject: [PATCH 13/14] Fix tests and codacy warnings --- .../nrw/commons/delete/DeleteHelperTest.kt | 5 ++--- .../commons/mwapi/OkHttpJsonApiClientTest.kt | 6 ++--- .../nrw/commons/review/ReviewHelperTest.kt | 22 ++++++++++++++----- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt index a8ff9ca1ed..0aad6ae448 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt @@ -7,8 +7,7 @@ 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.assertFalse -import junit.framework.Assert.assertNotNull +import junit.framework.Assert.* import org.junit.Before import org.junit.Test import org.mockito.InjectMocks @@ -63,7 +62,7 @@ class DeleteHelperTest { val makeDeletion = deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() assertNotNull(makeDeletion) - assertFalse(makeDeletion!!) + assertTrue(makeDeletion!!) } /** diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt index 0a7bad2d50..4e1bd8b9c8 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt @@ -215,7 +215,7 @@ class OkHttpJsonApiClientTest { val template = "Template:Potd/" + DateUtils.getCurrentDate() server.enqueue(getMediaList("", "", "", 1)) - val media = testObject.pictureOfTheDay!!.blockingGet() + val media = testObject.pictureOfTheDay?.blockingGet() assertBasicRequestParameters(server, "GET").let { request -> parseQueryParams(request).let { body -> @@ -253,7 +253,7 @@ class OkHttpJsonApiClientTest { } 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 -> @@ -269,7 +269,7 @@ class OkHttpJsonApiClientTest { 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 { diff --git a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt index d967b8fca9..2c7599b315 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt @@ -16,6 +16,9 @@ import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations +/** + * Test class for ReviewHelper + */ class ReviewHelperTest { @Mock @@ -26,31 +29,40 @@ class ReviewHelperTest { @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) + `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())) + `when`(mediaWikiApi?.pageExists(ArgumentMatchers.anyString())) .thenReturn(Single.just(true)) - val randomMedia = reviewHelper!!.randomMedia.blockingGet() + val randomMedia = reviewHelper?.randomMedia?.blockingGet() assertTrue(randomMedia is Media) } + /** + * Test for getting first revision of file + */ @Test fun getFirstRevisionOfFile() { - `when`(okHttpJsonApiClient!!.getFirstRevisionOfFile(ArgumentMatchers.anyString())) + `when`(okHttpJsonApiClient?.getFirstRevisionOfFile(ArgumentMatchers.anyString())) .thenReturn(Single.just(mock(MwQueryPage.Revision::class.java))) - val firstRevisionOfFile = reviewHelper!!.getFirstRevisionOfFile("Test.jpg").blockingGet() + val firstRevisionOfFile = reviewHelper?.getFirstRevisionOfFile("Test.jpg")?.blockingGet() assertTrue(firstRevisionOfFile is MwQueryPage.Revision) } From 5ca9415c0e3d10186aeb372b9c9cc5112792cccb Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Thu, 28 Mar 2019 20:37:46 +0000 Subject: [PATCH 14/14] Fix broken link on license media detail --- app/src/main/java/fr/free/nrw/commons/Media.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 46b0bbbabb..4688e54eb0 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -392,6 +392,10 @@ public String getLicenseUrl() { */ public void setLicenseInformation(String license, String licenseUrl) { this.license = license; + + if (!licenseUrl.startsWith("http://") && !licenseUrl.startsWith("https://")) { + licenseUrl = "https://" + licenseUrl; + } this.licenseUrl = licenseUrl; }