From d53858f7d25972f5c707845f44d0c003f592737f Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Tue, 4 Jul 2017 14:14:17 -0500 Subject: [PATCH 1/7] Gentle version bump and added the dexcount gradle plugin --- app/build.gradle | 5 ++++- build.gradle | 1 + gradle.properties | 5 +++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 567be9b70..c4794016e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'jacoco-android' apply from: 'quality.gradle' +apply plugin: 'com.getkeepsafe.dexcount' dependencies { compile 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07' @@ -13,10 +14,12 @@ dependencies { compile "com.android.support:support-v4:${project.supportLibVersion}" compile "com.android.support:appcompat-v7:${project.supportLibVersion}" compile "com.android.support:design:${project.supportLibVersion}" - compile 'com.google.code.gson:gson:2.7' + compile 'com.google.code.gson:gson:2.8.0' compile "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" annotationProcessor "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" compile 'com.jakewharton.timber:timber:4.5.1' + compile 'com.squareup.okhttp3:okhttp:3.8.1' + compile 'com.squareup.okio:okio:1.13.0' compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.0.2@aar'){ transitive=true } diff --git a/build.gradle b/build.gradle index aadbcc875..4929fec36 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,7 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:${project.gradleVersion}" classpath 'com.dicedmelon.gradle:jacoco-android:0.1.1' + classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.7.1' } } diff --git a/gradle.properties b/gradle.properties index b07acc3bb..aa730345f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ gradleVersion = 2.3.0 -supportLibVersion = 25.2.0 +supportLibVersion = 25.3.1 compileSdkVersion = android-25 buildToolsVersion = 25.0.1 @@ -12,6 +12,7 @@ targetSdkVersion = 23 android.useDeprecatedNdk=true # Library dependencies -BUTTERKNIFE_VERSION=8.4.0 +BUTTERKNIFE_VERSION=8.6.0 GUAVA_VERSION=19.0 +org.gradle.jvmargs=-Xmx1536M \ No newline at end of file From 5396fc6ed0ee3275907f6a4d400d5c1b5a8aa964 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Tue, 4 Jul 2017 14:23:13 -0500 Subject: [PATCH 2/7] Optimized imports before starting --- .../main/java/fr/free/nrw/commons/Utils.java | 16 +++--- .../fr/free/nrw/commons/WelcomeActivity.java | 1 - .../fr/free/nrw/commons/auth/AccountUtil.java | 1 + .../free/nrw/commons/auth/LoginActivity.java | 8 ++- .../fr/free/nrw/commons/auth/LoginTask.java | 2 +- .../BackgroundPoolExceptionHandler.java | 1 + .../commons/contributions/Contribution.java | 53 +++++++++---------- .../contributions/ContributionController.java | 3 -- .../ContributionsListFragment.java | 1 - .../contributions/UploadCountClient.java | 1 - .../media/MediaDetailPagerFragment.java | 2 +- .../nrw/commons/nearby/NearbyActivity.java | 2 +- .../free/nrw/commons/upload/GPSExtractor.java | 1 - .../commons/upload/MultipleShareActivity.java | 5 +- .../upload/MultipleUploadListFragment.java | 1 - .../nrw/commons/upload/ShareActivity.java | 2 +- .../commons/upload/SingleUploadFragment.java | 2 +- .../nrw/commons/upload/UploadController.java | 2 +- .../free/nrw/commons/utils/LengthUtils.java | 4 +- 19 files changed, 51 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/Utils.java b/app/src/main/java/fr/free/nrw/commons/Utils.java index 8279af808..c44a0465a 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -7,8 +7,12 @@ import android.preference.PreferenceManager; import android.text.Html; import android.text.Spanned; -import fr.free.nrw.commons.settings.Prefs; -import timber.log.Timber; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.codec.net.URLCodec; +import org.w3c.dom.Node; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; import java.io.BufferedInputStream; import java.io.IOException; @@ -34,12 +38,8 @@ import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.codec.net.URLCodec; -import org.w3c.dom.Node; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; +import fr.free.nrw.commons.settings.Prefs; +import timber.log.Timber; public class Utils { diff --git a/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java b/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java index 0a494f9c1..e35c7e6d0 100644 --- a/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java @@ -9,7 +9,6 @@ import com.viewpagerindicator.CirclePageIndicator; import butterknife.BindView; import butterknife.ButterKnife; -import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.theme.BaseActivity; public class WelcomeActivity extends BaseActivity { diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java index 773eb5d63..479b47444 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java @@ -7,6 +7,7 @@ import android.content.ContentResolver; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; + import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider; diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index 5ddd97f63..432c06c80 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -15,9 +15,13 @@ import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; - import android.widget.Toast; -import fr.free.nrw.commons.*; + +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; import timber.log.Timber; diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java index 6db5f8654..b7e1ab252 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java @@ -9,8 +9,8 @@ import android.os.Bundle; import java.io.IOException; import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.EventLog; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.mwapi.EventLog; import timber.log.Timber; class LoginTask extends AsyncTask { diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/BackgroundPoolExceptionHandler.java b/app/src/main/java/fr/free/nrw/commons/concurrency/BackgroundPoolExceptionHandler.java index f971eb8cc..2c730a0d1 100644 --- a/app/src/main/java/fr/free/nrw/commons/concurrency/BackgroundPoolExceptionHandler.java +++ b/app/src/main/java/fr/free/nrw/commons/concurrency/BackgroundPoolExceptionHandler.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.concurrency; import android.support.annotation.NonNull; + import fr.free.nrw.commons.BuildConfig; public class BackgroundPoolExceptionHandler implements ExceptionHandler { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java index e58be04e1..983f361a4 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java @@ -15,10 +15,9 @@ import java.util.Locale; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.EventLog; import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.settings.Prefs; public class Contribution extends Media { @@ -63,8 +62,6 @@ public class Contribution extends Media { isMultiple = multiple; } - public EventLog.LogBuilder event; - public Contribution(Uri localUri, String remoteUri, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) { super(localUri, remoteUri, filename, description, dataLength, dateCreated, dateUploaded, creator); this.decimalCoords = decimalCoords; @@ -132,14 +129,14 @@ public class Contribution extends Media { public String getPageContents() { StringBuffer buffer = new StringBuffer(); SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); - + buffer - .append("== {{int:filedesc}} ==\n") + .append("== {{int:filedesc}} ==\n") .append("{{Information\n") - .append("|description=").append(getDescription()).append("\n") - .append("|source=").append("{{own}}\n") - .append("|author=[[User:").append(creator).append("|").append(creator).append("]]\n"); - if(dateCreated != null) { + .append("|description=").append(getDescription()).append("\n") + .append("|source=").append("{{own}}\n") + .append("|author=[[User:").append(creator).append("|").append(creator).append("]]\n"); + if (dateCreated != null) { buffer .append("|date={{According to EXIF data|").append(isoFormat.format(dateCreated)).append("}}\n"); } @@ -148,13 +145,13 @@ public class Contribution extends Media { //Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null if (decimalCoords != null) { - buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n"); + buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n"); } buffer.append("== {{int:license-header}} ==\n") .append(Utils.licenseTemplateFor(getLicense())).append("\n\n") - .append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n") - .append(getTrackingTemplates()); + .append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n") + .append(getTrackingTemplates()); return buffer.toString(); } @@ -164,19 +161,19 @@ public class Contribution extends Media { public void save() { try { - if(contentUri == null) { + if (contentUri == null) { contentUri = client.insert(ContributionsContentProvider.BASE_URI, this.toContentValues()); } else { client.update(contentUri, toContentValues(), null, null); } - } catch(RemoteException e) { + } catch (RemoteException e) { throw new RuntimeException(e); } } public void delete() { try { - if(contentUri == null) { + if (contentUri == null) { // noooo throw new RuntimeException("tried to delete item with no content URI"); } else { @@ -191,20 +188,20 @@ public class Contribution extends Media { public ContentValues toContentValues() { ContentValues cv = new ContentValues(); cv.put(Table.COLUMN_FILENAME, getFilename()); - if(getLocalUri() != null) { + if (getLocalUri() != null) { cv.put(Table.COLUMN_LOCAL_URI, getLocalUri().toString()); } - if(getImageUrl() != null) { + if (getImageUrl() != null) { cv.put(Table.COLUMN_IMAGE_URL, getImageUrl()); } - if(getDateUploaded() != null) { + if (getDateUploaded() != null) { cv.put(Table.COLUMN_UPLOADED, getDateUploaded().getTime()); } cv.put(Table.COLUMN_LENGTH, getDataLength()); cv.put(Table.COLUMN_TIMESTAMP, getTimestamp().getTime()); cv.put(Table.COLUMN_STATE, getState()); cv.put(Table.COLUMN_TRANSFERRED, transferred); - cv.put(Table.COLUMN_SOURCE, source); + cv.put(Table.COLUMN_SOURCE, source); cv.put(Table.COLUMN_DESCRIPTION, description); cv.put(Table.COLUMN_CREATOR, creator); cv.put(Table.COLUMN_MULTIPLE, isMultiple ? 1 : 0); @@ -240,7 +237,7 @@ public class Contribution extends Media { c.timestamp = cursor.getLong(4) == 0 ? null : new Date(cursor.getLong(4)); c.state = cursor.getInt(5); c.dataLength = cursor.getLong(6); - c.dateUploaded = cursor.getLong(7) == 0 ? null : new Date(cursor.getLong(7)); + c.dateUploaded = cursor.getLong(7) == 0 ? null : new Date(cursor.getLong(7)); c.transferred = cursor.getLong(8); c.source = cursor.getString(9); c.description = cursor.getString(10); @@ -324,7 +321,7 @@ public class Contribution extends Media { + "width INTEGER," + "height INTEGER," + "LICENSE STRING" - + ");"; + + ");"; public static void onCreate(SQLiteDatabase db) { @@ -337,36 +334,36 @@ public class Contribution extends Media { } public static void onUpdate(SQLiteDatabase db, int from, int to) { - if(from == to) { + if (from == to) { return; } - if(from == 1) { + if (from == 1) { db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;"); db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;"); from++; onUpdate(db, from, to); return; } - if(from == 2) { + if (from == 2) { db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;"); db.execSQL("UPDATE " + TABLE_NAME + " SET multiple = 0"); from++; onUpdate(db, from, to); return; } - if(from == 3) { + if (from == 3) { // Do nothing from++; onUpdate(db, from, to); return; } - if(from == 4) { + if (from == 4) { // Do nothing -- added Category from++; onUpdate(db, from, to); return; } - if(from == 5) { + if (from == 5) { // Added width and height fields db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;"); db.execSQL("UPDATE " + TABLE_NAME + " SET width = 0"); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index 2adff7acd..481b4cc7f 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -3,15 +3,12 @@ package fr.free.nrw.commons.contributions; import android.app.Activity; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.os.Bundle; -import android.os.Environment; import android.provider.MediaStore; import android.support.v4.app.Fragment; import android.support.v4.content.FileProvider; import java.io.File; -import java.io.IOException; import java.util.Date; import fr.free.nrw.commons.upload.ShareActivity; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index b181ba2aa..2f91afa40 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -1,7 +1,6 @@ package fr.free.nrw.commons.contributions; import android.Manifest; -import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/UploadCountClient.java b/app/src/main/java/fr/free/nrw/commons/contributions/UploadCountClient.java index 5aaf0fdbe..2a8b54eff 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/UploadCountClient.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/UploadCountClient.java @@ -9,7 +9,6 @@ import java.net.HttpURLConnection; import java.net.URL; import java.util.Locale; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler; import fr.free.nrw.commons.concurrency.ThreadPoolExecutorService; import timber.log.Timber; diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index c7e06cefa..f20d32008 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -28,11 +28,11 @@ import android.view.View; import android.view.ViewGroup; import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.EventLog; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.mwapi.EventLog; public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPageChangeListener { private ViewPager pager; diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java index 731d9af87..a7d9a3d39 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java @@ -30,8 +30,8 @@ import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; -import fr.free.nrw.commons.R; import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.R; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.theme.NavigationBaseActivity; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java index bf6675caa..e7326246c 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java @@ -7,7 +7,6 @@ import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.media.ExifInterface; -import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.NonNull; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java index 5a4ea62b8..d7c807777 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java @@ -20,12 +20,10 @@ import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.Toast; -import butterknife.ButterKnife; - import java.util.ArrayList; +import butterknife.ButterKnife; import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.EventLog; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.AuthenticatedActivity; @@ -36,6 +34,7 @@ import fr.free.nrw.commons.modifications.CategoryModifier; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.TemplateRemoveModifier; +import fr.free.nrw.commons.mwapi.EventLog; import timber.log.Timber; public class MultipleShareActivity diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java index 9476d55e1..629d41639 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java @@ -6,7 +6,6 @@ import android.net.Uri; import android.os.Bundle; import android.support.graphics.drawable.VectorDrawableCompat; import android.support.v4.app.Fragment; -import android.support.v4.content.ContextCompat; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java index 3be35832b..981d7049f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java @@ -32,7 +32,6 @@ import java.util.List; import butterknife.ButterKnife; import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.EventLog; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.AuthenticatedActivity; @@ -42,6 +41,7 @@ import fr.free.nrw.commons.modifications.CategoryModifier; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.TemplateRemoveModifier; +import fr.free.nrw.commons.mwapi.EventLog; import timber.log.Timber; /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java index cb83bc489..12ace6b3b 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java @@ -32,9 +32,9 @@ import butterknife.ButterKnife; import butterknife.OnClick; import butterknife.OnItemSelected; import butterknife.OnTouch; -import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.settings.Prefs; import timber.log.Timber; public class SingleUploadFragment extends Fragment { 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 716a492f9..aa85b9c48 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 @@ -18,9 +18,9 @@ import java.util.Date; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.HandlerService; -import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.settings.Prefs; import timber.log.Timber; public class UploadController { diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java index c9c1fefc2..6fd9f9612 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java @@ -1,9 +1,9 @@ package fr.free.nrw.commons.utils; -import fr.free.nrw.commons.location.LatLng; - import java.text.NumberFormat; +import fr.free.nrw.commons.location.LatLng; + public class LengthUtils { /** Returns a formatted distance string between two points. * @param point1 LatLng type point1 From 599e7bb4531edf832789d2ca1ef799fe669b4531 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Tue, 4 Jul 2017 14:24:08 -0500 Subject: [PATCH 3/7] Consolidated media wiki api calls in a single place --- .../free/nrw/commons/CommonsApplication.java | 63 +---- .../java/fr/free/nrw/commons/EventLog.java | 130 --------- .../main/java/fr/free/nrw/commons/MWApi.java | 94 ------- .../free/nrw/commons/MediaDataExtractor.java | 13 +- .../nrw/commons/MediaThumbnailFetchTask.java | 14 +- .../auth/WikiAccountAuthenticator.java | 8 +- .../nrw/commons/category/MethodAUpdater.java | 13 +- .../nrw/commons/category/PrefixUpdater.java | 10 +- .../nrw/commons/category/TitleCategories.java | 13 +- .../ContributionsSyncAdapter.java | 18 +- .../ModificationsSyncAdapter.java | 17 +- .../mwapi/ApacheHttpClientMediaWikiApi.java | 258 ++++++++++++++++++ .../fr/free/nrw/commons/mwapi/EventLog.java | 28 ++ .../fr/free/nrw/commons/mwapi/LogBuilder.java | 71 +++++ .../fr/free/nrw/commons/mwapi/LogTask.java | 12 + .../free/nrw/commons/mwapi/MediaWikiApi.java | 46 ++++ .../nrw/commons/upload/ExistingFileAsync.java | 11 +- .../nrw/commons/upload/UploadService.java | 24 +- 18 files changed, 467 insertions(+), 376 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/EventLog.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/MWApi.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index 46f74810e..b78bfdc6f 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -8,41 +8,30 @@ import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.os.Build; import android.database.sqlite.SQLiteDatabase; import android.preference.PreferenceManager; import android.support.v4.util.LruCache; import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.stetho.Stetho; - -import fr.free.nrw.commons.caching.CacheController; -import fr.free.nrw.commons.category.Category; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.data.DBOpenHelper; -import fr.free.nrw.commons.modifications.ModifierSequence; -import fr.free.nrw.commons.auth.AccountUtil; -import fr.free.nrw.commons.nearby.NearbyPlaces; - import com.squareup.leakcanary.LeakCanary; import org.acra.ACRA; import org.acra.ReportingInteractionMode; import org.acra.annotation.ReportsCrashes; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.conn.scheme.PlainSocketFactory; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.conn.ssl.SSLSocketFactory; -import org.apache.http.impl.client.AbstractHttpClient; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.CoreProtocolPNames; import java.io.File; import java.io.IOException; +import fr.free.nrw.commons.auth.AccountUtil; +import fr.free.nrw.commons.caching.CacheController; +import fr.free.nrw.commons.category.Category; +import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.data.DBOpenHelper; +import fr.free.nrw.commons.modifications.ModifierSequence; +import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.nearby.NearbyPlaces; import fr.free.nrw.commons.utils.FileUtils; import timber.log.Timber; @@ -76,9 +65,8 @@ public class CommonsApplication extends Application { public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback"; private static CommonsApplication instance = null; - private AbstractHttpClient httpClient = null; - private MWApi api = null; - LruCache thumbnailUrlCache = new LruCache<>(1024); + private MediaWikiApi api = null; + private LruCache thumbnailUrlCache = new LruCache<>(1024); private CacheController cacheData = null; private DBOpenHelper dbOpenHelper = null; private NearbyPlaces nearbyPlaces = null; @@ -98,35 +86,13 @@ public class CommonsApplication extends Application { return instance; } - public AbstractHttpClient getHttpClient() { - if (httpClient == null) { - httpClient = newHttpClient(); - } - return httpClient; - } - - private AbstractHttpClient newHttpClient() { - BasicHttpParams params = new BasicHttpParams(); - SchemeRegistry schemeRegistry = new SchemeRegistry(); - schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); - final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory(); - schemeRegistry.register(new Scheme("https", sslSocketFactory, 443)); - ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry); - params.setParameter(CoreProtocolPNames.USER_AGENT, "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE); - return new DefaultHttpClient(cm, params); - } - - public MWApi getMWApi() { + public MediaWikiApi getMWApi() { if (api == null) { - api = newMWApi(); + api = new ApacheHttpClientMediaWikiApi(API_URL); } return api; } - private MWApi newMWApi() { - return new MWApi(API_URL, getHttpClient()); - } - public CacheController getCacheData() { if (cacheData == null) { cacheData = new CacheController(); @@ -174,9 +140,6 @@ public class CommonsApplication extends Application { Fresco.initialize(this); - // Initialize EventLogging - EventLog.setApp(this); - //For caching area -> categories cacheData = new CacheController(); } diff --git a/app/src/main/java/fr/free/nrw/commons/EventLog.java b/app/src/main/java/fr/free/nrw/commons/EventLog.java deleted file mode 100644 index 5cb36450b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/EventLog.java +++ /dev/null @@ -1,130 +0,0 @@ -package fr.free.nrw.commons; - -import android.content.SharedPreferences; -import android.os.AsyncTask; -import android.os.Build; -import android.preference.PreferenceManager; - -import org.apache.http.HttpResponse; -import org.apache.http.impl.client.AbstractHttpClient; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; - -import fr.free.nrw.commons.settings.Prefs; -import in.yuvi.http.fluent.Http; -import timber.log.Timber; - -public class EventLog { - - private static CommonsApplication app; - - private static class LogTask extends AsyncTask { - - @Override - protected Boolean doInBackground(LogBuilder... logBuilders) { - - boolean allSuccess = true; - // Not using the default URL connection, since that seems to have different behavior than the rest of the code - for(LogBuilder logBuilder: logBuilders) { - try { - URL url = logBuilder.toUrl(); - AbstractHttpClient httpClient = CommonsApplication.getInstance().getHttpClient(); - HttpResponse response = Http.get(url.toString()).use(httpClient).asResponse(); - - if(response.getStatusLine().getStatusCode() != 204) { - allSuccess = false; - } - Timber.d("EventLog hit %s", url); - - } catch (IOException e) { - // Probably just ignore for now. Can be much more robust with a service, etc later on. - Timber.d("IO Error, EventLog hit skipped"); - } - } - - return allSuccess; - } - } - - private static final String DEVICE; - static { - if (Build.MODEL.startsWith(Build.MANUFACTURER)) { - DEVICE = Utils.capitalize(Build.MODEL); - } else { - DEVICE = Utils.capitalize(Build.MANUFACTURER) + " " + Build.MODEL; - } - } - - public static void setApp(CommonsApplication app) { - EventLog.app = app; - } - - public static class LogBuilder { - private JSONObject data; - private long rev; - private String schema; - - private LogBuilder(String schema, long revision) { - data = new JSONObject(); - this.schema = schema; - this.rev = revision; - } - - public LogBuilder param(String key, Object value) { - try { - data.put(key, value); - } catch (JSONException e) { - throw new RuntimeException(e); - } - return this; - } - - private URL toUrl() { - JSONObject fullData = new JSONObject(); - try { - fullData.put("schema", schema); - fullData.put("revision", rev); - fullData.put("wiki", CommonsApplication.EVENTLOG_WIKI); - data.put("device", DEVICE); - data.put("platform", "Android/" + Build.VERSION.RELEASE); - data.put("appversion", "Android/" + BuildConfig.VERSION_NAME); - fullData.put("event", data); - return new URL(CommonsApplication.EVENTLOG_URL + "?" + Utils.urlEncode(fullData.toString()) + ";"); - } catch (MalformedURLException | JSONException e) { - throw new RuntimeException(e); - } - } - - // force param disregards user preference - // Use *only* for tracking the user preference change for EventLogging - // Attempting to use anywhere else will cause kitten explosions - public void log(boolean force) { - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(app); - if(!settings.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) { - return; // User has disabled tracking - } - LogTask logTask = new LogTask(); - logTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this); - } - - public void log() { - log(false); - } - - } - - public static LogBuilder schema(String schema, long revision) { - return new LogBuilder(schema, revision); - } - - public static LogBuilder schema(Object[] scid) { - if(scid.length != 2) { - throw new IllegalArgumentException("Needs an object array with schema as first param and revision as second"); - } - return schema((String)scid[0], (Long)scid[1]); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/MWApi.java b/app/src/main/java/fr/free/nrw/commons/MWApi.java deleted file mode 100644 index 785ea7886..000000000 --- a/app/src/main/java/fr/free/nrw/commons/MWApi.java +++ /dev/null @@ -1,94 +0,0 @@ -package fr.free.nrw.commons; - -import java.io.IOException; - -import org.apache.http.impl.client.AbstractHttpClient; -import org.mediawiki.api.ApiResult; - -/** - * @author Addshore - */ -public class MWApi extends org.mediawiki.api.MWApi { - - /** We don't actually use this but need to pass it in requests */ - private static String LOGIN_RETURN_TO_URL = "https://commons.wikimedia.org"; - - public MWApi(String apiURL, AbstractHttpClient client) { - super(apiURL, client); - } - - /** - * @param username String - * @param password String - * @return String as returned by this.getErrorCodeToReturn() - * @throws IOException On api request IO issue - */ - public String login(String username, String password) throws IOException { - String token = this.getLoginToken(); - ApiResult loginApiResult = this.action("clientlogin") - .param("rememberMe", "1") - .param("username", username) - .param("password", password) - .param("logintoken", token) - .param("loginreturnurl", LOGIN_RETURN_TO_URL) - .post(); - return this.getErrorCodeToReturn( loginApiResult ); - } - - /** - * @param username String - * @param password String - * @param twoFactorCode String - * @return String as returned by this.getErrorCodeToReturn() - * @throws IOException On api request IO issue - */ - public String login(String username, String password, String twoFactorCode) throws IOException { - String token = this.getLoginToken();//TODO cache this instead of calling again when 2FAing - ApiResult loginApiResult = this.action("clientlogin") - .param("rememberMe", "1") - .param("username", username) - .param("password", password) - .param("logintoken", token) - .param("logincontinue", "1") - .param("OATHToken", twoFactorCode) - .post(); - - return this.getErrorCodeToReturn( loginApiResult ); - } - - private String getLoginToken() throws IOException { - ApiResult tokenResult = this.action("query") - .param("action", "query") - .param("meta", "tokens") - .param("type", "login") - .post(); - return tokenResult.getString("/api/query/tokens/@logintoken"); - } - - /** - * @param loginApiResult ApiResult Any clientlogin api result - * @return String On success: "PASS" - * continue: "2FA" (More information required for 2FA) - * failure: A failure message code (defined by mediawiki) - * misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART - */ - private String getErrorCodeToReturn( ApiResult loginApiResult ) { - String status = loginApiResult.getString("/api/clientlogin/@status"); - if (status.equals("PASS")) { - this.isLoggedIn = true; - return status; - } else if (status.equals("FAIL")) { - return loginApiResult.getString("/api/clientlogin/@messagecode"); - } else if ( - status.equals("UI") - && loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest") - && loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).") - ) { - return "2FA"; - } - - // UI, REDIRECT, RESTART - return "genericerror-" + status; - } - -} 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 8bbc03f4d..c6c12c33c 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -21,6 +21,7 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; /** @@ -62,16 +63,8 @@ public class MediaDataExtractor { throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again."); } - MWApi api = CommonsApplication.getInstance().getMWApi(); - ApiResult result = api.action("query") - .param("prop", "revisions") - .param("titles", filename) - .param("rvprop", "content") - .param("rvlimit", 1) - .param("rvgeneratexml", 1) - .get(); - - processResult(result); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); + processResult(api.fetchMediaByFilename(filename)); fetched = true; } diff --git a/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java b/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java index fb73b5a2e..5ccc80c06 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java @@ -3,10 +3,9 @@ package fr.free.nrw.commons; import android.os.AsyncTask; import android.support.annotation.NonNull; -import org.mediawiki.api.ApiResult; +import fr.free.nrw.commons.mwapi.MediaWikiApi; class MediaThumbnailFetchTask extends AsyncTask { - private static final String THUMB_SIZE = "640"; protected final Media media; public MediaThumbnailFetchTask(@NonNull Media media) { @@ -16,15 +15,8 @@ class MediaThumbnailFetchTask extends AsyncTask { @Override protected String doInBackground(String... params) { try { - MWApi api = CommonsApplication.getInstance().getMWApi(); - ApiResult result =api.action("query") - .param("format", "xml") - .param("prop", "imageinfo") - .param("iiprop", "url") - .param("iiurlwidth", THUMB_SIZE) - .param("titles", params[0]) - .get(); - return result.getString("/api/query/pages/page/imageinfo/ii/@thumburl"); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); + return api.findThumbnailByFilename(params[0]); } catch (Exception e) { // Do something better! } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java index ebbd6c285..8ecfc67cb 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java @@ -8,13 +8,13 @@ import android.accounts.NetworkErrorException; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import java.io.IOException; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.MWApi; +import fr.free.nrw.commons.mwapi.MediaWikiApi; public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { @@ -75,7 +75,7 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { } private String getAuthCookie(String username, String password) throws IOException { - MWApi api = CommonsApplication.getInstance().getMWApi(); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); //TODO add 2fa support here String result = api.login(username, password); if(result.equals("PASS")) { diff --git a/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java b/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java index 9300f640d..95f77bc27 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java +++ b/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java @@ -3,7 +3,6 @@ package fr.free.nrw.commons.category; import android.os.AsyncTask; import android.view.View; -import fr.free.nrw.commons.MWApi; import org.mediawiki.api.ApiResult; import java.io.IOException; @@ -12,6 +11,7 @@ import java.util.Calendar; import java.util.Iterator; import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; /** @@ -79,20 +79,13 @@ public class MethodAUpdater extends AsyncTask> { protected ArrayList doInBackground(Void... voids) { //otherwise if user has typed something in that isn't in cache, search API for matching categories - MWApi api = CommonsApplication.getInstance().getMWApi(); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); ApiResult result; ArrayList categories = new ArrayList<>(); //URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch= try { - result = api.action("query") - .param("format", "xml") - .param("list", "search") - .param("srwhat", "text") - .param("srnamespace", "14") - .param("srlimit", catFragment.SEARCH_CATS_LIMIT) - .param("srsearch", filter) - .get(); + result = api.searchCategories(CategorizationFragment.SEARCH_CATS_LIMIT, filter); Timber.d("Method A URL filter %s", result); } catch (IOException e) { Timber.e(e, "IO Exception: "); diff --git a/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java b/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java index 05770081d..426a8eb37 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java +++ b/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java @@ -4,7 +4,6 @@ import android.os.AsyncTask; import android.text.TextUtils; import android.view.View; -import fr.free.nrw.commons.MWApi; import org.mediawiki.api.ApiResult; import java.io.IOException; @@ -13,6 +12,7 @@ import java.util.Calendar; import java.util.Iterator; import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; /** @@ -95,15 +95,11 @@ public class PrefixUpdater extends AsyncTask> { //otherwise if user has typed something in that isn't in cache, search API for matching categories //URL: https://commons.wikimedia.org/w/api.php?action=query&list=allcategories&acprefix=filter&aclimit=25 - MWApi api = CommonsApplication.getInstance().getMWApi(); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); ApiResult result; ArrayList categories = new ArrayList<>(); try { - result = api.action("query") - .param("list", "allcategories") - .param("acprefix", filter) - .param("aclimit", catFragment.SEARCH_CATS_LIMIT) - .get(); + result = api.allCategories(CategorizationFragment.SEARCH_CATS_LIMIT, this.filter); Timber.d("Prefix URL filter %s", result); } catch (IOException e) { Timber.e(e, "IO Exception: "); diff --git a/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java b/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java index 6ca46be0f..62d0f5d16 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java +++ b/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java @@ -2,13 +2,13 @@ package fr.free.nrw.commons.category; import android.os.AsyncTask; -import fr.free.nrw.commons.MWApi; import org.mediawiki.api.ApiResult; import java.io.IOException; import java.util.ArrayList; import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; /** @@ -34,20 +34,13 @@ public class TitleCategories extends AsyncTask> { @Override protected ArrayList doInBackground(Void... voids) { - MWApi api = CommonsApplication.getInstance().getMWApi(); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); ApiResult result; ArrayList items = new ArrayList<>(); //URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch= try { - result = api.action("query") - .param("format", "xml") - .param("list", "search") - .param("srwhat", "text") - .param("srnamespace", "14") - .param("srlimit", SEARCH_CATS_LIMIT) - .param("srsearch", title) - .get(); + result = api.searchTitles(SEARCH_CATS_LIMIT, this.title); Timber.d("Searching for cats for title: %s", result); } catch (IOException e) { Timber.e(e, "IO Exception: "); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java index d7ce9df9d..8f531ea2f 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java @@ -19,8 +19,8 @@ import java.util.ArrayList; import java.util.Date; import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.MWApi; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { @@ -61,7 +61,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) { // This code is fraught with possibilities of race conditions, but lalalalala I can't hear you! String user = account.name; - MWApi api = CommonsApplication.getInstance().getMWApi(); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); SharedPreferences prefs = this.getContext().getSharedPreferences("prefs", Context.MODE_PRIVATE); String lastModified = prefs.getString("lastSyncTimestamp", ""); Date curTime = new Date(); @@ -71,19 +71,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { while(!done) { try { - MWApi.RequestBuilder builder = api.action("query") - .param("list", "logevents") - .param("letype", "upload") - .param("leprop", "title|timestamp|ids") - .param("leuser", user) - .param("lelimit", getLimit()); - if(!TextUtils.isEmpty(lastModified)) { - builder.param("leend", lastModified); - } - if(!TextUtils.isEmpty(queryContinue)) { - builder.param("lestart", queryContinue); - } - result = builder.get(); + result = api.logEvents(user, lastModified, queryContinue, getLimit()); } catch (IOException e) { // There isn't really much we can do, eh? // FIXME: Perhaps add EventLogging? diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java index 898c41f7d..a307f36e3 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java @@ -12,7 +12,6 @@ import android.database.Cursor; import android.os.Bundle; import android.os.RemoteException; -import fr.free.nrw.commons.MWApi; import org.mediawiki.api.ApiResult; import java.io.IOException; @@ -21,6 +20,7 @@ import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.ContributionsContentProvider; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { @@ -61,7 +61,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { return; } - MWApi api = CommonsApplication.getInstance().getMWApi(); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); api.setAuthCookie(authCookie); String editToken; @@ -98,11 +98,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { if(contrib.getState() == Contribution.STATE_COMPLETED) { try { - requestResult = api.action("query") - .param("prop", "revisions") - .param("rvprop", "timestamp|content") - .param("titles", contrib.getFilename()) - .get(); + requestResult = api.revisionsByFilename(contrib.getFilename()); } catch (IOException e) { Timber.d("Network fuckup on modifications sync!"); continue; @@ -113,12 +109,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent); try { - responseResult = api.action("edit") - .param("title", contrib.getFilename()) - .param("token", editToken) - .param("text", processedPageContent) - .param("summary", sequence.getEditSummary()) - .post(); + responseResult = api.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary()); } catch (IOException e) { Timber.d("Network fuckup on modifications sync!"); continue; 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 new file mode 100644 index 000000000..c43f4f64e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -0,0 +1,258 @@ +package fr.free.nrw.commons.mwapi; + +import android.os.Build; +import android.text.TextUtils; + +import org.apache.http.HttpResponse; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.impl.client.AbstractHttpClient; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.CoreProtocolPNames; +import org.mediawiki.api.ApiResult; + +import java.io.IOException; +import java.net.URL; + +import fr.free.nrw.commons.BuildConfig; +import in.yuvi.http.fluent.Http; +import timber.log.Timber; + +/** + * @author Addshore + */ +public class ApacheHttpClientMediaWikiApi extends org.mediawiki.api.MWApi implements MediaWikiApi { + private static final String THUMB_SIZE = "640"; + private static AbstractHttpClient httpClient; + + public ApacheHttpClientMediaWikiApi(String apiURL) { + super(apiURL, getHttpClient()); + } + + private static AbstractHttpClient getHttpClient() { + if (httpClient == null) { + httpClient = newHttpClient(); + } + return httpClient; + } + + private static AbstractHttpClient newHttpClient() { + BasicHttpParams params = new BasicHttpParams(); + SchemeRegistry schemeRegistry = new SchemeRegistry(); + schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); + final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory(); + schemeRegistry.register(new Scheme("https", sslSocketFactory, 443)); + ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry); + params.setParameter(CoreProtocolPNames.USER_AGENT, "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE); + return new DefaultHttpClient(cm, params); + } + + + /** + * @param username String + * @param password String + * @return String as returned by this.getErrorCodeToReturn() + * @throws IOException On api request IO issue + */ + public String login(String username, String password) throws IOException { + return getErrorCodeToReturn(action("clientlogin") + .param("rememberMe", "1") + .param("username", username) + .param("password", password) + .param("logintoken", this.getLoginToken()) + .param("loginreturnurl", "https://commons.wikimedia.org") + .post()); + } + + /** + * @param username String + * @param password String + * @param twoFactorCode String + * @return String as returned by this.getErrorCodeToReturn() + * @throws IOException On api request IO issue + */ + public String login(String username, String password, String twoFactorCode) throws IOException { + return getErrorCodeToReturn(action("clientlogin") + .param("rememberMe", "1") + .param("username", username) + .param("password", password) + .param("logintoken", getLoginToken()) + .param("logincontinue", "1") + .param("OATHToken", twoFactorCode) + .post()); + } + + private String getLoginToken() throws IOException { + return this.action("query") + .param("action", "query") + .param("meta", "tokens") + .param("type", "login") + .post() + .getString("/api/query/tokens/@logintoken"); + } + + /** + * @param loginApiResult ApiResult Any clientlogin api result + * @return String On success: "PASS" + * continue: "2FA" (More information required for 2FA) + * failure: A failure message code (defined by mediawiki) + * misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART + */ + private String getErrorCodeToReturn(ApiResult loginApiResult) { + String status = loginApiResult.getString("/api/clientlogin/@status"); + if (status.equals("PASS")) { + this.isLoggedIn = true; + return status; + } else if (status.equals("FAIL")) { + return loginApiResult.getString("/api/clientlogin/@messagecode"); + } else if ( + status.equals("UI") + && loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest") + && loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).") + ) { + return "2FA"; + } + + // UI, REDIRECT, RESTART + return "genericerror-" + status; + } + + // Moved / consolidated methods + @Override + public boolean fileExistsWithName(String fileName) throws IOException { + return action("query") + .param("prop", "imageinfo") + .param("titles", "File:" + fileName) + .get() + .getNodes("/api/query/pages/page/imageinfo").size() > 0; + } + + @Override + public ApiResult edit(String editToken, String processedPageContent, String filename, String summary) throws IOException { + return action("edit") + .param("title", filename) + .param("token", editToken) + .param("text", processedPageContent) + .param("summary", summary) + .post(); + } + + @Override + public String findThumbnailByFilename(String filename) throws IOException { + return 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"); + } + + @Override + public ApiResult fetchMediaByFilename(String filename) throws IOException { + return action("query") + .param("prop", "revisions") + .param("titles", filename) + .param("rvprop", "content") + .param("rvlimit", 1) + .param("rvgeneratexml", 1) + .get(); + } + + @Override + public ApiResult searchCategories(int searchCatsLimit, String filterValue) throws IOException { + return action("query") + .param("format", "xml") + .param("list", "search") + .param("srwhat", "text") + .param("srnamespace", "14") + .param("srlimit", searchCatsLimit) + .param("srsearch", filterValue) + .get(); + } + + @Override + public ApiResult allCategories(int searchCatsLimit, String filterValue) throws IOException { + return action("query") + .param("list", "allcategories") + .param("acprefix", filterValue) + .param("aclimit", searchCatsLimit) + .get(); + } + + @Override + public ApiResult searchTitles(int searchCatsLimit, String title) throws IOException { + return action("query") + .param("format", "xml") + .param("list", "search") + .param("srwhat", "text") + .param("srnamespace", "14") + .param("srlimit", searchCatsLimit) + .param("srsearch", title) + .get(); + } + + @Override + public ApiResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException { + org.mediawiki.api.MWApi.RequestBuilder builder = action("query") + .param("list", "logevents") + .param("letype", "upload") + .param("leprop", "title|timestamp|ids") + .param("leuser", user) + .param("lelimit", limit); + if (!TextUtils.isEmpty(lastModified)) { + builder.param("leend", lastModified); + } + if (!TextUtils.isEmpty(queryContinue)) { + builder.param("lestart", queryContinue); + } + return builder.get(); + } + + @Override + public ApiResult revisionsByFilename(String filename) throws IOException { + return action("query") + .param("prop", "revisions") + .param("rvprop", "timestamp|content") + .param("titles", filename) + .get(); + } + + @Override + public ApiResult existingFile(String fileSha1) throws IOException { + return action("query") + .param("format", "xml") + .param("list", "allimages") + .param("aisha1", fileSha1) + .get(); + } + + @Override + public boolean logEvents(LogBuilder[] logBuilders) { + boolean allSuccess = true; + // Not using the default URL connection, since that seems to have different behavior than the rest of the code + for (LogBuilder logBuilder : logBuilders) { + try { + URL url = logBuilder.toUrl(); + HttpResponse response = Http.get(url.toString()).use(httpClient).asResponse(); + + if (response.getStatusLine().getStatusCode() != 204) { + allSuccess = false; + } + Timber.d("EventLog hit %s", url); + + } catch (IOException e) { + // Probably just ignore for now. Can be much more robust with a service, etc later on. + Timber.d("IO Error, EventLog hit skipped"); + } + } + + return allSuccess; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java b/app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java new file mode 100644 index 000000000..d3ba7c0d5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java @@ -0,0 +1,28 @@ +package fr.free.nrw.commons.mwapi; + +import android.os.Build; + +import fr.free.nrw.commons.Utils; + +public class EventLog { + static final String DEVICE; + + static { + if (Build.MODEL.startsWith(Build.MANUFACTURER)) { + DEVICE = Utils.capitalize(Build.MODEL); + } else { + DEVICE = Utils.capitalize(Build.MANUFACTURER) + " " + Build.MODEL; + } + } + + private static LogBuilder schema(String schema, long revision) { + return new LogBuilder(schema, revision); + } + + public static LogBuilder schema(Object[] scid) { + if (scid.length != 2) { + throw new IllegalArgumentException("Needs an object array with schema as first param and revision as second"); + } + return schema((String) scid[0], (Long) scid[1]); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java b/app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java new file mode 100644 index 000000000..b512f9647 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java @@ -0,0 +1,71 @@ +package fr.free.nrw.commons.mwapi; + +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Build; +import android.preference.PreferenceManager; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.MalformedURLException; +import java.net.URL; + +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.settings.Prefs; + +public class LogBuilder { + private JSONObject data; + private long rev; + private String schema; + + LogBuilder(String schema, long revision) { + data = new JSONObject(); + this.schema = schema; + this.rev = revision; + } + + public LogBuilder param(String key, Object value) { + try { + data.put(key, value); + } catch (JSONException e) { + throw new RuntimeException(e); + } + return this; + } + + URL toUrl() { + JSONObject fullData = new JSONObject(); + try { + fullData.put("schema", schema); + fullData.put("revision", rev); + fullData.put("wiki", CommonsApplication.EVENTLOG_WIKI); + data.put("device", EventLog.DEVICE); + data.put("platform", "Android/" + Build.VERSION.RELEASE); + data.put("appversion", "Android/" + BuildConfig.VERSION_NAME); + fullData.put("event", data); + return new URL(CommonsApplication.EVENTLOG_URL + "?" + Utils.urlEncode(fullData.toString()) + ";"); + } catch (MalformedURLException | JSONException e) { + throw new RuntimeException(e); + } + } + + // force param disregards user preference + // Use *only* for tracking the user preference change for EventLogging + // Attempting to use anywhere else will cause kitten explosions + public void log(boolean force) { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(CommonsApplication.getInstance()); + if (!settings.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) { + return; // User has disabled tracking + } + LogTask logTask = new LogTask(); + logTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this); + } + + public void log() { + log(false); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java b/app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java new file mode 100644 index 000000000..ee947afbc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.mwapi; + +import android.os.AsyncTask; + +import fr.free.nrw.commons.CommonsApplication; + +class LogTask extends AsyncTask { + @Override + protected Boolean doInBackground(LogBuilder... logBuilders) { + return CommonsApplication.getInstance().getMWApi().logEvents(logBuilders); + } +} 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 new file mode 100644 index 000000000..2f2f5dc4f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -0,0 +1,46 @@ +package fr.free.nrw.commons.mwapi; + +import org.mediawiki.api.ApiResult; + +import java.io.IOException; +import java.io.InputStream; + +import in.yuvi.http.fluent.ProgressListener; + +public interface MediaWikiApi { + String getAuthCookie(); + + void setAuthCookie(String authCookie); + + String login(String username, String password) throws IOException; + + String login(String username, String password, String twoFactorCode) throws IOException; + + boolean validateLogin() throws IOException; + + String getEditToken() throws IOException; + + ApiResult upload(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener) throws IOException; + + boolean fileExistsWithName(String fileName) throws IOException; + + ApiResult edit(String editToken, String processedPageContent, String filename, String summary) throws IOException; + + String findThumbnailByFilename(String filename) throws IOException; + + ApiResult fetchMediaByFilename(String filename) throws IOException; + + ApiResult searchCategories(int searchCatsLimit, String filterValue) throws IOException; + + ApiResult allCategories(int searchCatsLimit, String filter) throws IOException; + + ApiResult searchTitles(int searchCatsLimit, String title) throws IOException; + + ApiResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException; + + ApiResult revisionsByFilename(String filename) throws IOException; + + ApiResult existingFile(String fileSha1) throws IOException; + + boolean logEvents(LogBuilder[] logBuilders); +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java index 85e71860d..08b17c4c1 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java @@ -12,9 +12,9 @@ import java.io.IOException; import java.util.ArrayList; import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.MWApi; import fr.free.nrw.commons.R; import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; /** @@ -49,16 +49,13 @@ public class ExistingFileAsync extends AsyncTask { @Override protected Boolean doInBackground(Void... voids) { - MWApi api = CommonsApplication.getInstance().getMWApi(); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); ApiResult result; // https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba try { - result = api.action("query") - .param("format", "xml") - .param("list", "allimages") - .param("aisha1", fileSha1) - .get(); + String fileSha1 = this.fileSha1; + result = api.existingFile(fileSha1); Timber.d("Searching Commons API for existing file: %s", result); } catch (IOException e) { Timber.e(e, "IO Exception: "); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 853952fae..eba1dd586 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -13,7 +13,6 @@ import android.support.v4.app.NotificationCompat; import android.webkit.MimeTypeMap; import android.widget.Toast; -import fr.free.nrw.commons.*; import org.mediawiki.api.ApiResult; import java.io.FileNotFoundException; @@ -25,10 +24,16 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.HandlerService; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider; +import fr.free.nrw.commons.mwapi.EventLog; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import in.yuvi.http.fluent.ProgressListener; import timber.log.Timber; @@ -176,7 +181,7 @@ public class UploadService extends HandlerService { } private void uploadContribution(Contribution contribution) { - MWApi api = app.getMWApi(); + MediaWikiApi api = app.getMWApi(); ApiResult result; InputStream file = null; @@ -304,7 +309,7 @@ public class UploadService extends HandlerService { } private String findUniqueFilename(String fileName) throws IOException { - MWApi api = app.getMWApi(); + MediaWikiApi api = app.getMWApi(); String sequenceFileName; for ( int sequenceNumber = 1; true; sequenceNumber++ ) { if (sequenceNumber == 1) { @@ -320,7 +325,7 @@ public class UploadService extends HandlerService { sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2"); } } - if ( fileExistsWithName(api, sequenceFileName) || unfinishedUploads.contains(sequenceFileName) ) { + if ( api.fileExistsWithName(sequenceFileName) || unfinishedUploads.contains(sequenceFileName) ) { continue; } else { break; @@ -328,15 +333,4 @@ public class UploadService extends HandlerService { } return sequenceFileName; } - - private static boolean fileExistsWithName(MWApi api, String fileName) throws IOException { - ApiResult result; - - result = api.action("query") - .param("prop", "imageinfo") - .param("titles", "File:" + fileName) - .get(); - - return result.getNodes("/api/query/pages/page/imageinfo").size() > 0; - } } From d64c1b51cc588e515ac1883cd4f7a3d81116f870 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Tue, 4 Jul 2017 14:43:25 -0500 Subject: [PATCH 4/7] Refactoring to not expose the ApiResult and Apache HTTP implementation --- .../free/nrw/commons/MediaDataExtractor.java | 16 +- .../category/CategorizationFragment.java | 10 +- .../nrw/commons/category/MethodAUpdater.java | 32 ++- .../nrw/commons/category/PrefixUpdater.java | 30 ++- .../nrw/commons/category/TitleCategories.java | 28 +-- .../ContributionsSyncAdapter.java | 22 +- .../ModificationsSyncAdapter.java | 34 ++- .../mwapi/ApacheHttpClientMediaWikiApi.java | 202 ++++++++++++++---- .../nrw/commons/mwapi/LogEventResult.java | 51 +++++ .../free/nrw/commons/mwapi/MediaResult.java | 19 ++ .../free/nrw/commons/mwapi/MediaWikiApi.java | 54 +++-- .../free/nrw/commons/mwapi/UploadResult.java | 42 ++++ .../nrw/commons/upload/ExistingFileAsync.java | 13 +- .../nrw/commons/upload/UploadService.java | 33 ++- 14 files changed, 386 insertions(+), 200 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/LogEventResult.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/MediaResult.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java 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 c6c12c33c..698adb683 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -1,6 +1,5 @@ package fr.free.nrw.commons; -import org.mediawiki.api.ApiResult; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -21,6 +20,7 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.mwapi.MediaResult; import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; @@ -64,20 +64,14 @@ public class MediaDataExtractor { } MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); - processResult(api.fetchMediaByFilename(filename)); - fetched = true; - } - - private void processResult(ApiResult result) throws IOException { - - String wikiSource = result.getString("/api/query/pages/page/revisions/rev"); - String parseTreeXmlSource = result.getString("/api/query/pages/page/revisions/rev/@parsetree"); + MediaResult result = api.fetchMediaByFilename(filename); // In-page category links are extracted from source, as XML doesn't cover [[links]] - extractCategories(wikiSource); + extractCategories(result.getWikiSource()); // Description template info is extracted from preprocessor XML - processWikiParseTree(parseTreeXmlSource); + processWikiParseTree(result.getParseTreeXmlSource()); + fetched = true; } /** diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java index 3af2f6473..3f467183d 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java @@ -134,7 +134,7 @@ public class CategorizationFragment extends Fragment { //Override onPostExecute to access the results of async API call titleCategoriesSub = new TitleCategories(title) { @Override - protected void onPostExecute(ArrayList result) { + protected void onPostExecute(List result) { super.onPostExecute(result); Timber.d("Results in onPostExecute: %s", result); titleCatItems.addAll(result); @@ -277,8 +277,8 @@ public class CategorizationFragment extends Fragment { prefixUpdaterSub = new PrefixUpdater(this) { @Override - protected ArrayList doInBackground(Void... voids) { - ArrayList result = new ArrayList<>(); + protected List doInBackground(Void... voids) { + List result = new ArrayList<>(); try { result = super.doInBackground(); latch.await(); @@ -291,7 +291,7 @@ public class CategorizationFragment extends Fragment { } @Override - protected void onPostExecute(ArrayList result) { + protected void onPostExecute(List result) { super.onPostExecute(result); results.addAll(result); @@ -309,7 +309,7 @@ public class CategorizationFragment extends Fragment { methodAUpdaterSub = new MethodAUpdater(this) { @Override - protected void onPostExecute(ArrayList result) { + protected void onPostExecute(List result) { results.clear(); super.onPostExecute(result); diff --git a/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java b/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java index 95f77bc27..326f85ee3 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java +++ b/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java @@ -3,12 +3,11 @@ package fr.free.nrw.commons.category; import android.os.AsyncTask; import android.view.View; -import org.mediawiki.api.ApiResult; - import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.Iterator; +import java.util.List; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.mwapi.MediaWikiApi; @@ -19,12 +18,12 @@ import timber.log.Timber; * the keyword typed in by the user. The 'srsearch' action-specific parameter is used for this * purpose. This class should be subclassed in CategorizationFragment.java to aggregate the results. */ -public class MethodAUpdater extends AsyncTask> { +class MethodAUpdater extends AsyncTask> { private String filter; - CategorizationFragment catFragment; + private CategorizationFragment catFragment; - public MethodAUpdater(CategorizationFragment catFragment) { + MethodAUpdater(CategorizationFragment catFragment) { this.catFragment = catFragment; } @@ -42,10 +41,11 @@ public class MethodAUpdater extends AsyncTask> { * Remove categories that contain a year in them (starting with 19__ or 20__), except for this year * and previous year * Rationale: https://github.com/commons-app/apps-android-commons/issues/47 + * * @param items Unfiltered list of categories * @return Filtered category list */ - private ArrayList filterYears(ArrayList items) { + private List filterYears(List items) { Iterator iterator; @@ -60,12 +60,12 @@ public class MethodAUpdater extends AsyncTask> { Timber.d("Previous year: %s", prevYearInString); //Copy to Iterator to prevent ConcurrentModificationException when removing item - for(iterator = items.iterator(); iterator.hasNext();) { + for (iterator = items.iterator(); iterator.hasNext(); ) { String s = iterator.next(); //Check if s contains a 4-digit word anywhere within the string (.* is wildcard) //And that s does not equal the current year or previous year - if(s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString)) { + if (s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString)) { Timber.d("Filtering out year %s", s); iterator.remove(); } @@ -76,30 +76,22 @@ public class MethodAUpdater extends AsyncTask> { } @Override - protected ArrayList doInBackground(Void... voids) { + protected List doInBackground(Void... voids) { //otherwise if user has typed something in that isn't in cache, search API for matching categories MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); - ApiResult result; - ArrayList categories = new ArrayList<>(); + List categories = new ArrayList<>(); //URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch= try { - result = api.searchCategories(CategorizationFragment.SEARCH_CATS_LIMIT, filter); - Timber.d("Method A URL filter %s", result); + categories = api.searchCategories(CategorizationFragment.SEARCH_CATS_LIMIT, filter); + Timber.d("Method A URL filter %s", categories); } catch (IOException e) { Timber.e(e, "IO Exception: "); //Return empty arraylist return categories; } - ArrayList categoryNodes = result.getNodes("/api/query/search/p/@title"); - for(ApiResult categoryNode: categoryNodes) { - String cat = categoryNode.getDocument().getTextContent(); - String catString = cat.replace("Category:", ""); - categories.add(catString); - } - Timber.d("Found categories from Method A search, waiting for filter"); return new ArrayList<>(filterYears(categories)); } diff --git a/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java b/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java index 426a8eb37..121ae5a1c 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java +++ b/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java @@ -4,12 +4,11 @@ import android.os.AsyncTask; import android.text.TextUtils; import android.view.View; -import org.mediawiki.api.ApiResult; - import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.Iterator; +import java.util.List; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.mwapi.MediaWikiApi; @@ -21,7 +20,7 @@ import timber.log.Timber; * for this purpose. This class should be subclassed in CategorizationFragment.java to aggregate * the results. */ -public class PrefixUpdater extends AsyncTask> { +public class PrefixUpdater extends AsyncTask> { private String filter; private CategorizationFragment catFragment; @@ -44,10 +43,11 @@ public class PrefixUpdater extends AsyncTask> { * Remove categories that contain a year in them (starting with 19__ or 20__), except for this year * and previous year * Rationale: https://github.com/commons-app/apps-android-commons/issues/47 + * * @param items Unfiltered list of categories * @return Filtered category list */ - private ArrayList filterYears(ArrayList items) { + private List filterYears(List items) { Iterator iterator; @@ -62,12 +62,12 @@ public class PrefixUpdater extends AsyncTask> { Timber.d("Previous year: %s", prevYearInString); //Copy to Iterator to prevent ConcurrentModificationException when removing item - for(iterator = items.iterator(); iterator.hasNext();) { + for (iterator = items.iterator(); iterator.hasNext(); ) { String s = iterator.next(); //Check if s contains a 4-digit word anywhere within the string (.* is wildcard) //And that s does not equal the current year or previous year - if(s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString)) { + if (s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString)) { Timber.d("Filtering out year %s", s); iterator.remove(); } @@ -78,16 +78,16 @@ public class PrefixUpdater extends AsyncTask> { } @Override - protected ArrayList doInBackground(Void... voids) { + protected List doInBackground(Void... voids) { //If user hasn't typed anything in yet, get GPS and recent items - if(TextUtils.isEmpty(filter)) { + if (TextUtils.isEmpty(filter)) { ArrayList mergedItems = new ArrayList<>(catFragment.mergeItems()); Timber.d("Merged items, waiting for filter"); return new ArrayList<>(filterYears(mergedItems)); } //if user types in something that is in cache, return cached category - if(catFragment.categoriesCache.containsKey(filter)) { + if (catFragment.categoriesCache.containsKey(filter)) { ArrayList cachedItems = new ArrayList<>(catFragment.categoriesCache.get(filter)); Timber.d("Found cache items, waiting for filter"); return new ArrayList<>(filterYears(cachedItems)); @@ -96,22 +96,16 @@ public class PrefixUpdater extends AsyncTask> { //otherwise if user has typed something in that isn't in cache, search API for matching categories //URL: https://commons.wikimedia.org/w/api.php?action=query&list=allcategories&acprefix=filter&aclimit=25 MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); - ApiResult result; - ArrayList categories = new ArrayList<>(); + List categories = new ArrayList<>(); try { - result = api.allCategories(CategorizationFragment.SEARCH_CATS_LIMIT, this.filter); - Timber.d("Prefix URL filter %s", result); + categories = api.allCategories(CategorizationFragment.SEARCH_CATS_LIMIT, this.filter); + Timber.d("Prefix URL filter %s", categories); } catch (IOException e) { Timber.e(e, "IO Exception: "); //Return empty arraylist return categories; } - ArrayList categoryNodes = result.getNodes("/api/query/allcategories/c"); - for(ApiResult categoryNode: categoryNodes) { - categories.add(categoryNode.getDocument().getTextContent()); - } - Timber.d("Found categories from Prefix search, waiting for filter"); return new ArrayList<>(filterYears(categories)); } diff --git a/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java b/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java index 62d0f5d16..414c87e8a 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java +++ b/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java @@ -2,10 +2,9 @@ package fr.free.nrw.commons.category; import android.os.AsyncTask; -import org.mediawiki.api.ApiResult; - import java.io.IOException; import java.util.ArrayList; +import java.util.List; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.mwapi.MediaWikiApi; @@ -16,13 +15,13 @@ import timber.log.Timber; * the title entered in previous screen. The 'srsearch' action-specific parameter is used for this * purpose. This class should be subclassed in CategorizationFragment.java to add the results to recent and GPS cats. */ -public class TitleCategories extends AsyncTask> { +class TitleCategories extends AsyncTask> { private final static int SEARCH_CATS_LIMIT = 25; private String title; - public TitleCategories(String title) { + TitleCategories(String title) { this.title = title; } @@ -32,32 +31,23 @@ public class TitleCategories extends AsyncTask> { } @Override - protected ArrayList doInBackground(Void... voids) { + protected List doInBackground(Void... voids) { MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); - ApiResult result; - ArrayList items = new ArrayList<>(); + List titleCategories = new ArrayList<>(); //URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch= try { - result = api.searchTitles(SEARCH_CATS_LIMIT, this.title); - Timber.d("Searching for cats for title: %s", result); + titleCategories = api.searchTitles(SEARCH_CATS_LIMIT, this.title); } catch (IOException e) { Timber.e(e, "IO Exception: "); //Return empty arraylist - return items; + return titleCategories; } - ArrayList categoryNodes = result.getNodes("/api/query/search/p/@title"); - for(ApiResult categoryNode: categoryNodes) { - String cat = categoryNode.getDocument().getTextContent(); - String catString = cat.replace("Category:", ""); - items.add(catString); - } + Timber.d("Title cat query results: %s", titleCategories); - Timber.d("Title cat query results: %s", items); - - return items; + return titleCategories; } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java index 8f531ea2f..6d769e94c 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java @@ -12,14 +12,14 @@ import android.os.Bundle; import android.os.RemoteException; import android.text.TextUtils; -import org.mediawiki.api.ApiResult; - import java.io.IOException; import java.util.ArrayList; import java.util.Date; +import java.util.List; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.mwapi.LogEventResult; import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; @@ -65,7 +65,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { SharedPreferences prefs = this.getContext().getSharedPreferences("prefs", Context.MODE_PRIVATE); String lastModified = prefs.getString("lastSyncTimestamp", ""); Date curTime = new Date(); - ApiResult result; + LogEventResult result; Boolean done = false; String queryContinue = null; while(!done) { @@ -81,22 +81,21 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { } Timber.d("Last modified at %s", lastModified); - ArrayList uploads = result.getNodes("/api/query/logevents/item"); - Timber.d("%d results!", uploads.size()); + List logEvents = result.getLogEvents(); + Timber.d("%d results!", logEvents.size()); ArrayList imageValues = new ArrayList<>(); - for(ApiResult image: uploads) { - String pageId = image.getString("@pageid"); - if (pageId.equals("0")) { + for (LogEventResult.LogEvent image : logEvents) { + if (image.isDeleted()) { // means that this upload was deleted. continue; } - String filename = image.getString("@title"); + String filename = image.getFilename(); if(fileExists(contentProviderClient, filename)) { Timber.d("Skipping %s", filename); continue; } String thumbUrl = Utils.makeThumbBaseUrl(filename); - Date dateUpdated = Utils.parseMWDate(image.getString("@timestamp")); + Date dateUpdated = image.getDateUpdated(); Contribution contrib = new Contribution(null, thumbUrl, filename, "", -1, dateUpdated, dateUpdated, user, "", ""); contrib.setState(Contribution.STATE_COMPLETED); imageValues.add(contrib.toContentValues()); @@ -118,7 +117,8 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { throw new RuntimeException(e); } } - queryContinue = result.getString("/api/query-continue/logevents/@lestart"); + + queryContinue = result.getQueryContinue(); if(TextUtils.isEmpty(queryContinue)) { done = true; } diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java index a307f36e3..f2940ec74 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java @@ -12,8 +12,6 @@ import android.database.Cursor; import android.os.Bundle; import android.os.RemoteException; -import org.mediawiki.api.ApiResult; - import java.io.IOException; import fr.free.nrw.commons.CommonsApplication; @@ -41,14 +39,14 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { } // Exit early if nothing to do - if(allModifications == null || allModifications.getCount() == 0) { + if (allModifications == null || allModifications.getCount() == 0) { Timber.d("No modifications to perform"); return; } String authCookie; try { - authCookie = AccountManager.get(getContext()).blockingGetAuthToken(account, "", false); + authCookie = AccountManager.get(getContext()).blockingGetAuthToken(account, "", false); } catch (OperationCanceledException | AuthenticatorException e) { throw new RuntimeException(e); } catch (IOException e) { @@ -56,7 +54,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { return; } - if(Utils.isNullOrWhiteSpace(authCookie)) { + if (Utils.isNullOrWhiteSpace(authCookie)) { Timber.d("Could not authenticate :("); return; } @@ -65,7 +63,6 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { api.setAuthCookie(authCookie); String editToken; - ApiResult requestResult, responseResult; try { editToken = api.getEditToken(); } catch (IOException e) { @@ -81,7 +78,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { try { contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY); - while(!allModifications.isAfterLast()) { + while (!allModifications.isAfterLast()) { ModifierSequence sequence = ModifierSequence.fromCursor(allModifications); sequence.setContentProviderClient(contentProviderClient); Contribution contrib; @@ -95,32 +92,31 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { contributionCursor.moveToFirst(); contrib = Contribution.fromCursor(contributionCursor); - if(contrib.getState() == Contribution.STATE_COMPLETED) { - + if (contrib.getState() == Contribution.STATE_COMPLETED) { + String pageContent; try { - requestResult = api.revisionsByFilename(contrib.getFilename()); + pageContent = api.revisionsByFilename(contrib.getFilename()); } catch (IOException e) { Timber.d("Network fuckup on modifications sync!"); continue; } - Timber.d("Page content is %s", Utils.getStringFromDOM(requestResult.getDocument())); - String pageContent = requestResult.getString("/api/query/pages/page/revisions/rev"); - String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent); + Timber.d("Page content is %s", pageContent); + String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent); + String editResult; try { - responseResult = api.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary()); + editResult = api.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary()); } catch (IOException e) { Timber.d("Network fuckup on modifications sync!"); continue; } - Timber.d("Response is %s", Utils.getStringFromDOM(responseResult.getDocument())); + Timber.d("Response is %s", editResult); - String result = responseResult.getString("/api/edit/@result"); - if(!result.equals("Success")) { + if (!editResult.equals("Success")) { // FIXME: Log this somewhere else - Timber.d("Non success result! %s", result); + Timber.d("Non success result! %s", editResult); } else { sequence.delete(); } @@ -128,7 +124,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { allModifications.moveToNext(); } } finally { - if(contributionsClient != null) { + if (contributionsClient != null) { contributionsClient.release(); } } 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 c43f4f64e..080aec51a 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 @@ -1,6 +1,8 @@ package fr.free.nrw.commons.mwapi; import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import org.apache.http.HttpResponse; @@ -15,33 +17,30 @@ import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.CoreProtocolPNames; import org.mediawiki.api.ApiResult; +import org.mediawiki.api.MWApi; import java.io.IOException; +import java.io.InputStream; import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.Utils; import in.yuvi.http.fluent.Http; import timber.log.Timber; /** * @author Addshore */ -public class ApacheHttpClientMediaWikiApi extends org.mediawiki.api.MWApi implements MediaWikiApi { +public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { private static final String THUMB_SIZE = "640"; - private static AbstractHttpClient httpClient; + private AbstractHttpClient httpClient; + private MWApi api; public ApacheHttpClientMediaWikiApi(String apiURL) { - super(apiURL, getHttpClient()); - } - - private static AbstractHttpClient getHttpClient() { - if (httpClient == null) { - httpClient = newHttpClient(); - } - return httpClient; - } - - private static AbstractHttpClient newHttpClient() { BasicHttpParams params = new BasicHttpParams(); SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); @@ -49,10 +48,10 @@ public class ApacheHttpClientMediaWikiApi extends org.mediawiki.api.MWApi implem schemeRegistry.register(new Scheme("https", sslSocketFactory, 443)); ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry); params.setParameter(CoreProtocolPNames.USER_AGENT, "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE); - return new DefaultHttpClient(cm, params); + httpClient = new DefaultHttpClient(cm, params); + api = new MWApi(apiURL, httpClient); } - /** * @param username String * @param password String @@ -60,7 +59,7 @@ public class ApacheHttpClientMediaWikiApi extends org.mediawiki.api.MWApi implem * @throws IOException On api request IO issue */ public String login(String username, String password) throws IOException { - return getErrorCodeToReturn(action("clientlogin") + return getErrorCodeToReturn(api.action("clientlogin") .param("rememberMe", "1") .param("username", username) .param("password", password) @@ -77,7 +76,7 @@ public class ApacheHttpClientMediaWikiApi extends org.mediawiki.api.MWApi implem * @throws IOException On api request IO issue */ public String login(String username, String password, String twoFactorCode) throws IOException { - return getErrorCodeToReturn(action("clientlogin") + return getErrorCodeToReturn(api.action("clientlogin") .param("rememberMe", "1") .param("username", username) .param("password", password) @@ -88,7 +87,7 @@ public class ApacheHttpClientMediaWikiApi extends org.mediawiki.api.MWApi implem } private String getLoginToken() throws IOException { - return this.action("query") + return api.action("query") .param("action", "query") .param("meta", "tokens") .param("type", "login") @@ -106,7 +105,7 @@ public class ApacheHttpClientMediaWikiApi extends org.mediawiki.api.MWApi implem private String getErrorCodeToReturn(ApiResult loginApiResult) { String status = loginApiResult.getString("/api/clientlogin/@status"); if (status.equals("PASS")) { - this.isLoggedIn = true; + api.isLoggedIn = true; return status; } else if (status.equals("FAIL")) { return loginApiResult.getString("/api/clientlogin/@messagecode"); @@ -122,10 +121,29 @@ public class ApacheHttpClientMediaWikiApi extends org.mediawiki.api.MWApi implem return "genericerror-" + status; } - // Moved / consolidated methods + @Override + public String getAuthCookie() { + return api.getAuthCookie(); + } + + @Override + public void setAuthCookie(String authCookie) { + api.setAuthCookie(authCookie); + } + + @Override + public boolean validateLogin() throws IOException { + return api.validateLogin(); + } + + @Override + public String getEditToken() throws IOException { + return api.getEditToken(); + } + @Override public boolean fileExistsWithName(String fileName) throws IOException { - return action("query") + return api.action("query") .param("prop", "imageinfo") .param("titles", "File:" + fileName) .get() @@ -133,18 +151,20 @@ public class ApacheHttpClientMediaWikiApi extends org.mediawiki.api.MWApi implem } @Override - public ApiResult edit(String editToken, String processedPageContent, String filename, String summary) throws IOException { - return action("edit") + @Nullable + public String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException { + return api.action("edit") .param("title", filename) .param("token", editToken) .param("text", processedPageContent) .param("summary", summary) - .post(); + .post() + .getString("/api/edit/@result"); } @Override public String findThumbnailByFilename(String filename) throws IOException { - return action("query") + return api.action("query") .param("format", "xml") .param("prop", "imageinfo") .param("iiprop", "url") @@ -155,52 +175,101 @@ public class ApacheHttpClientMediaWikiApi extends org.mediawiki.api.MWApi implem } @Override - public ApiResult fetchMediaByFilename(String filename) throws IOException { - return action("query") + @NonNull + public MediaResult fetchMediaByFilename(String filename) throws IOException { + ApiResult 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 - public ApiResult searchCategories(int searchCatsLimit, String filterValue) throws IOException { - return action("query") + @NonNull + public List searchCategories(int searchCatsLimit, String filterValue) throws IOException { + List categoryNodes = api.action("query") .param("format", "xml") .param("list", "search") .param("srwhat", "text") .param("srnamespace", "14") .param("srlimit", searchCatsLimit) .param("srsearch", filterValue) - .get(); + .get() + .getNodes("/api/query/search/p/@title"); + + if (categoryNodes == null) { + return Collections.emptyList(); + } + + List categories = new ArrayList<>(); + for (ApiResult categoryNode : categoryNodes) { + String cat = categoryNode.getDocument().getTextContent(); + String catString = cat.replace("Category:", ""); + categories.add(catString); + } + + return categories; } @Override - public ApiResult allCategories(int searchCatsLimit, String filterValue) throws IOException { - return action("query") + @NonNull + public List allCategories(int searchCatsLimit, String filterValue) throws IOException { + ArrayList categoryNodes = api.action("query") .param("list", "allcategories") .param("acprefix", filterValue) .param("aclimit", searchCatsLimit) - .get(); + .get() + .getNodes("/api/query/allcategories/c"); + + if (categoryNodes == null) { + return Collections.emptyList(); + } + + List categories = new ArrayList<>(); + for (ApiResult categoryNode : categoryNodes) { + categories.add(categoryNode.getDocument().getTextContent()); + } + + return categories; } @Override - public ApiResult searchTitles(int searchCatsLimit, String title) throws IOException { - return action("query") + @NonNull + public List searchTitles(int searchCatsLimit, String title) throws IOException { + ArrayList categoryNodes = api.action("query") .param("format", "xml") .param("list", "search") .param("srwhat", "text") .param("srnamespace", "14") .param("srlimit", searchCatsLimit) .param("srsearch", title) - .get(); + .get() + .getNodes("/api/query/search/p/@title"); + + if (categoryNodes == null) { + return Collections.emptyList(); + } + + List titleCategories = new ArrayList<>(); + for (ApiResult categoryNode : categoryNodes) { + String cat = categoryNode.getDocument().getTextContent(); + String catString = cat.replace("Category:", ""); + titleCategories.add(catString); + } + + return titleCategories; } @Override - public ApiResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException { - org.mediawiki.api.MWApi.RequestBuilder builder = action("query") + @NonNull + public LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException { + org.mediawiki.api.MWApi.RequestBuilder builder = api.action("query") .param("list", "logevents") .param("letype", "upload") .param("leprop", "title|timestamp|ids") @@ -212,25 +281,47 @@ public class ApacheHttpClientMediaWikiApi extends org.mediawiki.api.MWApi implem if (!TextUtils.isEmpty(queryContinue)) { builder.param("lestart", queryContinue); } - return builder.get(); + ApiResult result = builder.get(); + + return new LogEventResult( + getLogEventsFromResult(result), + result.getString("/api/query-continue/logevents/@lestart")); + } + + @NonNull + private ArrayList getLogEventsFromResult(ApiResult result) { + ArrayList uploads = result.getNodes("/api/query/logevents/item"); + Timber.d("%d results!", uploads.size()); + ArrayList logEvents = new ArrayList<>(); + for (ApiResult image : uploads) { + logEvents.add(new LogEventResult.LogEvent( + image.getString("@pageid"), + image.getString("@title"), + Utils.parseMWDate(image.getString("@timestamp"))) + ); + } + return logEvents; } @Override - public ApiResult revisionsByFilename(String filename) throws IOException { - return action("query") + @Nullable + public String revisionsByFilename(String filename) throws IOException { + return api.action("query") .param("prop", "revisions") .param("rvprop", "timestamp|content") .param("titles", filename) - .get(); + .get() + .getString("/api/query/pages/page/revisions/rev"); } @Override - public ApiResult existingFile(String fileSha1) throws IOException { - return action("query") + public boolean existingFile(String fileSha1) throws IOException { + return api.action("query") .param("format", "xml") .param("list", "allimages") .param("aisha1", fileSha1) - .get(); + .get() + .getNodes("/api/query/allimages/img").size() > 0; } @Override @@ -255,4 +346,25 @@ public class ApacheHttpClientMediaWikiApi extends org.mediawiki.api.MWApi implem return allSuccess; } + + @Override + @NonNull + public UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, final ProgressListener progressListener) throws IOException { + ApiResult result = api.upload(filename, file, dataLength, pageContents, editSummary, new in.yuvi.http.fluent.ProgressListener() { + @Override + public void onProgress(long transferred, long total) { + progressListener.onProgress(transferred, total); + } + }); + String resultStatus = result.getString("/api/upload/@result"); + if (!resultStatus.equals("Success")) { + String errorCode = result.getString("/api/error/@code"); + return new UploadResult(resultStatus, errorCode); + } else { + Date dateUploaded = Utils.parseMWDate(result.getString("/api/upload/imageinfo/@timestamp")); + String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename + String imageUrl = result.getString("/api/upload/imageinfo/@url"); + return new UploadResult(dateUploaded, canonicalFilename, imageUrl); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/LogEventResult.java b/app/src/main/java/fr/free/nrw/commons/mwapi/LogEventResult.java new file mode 100644 index 000000000..3917d0fce --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/LogEventResult.java @@ -0,0 +1,51 @@ +package fr.free.nrw.commons.mwapi; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.Date; +import java.util.List; + +public class LogEventResult { + private final List logEvents; + private final String queryContinue; + + LogEventResult(@NonNull List logEvents, String queryContinue) { + this.logEvents = logEvents; + this.queryContinue = queryContinue; + } + + @NonNull + public List getLogEvents() { + return logEvents; + } + + @Nullable + public String getQueryContinue() { + return queryContinue; + } + + public static class LogEvent { + private final String pageId; + private final String filename; + private final Date dateUpdated; + + LogEvent(String pageId, String filename, Date dateUpdated) { + this.pageId = pageId; + this.filename = filename; + this.dateUpdated = dateUpdated; + } + + public boolean isDeleted() { + return pageId.equals("0"); + } + + public String getFilename() { + return filename; + } + + public Date getDateUpdated() { + return dateUpdated; + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaResult.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaResult.java new file mode 100644 index 000000000..cfddf8c15 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaResult.java @@ -0,0 +1,19 @@ +package fr.free.nrw.commons.mwapi; + +public class MediaResult { + private final String wikiSource; + private final String parseTreeXmlSource; + + MediaResult(String wikiSource, String parseTreeXmlSource) { + this.wikiSource = wikiSource; + this.parseTreeXmlSource = parseTreeXmlSource; + } + + public String getWikiSource() { + return wikiSource; + } + + public String getParseTreeXmlSource() { + return parseTreeXmlSource; + } +} 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 2f2f5dc4f..878a4118c 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 @@ -1,11 +1,11 @@ package fr.free.nrw.commons.mwapi; -import org.mediawiki.api.ApiResult; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import java.io.IOException; import java.io.InputStream; - -import in.yuvi.http.fluent.ProgressListener; +import java.util.List; public interface MediaWikiApi { String getAuthCookie(); @@ -20,27 +20,39 @@ public interface MediaWikiApi { String getEditToken() throws IOException; - ApiResult upload(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener) throws IOException; - boolean fileExistsWithName(String fileName) throws IOException; - ApiResult edit(String editToken, String processedPageContent, String filename, String summary) throws IOException; - String findThumbnailByFilename(String filename) throws IOException; - ApiResult fetchMediaByFilename(String filename) throws IOException; - - ApiResult searchCategories(int searchCatsLimit, String filterValue) throws IOException; - - ApiResult allCategories(int searchCatsLimit, String filter) throws IOException; - - ApiResult searchTitles(int searchCatsLimit, String title) throws IOException; - - ApiResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException; - - ApiResult revisionsByFilename(String filename) throws IOException; - - ApiResult existingFile(String fileSha1) throws IOException; - boolean logEvents(LogBuilder[] logBuilders); + + @NonNull + UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener) throws IOException; + + @Nullable + String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException; + + @NonNull + MediaResult fetchMediaByFilename(String filename) throws IOException; + + @NonNull + List searchCategories(int searchCatsLimit, String filterValue) throws IOException; + + @NonNull + List allCategories(int searchCatsLimit, String filter) throws IOException; + + @NonNull + List searchTitles(int searchCatsLimit, String title) throws IOException; + + @Nullable + String revisionsByFilename(String filename) throws IOException; + + boolean existingFile(String fileSha1) throws IOException; + + @NonNull + LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException; + + interface ProgressListener { + void onProgress(long transferred, long total); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java b/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java new file mode 100644 index 000000000..08c292a7b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java @@ -0,0 +1,42 @@ +package fr.free.nrw.commons.mwapi; + +import java.util.Date; + +public class UploadResult { + private String errorCode; + private String resultStatus; + private Date dateUploaded; + private String imageUrl; + private String canonicalFilename; + + UploadResult(String resultStatus, String errorCode) { + this.resultStatus = resultStatus; + this.errorCode = errorCode; + } + + UploadResult(Date dateUploaded, String canonicalFilename, String imageUrl) { + this.dateUploaded = dateUploaded; + this.canonicalFilename = canonicalFilename; + this.imageUrl = imageUrl; + } + + public Date getDateUploaded() { + return dateUploaded; + } + + public String getImageUrl() { + return imageUrl; + } + + public String getCanonicalFilename() { + return canonicalFilename; + } + + public String getErrorCode() { + return errorCode; + } + + public String getResultStatus() { + return resultStatus; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java index 08b17c4c1..cacc75596 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java @@ -6,10 +6,7 @@ import android.content.Intent; import android.os.AsyncTask; import android.support.v7.app.AlertDialog; -import org.mediawiki.api.ApiResult; - import java.io.IOException; -import java.util.ArrayList; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; @@ -50,23 +47,17 @@ public class ExistingFileAsync extends AsyncTask { @Override protected Boolean doInBackground(Void... voids) { MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); - ApiResult result; // https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba + boolean fileExists; try { String fileSha1 = this.fileSha1; - result = api.existingFile(fileSha1); - Timber.d("Searching Commons API for existing file: %s", result); + fileExists = api.existingFile(fileSha1); } catch (IOException e) { Timber.e(e, "IO Exception: "); return false; } - ArrayList resultNodes = result.getNodes("/api/query/allimages/img"); - Timber.d("Result nodes: %s", resultNodes); - - boolean fileExists = !resultNodes.isEmpty(); - Timber.d("File already exists in Commons: %s", fileExists); return fileExists; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index eba1dd586..1165d85fe 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.upload; +import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -13,12 +14,9 @@ import android.support.v4.app.NotificationCompat; import android.webkit.MimeTypeMap; import android.widget.Toast; -import org.mediawiki.api.ApiResult; - import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.util.Date; import java.util.HashSet; import java.util.Set; import java.util.regex.Matcher; @@ -34,7 +32,7 @@ import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.mwapi.EventLog; import fr.free.nrw.commons.mwapi.MediaWikiApi; -import in.yuvi.http.fluent.ProgressListener; +import fr.free.nrw.commons.mwapi.UploadResult; import timber.log.Timber; public class UploadService extends HandlerService { @@ -69,7 +67,7 @@ public class UploadService extends HandlerService { super("UploadService"); } - private class NotificationUpdateProgressListener implements ProgressListener { + private class NotificationUpdateProgressListener implements MediaWikiApi.ProgressListener { String notificationTag; boolean notificationTitleChanged; @@ -180,10 +178,10 @@ public class UploadService extends HandlerService { return START_REDELIVER_INTENT; } + @SuppressLint("StringFormatInvalid") private void uploadContribution(Contribution contribution) { MediaWikiApi api = app.getMWApi(); - ApiResult result; InputStream file = null; String notificationTag = contribution.getLocalUri().toString(); @@ -240,32 +238,27 @@ public class UploadService extends HandlerService { getString(R.string.upload_progress_notification_title_finishing, contribution.getDisplayTitle()), contribution ); - result = api.upload(filename, file, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), notificationUpdater); + UploadResult uploadResult = api.uploadFile(filename, file, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), notificationUpdater); - Timber.d("Response is %s", Utils.getStringFromDOM(result.getDocument())); + Timber.d("Response is %s", uploadResult.toString()); curProgressNotification = null; - String resultStatus = result.getString("/api/upload/@result"); + String resultStatus = uploadResult.getResultStatus(); if(!resultStatus.equals("Success")) { - String errorCode = result.getString("/api/error/@code"); showFailedNotification(contribution); EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT) .param("username", app.getCurrentAccount().name) .param("source", contribution.getSource()) .param("multiple", contribution.getMultiple()) - .param("result", errorCode) + .param("result", uploadResult.getErrorCode()) .param("filename", contribution.getFilename()) .log(); } else { - Date dateUploaded = null; - dateUploaded = Utils.parseMWDate(result.getString("/api/upload/imageinfo/@timestamp")); - String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename - String imageUrl = result.getString("/api/upload/imageinfo/@url"); - contribution.setFilename(canonicalFilename); - contribution.setImageUrl(imageUrl); + contribution.setFilename(uploadResult.getCanonicalFilename()); + contribution.setImageUrl(uploadResult.getImageUrl()); contribution.setState(Contribution.STATE_COMPLETED); - contribution.setDateUploaded(dateUploaded); + contribution.setDateUploaded(uploadResult.getDateUploaded()); contribution.save(); EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT) @@ -279,7 +272,6 @@ public class UploadService extends HandlerService { } catch(IOException e) { Timber.d("I have a network fuckup"); showFailedNotification(contribution); - return; } finally { if ( filename != null ) { unfinishedUploads.remove(filename); @@ -293,8 +285,9 @@ public class UploadService extends HandlerService { } } + @SuppressLint("StringFormatInvalid") private void showFailedNotification(Contribution contribution) { - Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true) + Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true) .setSmallIcon(R.drawable.ic_launcher) .setAutoCancel(true) .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0)) From c035b368c407511bde35e0fb898bf746ec7459bd Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Tue, 4 Jul 2017 22:27:42 -0500 Subject: [PATCH 5/7] Added smoke test - run through a few api calls - to verify that the parameters are all correctly encoded and sent, and responses are parsed. --- app/build.gradle | 1 + .../ApacheHttpClientMediaWikiApiTest.java | 229 ++++++++++++++++++ .../mwapi/ApacheHttpClientMediaWikiApi.java | 2 +- 3 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 app/src/androidTest/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java diff --git a/app/build.gradle b/app/build.gradle index c4794016e..a731a9e3a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,6 +28,7 @@ dependencies { compile "com.google.guava:guava:${GUAVA_VERSION}" testCompile 'junit:junit:4.12' + androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.8.1' androidTestCompile "com.android.support:support-annotations:${project.supportLibVersion}" androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' diff --git a/app/src/androidTest/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java b/app/src/androidTest/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java new file mode 100644 index 000000000..046962499 --- /dev/null +++ b/app/src/androidTest/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java @@ -0,0 +1,229 @@ +package fr.free.nrw.commons.mwapi; + +import android.os.Build; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import fr.free.nrw.commons.BuildConfig; +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/* + * XML for individual tests was captured by hand from the API sandbox - + * https://en.wikipedia.org/wiki/Special:ApiSandbox + */ +@RunWith(AndroidJUnit4.class) +public class ApacheHttpClientMediaWikiApiTest { + + private ApacheHttpClientMediaWikiApi testObject; + private MockWebServer server; + + @Before + public void setUp() throws Exception { + server = new MockWebServer(); + testObject = new ApacheHttpClientMediaWikiApi("http://localhost:" + server.getPort() + "/"); + } + + @After + public void teardown() throws IOException { + server.shutdown(); + } + + @Test + public void authCookiesAreHandled() { + assertEquals("", testObject.getAuthCookie()); + + testObject.setAuthCookie("cookie=chocolate-chip"); + + assertEquals("cookie=chocolate-chip", testObject.getAuthCookie()); + } + + @Test + public void simpleLoginWithWrongPassword() throws Exception { + server.enqueue(new MockResponse().setBody("")); + server.enqueue(new MockResponse().setBody("")); + + String result = testObject.login("foo", "bar"); + + RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "POST"); + Map body = parseBody(loginTokenRequest.getBody().readUtf8()); + assertEquals("xml", body.get("format")); + assertEquals("query", body.get("action")); + assertEquals("login", body.get("type")); + assertEquals("tokens", body.get("meta")); + + RecordedRequest loginRequest = assertBasicRequestParameters(server, "POST"); + body = parseBody(loginRequest.getBody().readUtf8()); + assertEquals("1", body.get("rememberMe")); + assertEquals("foo", body.get("username")); + assertEquals("bar", body.get("password")); + assertEquals("baz", body.get("logintoken")); + assertEquals("https://commons.wikimedia.org", body.get("loginreturnurl")); + assertEquals("xml", body.get("format")); + + assertEquals("wrongpassword", result); + } + + @Test + public void simpleLogin() throws Exception { + server.enqueue(new MockResponse().setBody("")); + server.enqueue(new MockResponse().setBody("")); + + String result = testObject.login("foo", "bar"); + + RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "POST"); + Map body = parseBody(loginTokenRequest.getBody().readUtf8()); + assertEquals("xml", body.get("format")); + assertEquals("query", body.get("action")); + assertEquals("login", body.get("type")); + assertEquals("tokens", body.get("meta")); + + RecordedRequest loginRequest = assertBasicRequestParameters(server, "POST"); + body = parseBody(loginRequest.getBody().readUtf8()); + assertEquals("1", body.get("rememberMe")); + assertEquals("foo", body.get("username")); + assertEquals("bar", body.get("password")); + assertEquals("baz", body.get("logintoken")); + assertEquals("https://commons.wikimedia.org", body.get("loginreturnurl")); + assertEquals("xml", body.get("format")); + + assertEquals("PASS", result); + } + + @Test + public void twoFactorLogin() throws Exception { + server.enqueue(new MockResponse().setBody("")); + server.enqueue(new MockResponse().setBody("")); + + String result = testObject.login("foo", "bar", "2fa"); + + RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "POST"); + Map body = parseBody(loginTokenRequest.getBody().readUtf8()); + assertEquals("xml", body.get("format")); + assertEquals("query", body.get("action")); + assertEquals("login", body.get("type")); + assertEquals("tokens", body.get("meta")); + + RecordedRequest loginRequest = assertBasicRequestParameters(server, "POST"); + body = parseBody(loginRequest.getBody().readUtf8()); + assertEquals("1", body.get("rememberMe")); + assertEquals("foo", body.get("username")); + assertEquals("bar", body.get("password")); + assertEquals("baz", body.get("logintoken")); + assertEquals("1", body.get("logincontinue")); + assertEquals("2fa", body.get("OATHToken")); + assertEquals("xml", body.get("format")); + + assertEquals("PASS", result); + } + + @Test + public void validateLoginForLoggedInUser() throws Exception { + server.enqueue(new MockResponse().setBody("")); + + boolean result = testObject.validateLogin(); + + RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "GET"); + Map body = parseQueryParams(loginTokenRequest); + assertEquals("xml", body.get("format")); + assertEquals("query", body.get("action")); + assertEquals("userinfo", body.get("meta")); + + assertTrue(result); + } + + @Test + public void validateLoginForLoggedOutUser() throws Exception { + server.enqueue(new MockResponse().setBody("")); + + boolean result = testObject.validateLogin(); + + RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "GET"); + Map params = parseQueryParams(loginTokenRequest); + assertEquals("xml", params.get("format")); + assertEquals("query", params.get("action")); + assertEquals("userinfo", params.get("meta")); + + assertFalse(result); + } + + @Test + public void editToken() throws Exception{ + server.enqueue(new MockResponse().setBody("")); + + String result = testObject.getEditToken(); + + RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "GET"); + Map params = parseQueryParams(loginTokenRequest); + assertEquals("xml", params.get("format")); + assertEquals("tokens", params.get("action")); + assertEquals("edit", params.get("type")); + + assertEquals("baz", result); + } + + @Test + public void fileExistsWithName_FileNotFound() throws Exception { + server.enqueue(new MockResponse().setBody(" ")); + + boolean result = testObject.fileExistsWithName("foo"); + + RecordedRequest request = assertBasicRequestParameters(server, "GET"); + Map params = parseQueryParams(request); + assertEquals("xml", params.get("format")); + assertEquals("query", params.get("action")); + assertEquals("imageinfo", params.get("prop")); + assertEquals("File:foo", params.get("titles")); + + assertFalse(result); + } + + + private RecordedRequest assertBasicRequestParameters(MockWebServer server, String method) throws InterruptedException { + RecordedRequest request = server.takeRequest(); + assertEquals("/", request.getRequestUrl().encodedPath()); + assertEquals(method, request.getMethod()); + assertEquals("Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE, request.getHeader("User-Agent")); + if ("POST".equals(method)) { + assertEquals("application/x-www-form-urlencoded", request.getHeader("Content-Type")); + } + return request; + } + + private Map parseQueryParams(RecordedRequest request) { + Map result = new HashMap<>(); + HttpUrl url = request.getRequestUrl(); + Set params = url.queryParameterNames(); + for (String name : params) { + result.put(name, url.queryParameter(name)); + } + return result; + } + + private Map parseBody(String body) throws UnsupportedEncodingException { + String[] props = body.split("&"); + Map result = new HashMap<>(); + for (String prop : props) { + String[] pair = prop.split("="); + result.put(pair[0], URLDecoder.decode(pair[1], "utf-8")); + } + return result; + } +} \ No newline at end of file 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 080aec51a..69bfd521d 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 @@ -63,7 +63,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { .param("rememberMe", "1") .param("username", username) .param("password", password) - .param("logintoken", this.getLoginToken()) + .param("logintoken", getLoginToken()) .param("loginreturnurl", "https://commons.wikimedia.org") .post()); } From ddce34bc902b8fbd5e7c64bb7d5f0cb3403b1800 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Wed, 5 Jul 2017 20:23:37 -0500 Subject: [PATCH 6/7] Shifted the ApacheHttpClientMediaWikiApiTest to run using Robolectric (as a unit test) rather than on-device. --- app/build.gradle | 5 +++++ .../mwapi/ApacheHttpClientMediaWikiApiTest.java | 12 +++++------- 2 files changed, 10 insertions(+), 7 deletions(-) rename app/src/{androidTest => test}/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java (97%) diff --git a/app/build.gradle b/app/build.gradle index a731a9e3a..fa5c549bc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,6 +28,11 @@ dependencies { compile "com.google.guava:guava:${GUAVA_VERSION}" testCompile 'junit:junit:4.12' + testCompile ('org.robolectric:robolectric:3.3.2') { + exclude module: 'guava' + } + + testCompile 'com.squareup.okhttp3:mockwebserver:3.8.1' androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.8.1' androidTestCompile "com.android.support:support-annotations:${project.supportLibVersion}" androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' diff --git a/app/src/androidTest/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java b/app/src/test/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java similarity index 97% rename from app/src/androidTest/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java rename to app/src/test/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java index 046962499..1e0779b17 100644 --- a/app/src/androidTest/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java +++ b/app/src/test/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java @@ -1,12 +1,13 @@ package fr.free.nrw.commons.mwapi; import android.os.Build; -import android.support.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -25,11 +26,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -/* - * XML for individual tests was captured by hand from the API sandbox - - * https://en.wikipedia.org/wiki/Special:ApiSandbox - */ -@RunWith(AndroidJUnit4.class) +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class) public class ApacheHttpClientMediaWikiApiTest { private ApacheHttpClientMediaWikiApi testObject; @@ -226,4 +224,4 @@ public class ApacheHttpClientMediaWikiApiTest { } return result; } -} \ No newline at end of file +} From 6f86b1871fdb77a609bf09e4d253f3047a2cd2d5 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Mon, 10 Jul 2017 15:23:13 -0500 Subject: [PATCH 7/7] Fixed NPE caused by not initializing the resultStatus field of UploadResult when on the happy-path. --- .../nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java | 6 +++++- .../main/java/fr/free/nrw/commons/mwapi/UploadResult.java | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) 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 69bfd521d..bb2356dd6 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 @@ -4,6 +4,7 @@ import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; +import android.util.Log; import org.apache.http.HttpResponse; import org.apache.http.conn.ClientConnectionManager; @@ -356,6 +357,9 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { progressListener.onProgress(transferred, total); } }); + + Log.e("WTF", "Result: "+result.toString()); + String resultStatus = result.getString("/api/upload/@result"); if (!resultStatus.equals("Success")) { String errorCode = result.getString("/api/error/@code"); @@ -364,7 +368,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { Date dateUploaded = Utils.parseMWDate(result.getString("/api/upload/imageinfo/@timestamp")); String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename String imageUrl = result.getString("/api/upload/imageinfo/@url"); - return new UploadResult(dateUploaded, canonicalFilename, imageUrl); + return new UploadResult(resultStatus, dateUploaded, canonicalFilename, imageUrl); } } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java b/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java index 08c292a7b..34d050b2c 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java @@ -14,7 +14,8 @@ public class UploadResult { this.errorCode = errorCode; } - UploadResult(Date dateUploaded, String canonicalFilename, String imageUrl) { + UploadResult(String resultStatus, Date dateUploaded, String canonicalFilename, String imageUrl) { + this.resultStatus = resultStatus; this.dateUploaded = dateUploaded; this.canonicalFilename = canonicalFilename; this.imageUrl = imageUrl;