Merge pull request #763 from psh/master

Refactoring the API calls
This commit is contained in:
Josephine Lim 2017-07-16 02:27:50 +10:00 committed by GitHub
commit 53d6792f5b
41 changed files with 1061 additions and 551 deletions

View file

@ -1,6 +1,7 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'jacoco-android' apply plugin: 'jacoco-android'
apply from: 'quality.gradle' apply from: 'quality.gradle'
apply plugin: 'com.getkeepsafe.dexcount'
dependencies { dependencies {
compile 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07' compile 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
@ -13,11 +14,13 @@ dependencies {
compile "com.android.support:support-v4:${project.supportLibVersion}" compile "com.android.support:support-v4:${project.supportLibVersion}"
compile "com.android.support:appcompat-v7:${project.supportLibVersion}" compile "com.android.support:appcompat-v7:${project.supportLibVersion}"
compile "com.android.support:design:${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" compile "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
compile 'com.github.pedrovgs:renderers:3.3.0' compile 'com.github.pedrovgs:renderers:3.3.0'
annotationProcessor "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" annotationProcessor "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
compile 'com.jakewharton.timber:timber:4.5.1' 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.1.0@aar'){ compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.1.0@aar'){
transitive=true transitive=true
} }
@ -29,6 +32,9 @@ dependencies {
testCompile ('org.robolectric:robolectric:3.3.2') { testCompile ('org.robolectric:robolectric:3.3.2') {
exclude module: 'guava' 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:support-annotations:${project.supportLibVersion}"
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'

View file

@ -8,41 +8,30 @@ import android.app.Application;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v4.util.LruCache; import android.support.v4.util.LruCache;
import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.stetho.Stetho; 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 com.squareup.leakcanary.LeakCanary;
import org.acra.ACRA; import org.acra.ACRA;
import org.acra.ReportingInteractionMode; import org.acra.ReportingInteractionMode;
import org.acra.annotation.ReportsCrashes; 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.File;
import java.io.IOException; 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 fr.free.nrw.commons.utils.FileUtils;
import timber.log.Timber; 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"; public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
private static CommonsApplication instance = null; private static CommonsApplication instance = null;
private AbstractHttpClient httpClient = null; private MediaWikiApi api = null;
private MWApi api = null; private LruCache<String, String> thumbnailUrlCache = new LruCache<>(1024);
LruCache<String, String> thumbnailUrlCache = new LruCache<>(1024);
private CacheController cacheData = null; private CacheController cacheData = null;
private DBOpenHelper dbOpenHelper = null; private DBOpenHelper dbOpenHelper = null;
private NearbyPlaces nearbyPlaces = null; private NearbyPlaces nearbyPlaces = null;
@ -98,35 +86,13 @@ public class CommonsApplication extends Application {
return instance; return instance;
} }
public AbstractHttpClient getHttpClient() { public MediaWikiApi getMWApi() {
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() {
if (api == null) { if (api == null) {
api = newMWApi(); api = new ApacheHttpClientMediaWikiApi(API_URL);
} }
return api; return api;
} }
private MWApi newMWApi() {
return new MWApi(API_URL, getHttpClient());
}
public CacheController getCacheData() { public CacheController getCacheData() {
if (cacheData == null) { if (cacheData == null) {
cacheData = new CacheController(); cacheData = new CacheController();
@ -174,9 +140,6 @@ public class CommonsApplication extends Application {
Fresco.initialize(this); Fresco.initialize(this);
// Initialize EventLogging
EventLog.setApp(this);
//For caching area -> categories //For caching area -> categories
cacheData = new CacheController(); cacheData = new CacheController();
} }

View file

@ -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<LogBuilder, Void, Boolean> {
@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]);
}
}

View file

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

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import org.mediawiki.api.ApiResult;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.Node; import org.w3c.dom.Node;
@ -21,6 +20,8 @@ import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import fr.free.nrw.commons.location.LatLng; 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; import timber.log.Timber;
/** /**
@ -62,29 +63,15 @@ public class MediaDataExtractor {
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again."); throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
} }
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
ApiResult result = api.action("query") MediaResult result = api.fetchMediaByFilename(filename);
.param("prop", "revisions")
.param("titles", filename)
.param("rvprop", "content")
.param("rvlimit", 1)
.param("rvgeneratexml", 1)
.get();
processResult(result);
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");
// In-page category links are extracted from source, as XML doesn't cover [[links]] // 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 // Description template info is extracted from preprocessor XML
processWikiParseTree(parseTreeXmlSource); processWikiParseTree(result.getParseTreeXmlSource());
fetched = true;
} }
/** /**

View file

@ -3,10 +3,9 @@ package fr.free.nrw.commons;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import org.mediawiki.api.ApiResult; import fr.free.nrw.commons.mwapi.MediaWikiApi;
class MediaThumbnailFetchTask extends AsyncTask<String, String, String> { class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
private static final String THUMB_SIZE = "640";
protected final Media media; protected final Media media;
public MediaThumbnailFetchTask(@NonNull Media media) { public MediaThumbnailFetchTask(@NonNull Media media) {
@ -16,15 +15,8 @@ class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
@Override @Override
protected String doInBackground(String... params) { protected String doInBackground(String... params) {
try { try {
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
ApiResult result =api.action("query") return api.findThumbnailByFilename(params[0]);
.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");
} catch (Exception e) { } catch (Exception e) {
// Do something better! // Do something better!
} }

View file

@ -9,7 +9,6 @@ import com.viewpagerindicator.CirclePageIndicator;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.theme.BaseActivity; import fr.free.nrw.commons.theme.BaseActivity;
public class WelcomeActivity extends BaseActivity { public class WelcomeActivity extends BaseActivity {

View file

@ -7,6 +7,7 @@ import android.content.ContentResolver;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider;

View file

@ -19,9 +19,10 @@ import android.widget.Toast;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import timber.log.Timber; import timber.log.Timber;

View file

@ -9,8 +9,8 @@ import android.os.Bundle;
import java.io.IOException; import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.EventLog;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.mwapi.EventLog;
import timber.log.Timber; import timber.log.Timber;
class LoginTask extends AsyncTask<String, String, String> { class LoginTask extends AsyncTask<String, String, String> {

View file

@ -8,13 +8,13 @@ import android.accounts.NetworkErrorException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.IOException; 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.CommonsApplication;
import fr.free.nrw.commons.MWApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
@ -75,7 +75,7 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
} }
private String getAuthCookie(String username, String password) throws IOException { private String getAuthCookie(String username, String password) throws IOException {
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
//TODO add 2fa support here //TODO add 2fa support here
String result = api.login(username, password); String result = api.login(username, password);
if(result.equals("PASS")) { if(result.equals("PASS")) {

View file

@ -134,7 +134,7 @@ public class CategorizationFragment extends Fragment {
//Override onPostExecute to access the results of async API call //Override onPostExecute to access the results of async API call
titleCategoriesSub = new TitleCategories(title) { titleCategoriesSub = new TitleCategories(title) {
@Override @Override
protected void onPostExecute(ArrayList<String> result) { protected void onPostExecute(List<String> result) {
super.onPostExecute(result); super.onPostExecute(result);
Timber.d("Results in onPostExecute: %s", result); Timber.d("Results in onPostExecute: %s", result);
titleCatItems.addAll(result); titleCatItems.addAll(result);
@ -277,8 +277,8 @@ public class CategorizationFragment extends Fragment {
prefixUpdaterSub = new PrefixUpdater(this) { prefixUpdaterSub = new PrefixUpdater(this) {
@Override @Override
protected ArrayList<String> doInBackground(Void... voids) { protected List<String> doInBackground(Void... voids) {
ArrayList<String> result = new ArrayList<>(); List<String> result = new ArrayList<>();
try { try {
result = super.doInBackground(); result = super.doInBackground();
latch.await(); latch.await();
@ -291,7 +291,7 @@ public class CategorizationFragment extends Fragment {
} }
@Override @Override
protected void onPostExecute(ArrayList<String> result) { protected void onPostExecute(List<String> result) {
super.onPostExecute(result); super.onPostExecute(result);
results.addAll(result); results.addAll(result);
@ -309,7 +309,7 @@ public class CategorizationFragment extends Fragment {
methodAUpdaterSub = new MethodAUpdater(this) { methodAUpdaterSub = new MethodAUpdater(this) {
@Override @Override
protected void onPostExecute(ArrayList<String> result) { protected void onPostExecute(List<String> result) {
results.clear(); results.clear();
super.onPostExecute(result); super.onPostExecute(result);

View file

@ -3,15 +3,14 @@ package fr.free.nrw.commons.category;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.view.View; import android.view.View;
import fr.free.nrw.commons.MWApi;
import org.mediawiki.api.ApiResult;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -19,12 +18,12 @@ import timber.log.Timber;
* the keyword typed in by the user. The 'srsearch' action-specific parameter is used for this * 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. * purpose. This class should be subclassed in CategorizationFragment.java to aggregate the results.
*/ */
public class MethodAUpdater extends AsyncTask<Void, Void, ArrayList<String>> { class MethodAUpdater extends AsyncTask<Void, Void, List<String>> {
private String filter; private String filter;
CategorizationFragment catFragment; private CategorizationFragment catFragment;
public MethodAUpdater(CategorizationFragment catFragment) { MethodAUpdater(CategorizationFragment catFragment) {
this.catFragment = catFragment; this.catFragment = catFragment;
} }
@ -42,10 +41,11 @@ public class MethodAUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
* Remove categories that contain a year in them (starting with 19__ or 20__), except for this year * Remove categories that contain a year in them (starting with 19__ or 20__), except for this year
* and previous year * and previous year
* Rationale: https://github.com/commons-app/apps-android-commons/issues/47 * Rationale: https://github.com/commons-app/apps-android-commons/issues/47
*
* @param items Unfiltered list of categories * @param items Unfiltered list of categories
* @return Filtered category list * @return Filtered category list
*/ */
private ArrayList<String> filterYears(ArrayList<String> items) { private List<String> filterYears(List<String> items) {
Iterator<String> iterator; Iterator<String> iterator;
@ -60,12 +60,12 @@ public class MethodAUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
Timber.d("Previous year: %s", prevYearInString); Timber.d("Previous year: %s", prevYearInString);
//Copy to Iterator to prevent ConcurrentModificationException when removing item //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(); String s = iterator.next();
//Check if s contains a 4-digit word anywhere within the string (.* is wildcard) //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 //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); Timber.d("Filtering out year %s", s);
iterator.remove(); iterator.remove();
} }
@ -76,37 +76,22 @@ public class MethodAUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
} }
@Override @Override
protected ArrayList<String> doInBackground(Void... voids) { protected List<String> doInBackground(Void... voids) {
//otherwise if user has typed something in that isn't in cache, search API for matching categories //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; List<String> categories = new ArrayList<>();
ArrayList<String> 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= //URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch=
try { try {
result = api.action("query") categories = api.searchCategories(CategorizationFragment.SEARCH_CATS_LIMIT, filter);
.param("format", "xml") Timber.d("Method A URL filter %s", categories);
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", catFragment.SEARCH_CATS_LIMIT)
.param("srsearch", filter)
.get();
Timber.d("Method A URL filter %s", result);
} catch (IOException e) { } catch (IOException e) {
Timber.e(e, "IO Exception: "); Timber.e(e, "IO Exception: ");
//Return empty arraylist //Return empty arraylist
return categories; return categories;
} }
ArrayList<ApiResult> 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"); Timber.d("Found categories from Method A search, waiting for filter");
return new ArrayList<>(filterYears(categories)); return new ArrayList<>(filterYears(categories));
} }

View file

@ -4,15 +4,14 @@ import android.os.AsyncTask;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
import fr.free.nrw.commons.MWApi;
import org.mediawiki.api.ApiResult;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -21,7 +20,7 @@ import timber.log.Timber;
* for this purpose. This class should be subclassed in CategorizationFragment.java to aggregate * for this purpose. This class should be subclassed in CategorizationFragment.java to aggregate
* the results. * the results.
*/ */
public class PrefixUpdater extends AsyncTask<Void, Void, ArrayList<String>> { public class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
private String filter; private String filter;
private CategorizationFragment catFragment; private CategorizationFragment catFragment;
@ -44,10 +43,11 @@ public class PrefixUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
* Remove categories that contain a year in them (starting with 19__ or 20__), except for this year * Remove categories that contain a year in them (starting with 19__ or 20__), except for this year
* and previous year * and previous year
* Rationale: https://github.com/commons-app/apps-android-commons/issues/47 * Rationale: https://github.com/commons-app/apps-android-commons/issues/47
*
* @param items Unfiltered list of categories * @param items Unfiltered list of categories
* @return Filtered category list * @return Filtered category list
*/ */
private ArrayList<String> filterYears(ArrayList<String> items) { private List<String> filterYears(List<String> items) {
Iterator<String> iterator; Iterator<String> iterator;
@ -62,12 +62,12 @@ public class PrefixUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
Timber.d("Previous year: %s", prevYearInString); Timber.d("Previous year: %s", prevYearInString);
//Copy to Iterator to prevent ConcurrentModificationException when removing item //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(); String s = iterator.next();
//Check if s contains a 4-digit word anywhere within the string (.* is wildcard) //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 //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); Timber.d("Filtering out year %s", s);
iterator.remove(); iterator.remove();
} }
@ -78,16 +78,16 @@ public class PrefixUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
} }
@Override @Override
protected ArrayList<String> doInBackground(Void... voids) { protected List<String> doInBackground(Void... voids) {
//If user hasn't typed anything in yet, get GPS and recent items //If user hasn't typed anything in yet, get GPS and recent items
if(TextUtils.isEmpty(filter)) { if (TextUtils.isEmpty(filter)) {
ArrayList<String> mergedItems = new ArrayList<>(catFragment.mergeItems()); ArrayList<String> mergedItems = new ArrayList<>(catFragment.mergeItems());
Timber.d("Merged items, waiting for filter"); Timber.d("Merged items, waiting for filter");
return new ArrayList<>(filterYears(mergedItems)); return new ArrayList<>(filterYears(mergedItems));
} }
//if user types in something that is in cache, return cached category //if user types in something that is in cache, return cached category
if(catFragment.categoriesCache.containsKey(filter)) { if (catFragment.categoriesCache.containsKey(filter)) {
ArrayList<String> cachedItems = new ArrayList<>(catFragment.categoriesCache.get(filter)); ArrayList<String> cachedItems = new ArrayList<>(catFragment.categoriesCache.get(filter));
Timber.d("Found cache items, waiting for filter"); Timber.d("Found cache items, waiting for filter");
return new ArrayList<>(filterYears(cachedItems)); return new ArrayList<>(filterYears(cachedItems));
@ -95,27 +95,17 @@ public class PrefixUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
//otherwise if user has typed something in that isn't in cache, search API for matching categories //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 //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; List<String> categories = new ArrayList<>();
ArrayList<String> categories = new ArrayList<>();
try { try {
result = api.action("query") categories = api.allCategories(CategorizationFragment.SEARCH_CATS_LIMIT, this.filter);
.param("list", "allcategories") Timber.d("Prefix URL filter %s", categories);
.param("acprefix", filter)
.param("aclimit", catFragment.SEARCH_CATS_LIMIT)
.get();
Timber.d("Prefix URL filter %s", result);
} catch (IOException e) { } catch (IOException e) {
Timber.e(e, "IO Exception: "); Timber.e(e, "IO Exception: ");
//Return empty arraylist //Return empty arraylist
return categories; return categories;
} }
ArrayList<ApiResult> 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"); Timber.d("Found categories from Prefix search, waiting for filter");
return new ArrayList<>(filterYears(categories)); return new ArrayList<>(filterYears(categories));
} }

View file

@ -2,13 +2,12 @@ package fr.free.nrw.commons.category;
import android.os.AsyncTask; import android.os.AsyncTask;
import fr.free.nrw.commons.MWApi;
import org.mediawiki.api.ApiResult;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -16,13 +15,13 @@ import timber.log.Timber;
* the title entered in previous screen. The 'srsearch' action-specific parameter is used for this * 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. * purpose. This class should be subclassed in CategorizationFragment.java to add the results to recent and GPS cats.
*/ */
public class TitleCategories extends AsyncTask<Void, Void, ArrayList<String>> { class TitleCategories extends AsyncTask<Void, Void, List<String>> {
private final static int SEARCH_CATS_LIMIT = 25; private final static int SEARCH_CATS_LIMIT = 25;
private String title; private String title;
public TitleCategories(String title) { TitleCategories(String title) {
this.title = title; this.title = title;
} }
@ -32,39 +31,23 @@ public class TitleCategories extends AsyncTask<Void, Void, ArrayList<String>> {
} }
@Override @Override
protected ArrayList<String> doInBackground(Void... voids) { protected List<String> doInBackground(Void... voids) {
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
ApiResult result; List<String> titleCategories = new ArrayList<>();
ArrayList<String> 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= //URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch=
try { try {
result = api.action("query") titleCategories = api.searchTitles(SEARCH_CATS_LIMIT, this.title);
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", SEARCH_CATS_LIMIT)
.param("srsearch", title)
.get();
Timber.d("Searching for cats for title: %s", result);
} catch (IOException e) { } catch (IOException e) {
Timber.e(e, "IO Exception: "); Timber.e(e, "IO Exception: ");
//Return empty arraylist //Return empty arraylist
return items; return titleCategories;
} }
ArrayList<ApiResult> categoryNodes = result.getNodes("/api/query/search/p/@title"); Timber.d("Title cat query results: %s", titleCategories);
for(ApiResult categoryNode: categoryNodes) {
String cat = categoryNode.getDocument().getTextContent();
String catString = cat.replace("Category:", "");
items.add(catString);
}
Timber.d("Title cat query results: %s", items); return titleCategories;
return items;
} }
} }

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.concurrency; package fr.free.nrw.commons.concurrency;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
public class BackgroundPoolExceptionHandler implements ExceptionHandler { public class BackgroundPoolExceptionHandler implements ExceptionHandler {

View file

@ -15,10 +15,9 @@ import java.util.Locale;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.EventLog;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.settings.Prefs;
public class Contribution extends Media { public class Contribution extends Media {
@ -63,8 +62,6 @@ public class Contribution extends Media {
isMultiple = multiple; 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) { 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); super(localUri, remoteUri, filename, description, dataLength, dateCreated, dateUploaded, creator);
this.decimalCoords = decimalCoords; this.decimalCoords = decimalCoords;
@ -134,12 +131,12 @@ public class Contribution extends Media {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
buffer buffer
.append("== {{int:filedesc}} ==\n") .append("== {{int:filedesc}} ==\n")
.append("{{Information\n") .append("{{Information\n")
.append("|description=").append(getDescription()).append("\n") .append("|description=").append(getDescription()).append("\n")
.append("|source=").append("{{own}}\n") .append("|source=").append("{{own}}\n")
.append("|author=[[User:").append(creator).append("|").append(creator).append("]]\n"); .append("|author=[[User:").append(creator).append("|").append(creator).append("]]\n");
if(dateCreated != null) { if (dateCreated != null) {
buffer buffer
.append("|date={{According to EXIF data|").append(isoFormat.format(dateCreated)).append("}}\n"); .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 //Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null
if (decimalCoords != 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") buffer.append("== {{int:license-header}} ==\n")
.append(Utils.licenseTemplateFor(getLicense())).append("\n\n") .append(Utils.licenseTemplateFor(getLicense())).append("\n\n")
.append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n") .append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n")
.append(getTrackingTemplates()); .append(getTrackingTemplates());
return buffer.toString(); return buffer.toString();
} }
@ -164,19 +161,19 @@ public class Contribution extends Media {
public void save() { public void save() {
try { try {
if(contentUri == null) { if (contentUri == null) {
contentUri = client.insert(ContributionsContentProvider.BASE_URI, this.toContentValues()); contentUri = client.insert(ContributionsContentProvider.BASE_URI, this.toContentValues());
} else { } else {
client.update(contentUri, toContentValues(), null, null); client.update(contentUri, toContentValues(), null, null);
} }
} catch(RemoteException e) { } catch (RemoteException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
public void delete() { public void delete() {
try { try {
if(contentUri == null) { if (contentUri == null) {
// noooo // noooo
throw new RuntimeException("tried to delete item with no content URI"); throw new RuntimeException("tried to delete item with no content URI");
} else { } else {
@ -191,20 +188,20 @@ public class Contribution extends Media {
public ContentValues toContentValues() { public ContentValues toContentValues() {
ContentValues cv = new ContentValues(); ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_FILENAME, getFilename()); cv.put(Table.COLUMN_FILENAME, getFilename());
if(getLocalUri() != null) { if (getLocalUri() != null) {
cv.put(Table.COLUMN_LOCAL_URI, getLocalUri().toString()); cv.put(Table.COLUMN_LOCAL_URI, getLocalUri().toString());
} }
if(getImageUrl() != null) { if (getImageUrl() != null) {
cv.put(Table.COLUMN_IMAGE_URL, getImageUrl()); cv.put(Table.COLUMN_IMAGE_URL, getImageUrl());
} }
if(getDateUploaded() != null) { if (getDateUploaded() != null) {
cv.put(Table.COLUMN_UPLOADED, getDateUploaded().getTime()); cv.put(Table.COLUMN_UPLOADED, getDateUploaded().getTime());
} }
cv.put(Table.COLUMN_LENGTH, getDataLength()); cv.put(Table.COLUMN_LENGTH, getDataLength());
cv.put(Table.COLUMN_TIMESTAMP, getTimestamp().getTime()); cv.put(Table.COLUMN_TIMESTAMP, getTimestamp().getTime());
cv.put(Table.COLUMN_STATE, getState()); cv.put(Table.COLUMN_STATE, getState());
cv.put(Table.COLUMN_TRANSFERRED, transferred); 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_DESCRIPTION, description);
cv.put(Table.COLUMN_CREATOR, creator); cv.put(Table.COLUMN_CREATOR, creator);
cv.put(Table.COLUMN_MULTIPLE, isMultiple ? 1 : 0); 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.timestamp = cursor.getLong(4) == 0 ? null : new Date(cursor.getLong(4));
c.state = cursor.getInt(5); c.state = cursor.getInt(5);
c.dataLength = cursor.getLong(6); 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.transferred = cursor.getLong(8);
c.source = cursor.getString(9); c.source = cursor.getString(9);
c.description = cursor.getString(10); c.description = cursor.getString(10);
@ -324,7 +321,7 @@ public class Contribution extends Media {
+ "width INTEGER," + "width INTEGER,"
+ "height INTEGER," + "height INTEGER,"
+ "LICENSE STRING" + "LICENSE STRING"
+ ");"; + ");";
public static void onCreate(SQLiteDatabase db) { 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) { public static void onUpdate(SQLiteDatabase db, int from, int to) {
if(from == to) { if (from == to) {
return; 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 description STRING;");
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;"); db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;");
from++; from++;
onUpdate(db, from, to); onUpdate(db, from, to);
return; return;
} }
if(from == 2) { if (from == 2) {
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;"); db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;");
db.execSQL("UPDATE " + TABLE_NAME + " SET multiple = 0"); db.execSQL("UPDATE " + TABLE_NAME + " SET multiple = 0");
from++; from++;
onUpdate(db, from, to); onUpdate(db, from, to);
return; return;
} }
if(from == 3) { if (from == 3) {
// Do nothing // Do nothing
from++; from++;
onUpdate(db, from, to); onUpdate(db, from, to);
return; return;
} }
if(from == 4) { if (from == 4) {
// Do nothing -- added Category // Do nothing -- added Category
from++; from++;
onUpdate(db, from, to); onUpdate(db, from, to);
return; return;
} }
if(from == 5) { if (from == 5) {
// Added width and height fields // Added width and height fields
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;"); db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;");
db.execSQL("UPDATE " + TABLE_NAME + " SET width = 0"); db.execSQL("UPDATE " + TABLE_NAME + " SET width = 0");

View file

@ -3,15 +3,12 @@ package fr.free.nrw.commons.contributions;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.content.FileProvider; import android.support.v4.content.FileProvider;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.Date; import java.util.Date;
import fr.free.nrw.commons.upload.ShareActivity; import fr.free.nrw.commons.upload.ShareActivity;

View file

@ -12,15 +12,15 @@ import android.os.Bundle;
import android.os.RemoteException; import android.os.RemoteException;
import android.text.TextUtils; import android.text.TextUtils;
import org.mediawiki.api.ApiResult;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MWApi;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.mwapi.LogEventResult;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
@ -61,29 +61,17 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) { 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! // This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
String user = account.name; String user = account.name;
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
SharedPreferences prefs = this.getContext().getSharedPreferences("prefs", Context.MODE_PRIVATE); SharedPreferences prefs = this.getContext().getSharedPreferences("prefs", Context.MODE_PRIVATE);
String lastModified = prefs.getString("lastSyncTimestamp", ""); String lastModified = prefs.getString("lastSyncTimestamp", "");
Date curTime = new Date(); Date curTime = new Date();
ApiResult result; LogEventResult result;
Boolean done = false; Boolean done = false;
String queryContinue = null; String queryContinue = null;
while(!done) { while(!done) {
try { try {
MWApi.RequestBuilder builder = api.action("query") result = api.logEvents(user, lastModified, queryContinue, getLimit());
.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();
} catch (IOException e) { } catch (IOException e) {
// There isn't really much we can do, eh? // There isn't really much we can do, eh?
// FIXME: Perhaps add EventLogging? // FIXME: Perhaps add EventLogging?
@ -93,22 +81,21 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
} }
Timber.d("Last modified at %s", lastModified); Timber.d("Last modified at %s", lastModified);
ArrayList<ApiResult> uploads = result.getNodes("/api/query/logevents/item"); List<LogEventResult.LogEvent> logEvents = result.getLogEvents();
Timber.d("%d results!", uploads.size()); Timber.d("%d results!", logEvents.size());
ArrayList<ContentValues> imageValues = new ArrayList<>(); ArrayList<ContentValues> imageValues = new ArrayList<>();
for(ApiResult image: uploads) { for (LogEventResult.LogEvent image : logEvents) {
String pageId = image.getString("@pageid"); if (image.isDeleted()) {
if (pageId.equals("0")) {
// means that this upload was deleted. // means that this upload was deleted.
continue; continue;
} }
String filename = image.getString("@title"); String filename = image.getFilename();
if(fileExists(contentProviderClient, filename)) { if(fileExists(contentProviderClient, filename)) {
Timber.d("Skipping %s", filename); Timber.d("Skipping %s", filename);
continue; continue;
} }
String thumbUrl = Utils.makeThumbBaseUrl(filename); 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, "", ""); Contribution contrib = new Contribution(null, thumbUrl, filename, "", -1, dateUpdated, dateUpdated, user, "", "");
contrib.setState(Contribution.STATE_COMPLETED); contrib.setState(Contribution.STATE_COMPLETED);
imageValues.add(contrib.toContentValues()); imageValues.add(contrib.toContentValues());
@ -130,7 +117,8 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
queryContinue = result.getString("/api/query-continue/logevents/@lestart");
queryContinue = result.getQueryContinue();
if(TextUtils.isEmpty(queryContinue)) { if(TextUtils.isEmpty(queryContinue)) {
done = true; done = true;
} }

View file

@ -28,11 +28,11 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.EventLog;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.mwapi.EventLog;
public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPageChangeListener { public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPageChangeListener {
private ViewPager pager; private ViewPager pager;

View file

@ -12,15 +12,13 @@ import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.os.RemoteException; import android.os.RemoteException;
import fr.free.nrw.commons.MWApi;
import org.mediawiki.api.ApiResult;
import java.io.IOException; import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
@ -41,14 +39,14 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
} }
// Exit early if nothing to do // Exit early if nothing to do
if(allModifications == null || allModifications.getCount() == 0) { if (allModifications == null || allModifications.getCount() == 0) {
Timber.d("No modifications to perform"); Timber.d("No modifications to perform");
return; return;
} }
String authCookie; String authCookie;
try { try {
authCookie = AccountManager.get(getContext()).blockingGetAuthToken(account, "", false); authCookie = AccountManager.get(getContext()).blockingGetAuthToken(account, "", false);
} catch (OperationCanceledException | AuthenticatorException e) { } catch (OperationCanceledException | AuthenticatorException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} catch (IOException e) { } catch (IOException e) {
@ -56,16 +54,15 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
return; return;
} }
if(Utils.isNullOrWhiteSpace(authCookie)) { if (Utils.isNullOrWhiteSpace(authCookie)) {
Timber.d("Could not authenticate :("); Timber.d("Could not authenticate :(");
return; return;
} }
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
api.setAuthCookie(authCookie); api.setAuthCookie(authCookie);
String editToken; String editToken;
ApiResult requestResult, responseResult;
try { try {
editToken = api.getEditToken(); editToken = api.getEditToken();
} catch (IOException e) { } catch (IOException e) {
@ -81,7 +78,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
try { try {
contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY); contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY);
while(!allModifications.isAfterLast()) { while (!allModifications.isAfterLast()) {
ModifierSequence sequence = ModifierSequence.fromCursor(allModifications); ModifierSequence sequence = ModifierSequence.fromCursor(allModifications);
sequence.setContentProviderClient(contentProviderClient); sequence.setContentProviderClient(contentProviderClient);
Contribution contrib; Contribution contrib;
@ -95,41 +92,31 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
contributionCursor.moveToFirst(); contributionCursor.moveToFirst();
contrib = Contribution.fromCursor(contributionCursor); contrib = Contribution.fromCursor(contributionCursor);
if(contrib.getState() == Contribution.STATE_COMPLETED) { if (contrib.getState() == Contribution.STATE_COMPLETED) {
String pageContent;
try { try {
requestResult = api.action("query") pageContent = api.revisionsByFilename(contrib.getFilename());
.param("prop", "revisions")
.param("rvprop", "timestamp|content")
.param("titles", contrib.getFilename())
.get();
} catch (IOException e) { } catch (IOException e) {
Timber.d("Network fuckup on modifications sync!"); Timber.d("Network fuckup on modifications sync!");
continue; continue;
} }
Timber.d("Page content is %s", Utils.getStringFromDOM(requestResult.getDocument())); Timber.d("Page content is %s", pageContent);
String pageContent = requestResult.getString("/api/query/pages/page/revisions/rev"); String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent);
String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent);
String editResult;
try { try {
responseResult = api.action("edit") editResult = api.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
.param("title", contrib.getFilename())
.param("token", editToken)
.param("text", processedPageContent)
.param("summary", sequence.getEditSummary())
.post();
} catch (IOException e) { } catch (IOException e) {
Timber.d("Network fuckup on modifications sync!"); Timber.d("Network fuckup on modifications sync!");
continue; continue;
} }
Timber.d("Response is %s", Utils.getStringFromDOM(responseResult.getDocument())); Timber.d("Response is %s", editResult);
String result = responseResult.getString("/api/edit/@result"); if (!editResult.equals("Success")) {
if(!result.equals("Success")) {
// FIXME: Log this somewhere else // FIXME: Log this somewhere else
Timber.d("Non success result! %s", result); Timber.d("Non success result! %s", editResult);
} else { } else {
sequence.delete(); sequence.delete();
} }
@ -137,7 +124,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
allModifications.moveToNext(); allModifications.moveToNext();
} }
} finally { } finally {
if(contributionsClient != null) { if (contributionsClient != null) {
contributionsClient.release(); contributionsClient.release();
} }
} }

View file

@ -0,0 +1,374 @@
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 android.util.Log;
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 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 implements MediaWikiApi {
private static final String THUMB_SIZE = "640";
private AbstractHttpClient httpClient;
private MWApi api;
public ApacheHttpClientMediaWikiApi(String apiURL) {
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);
httpClient = new DefaultHttpClient(cm, params);
api = new MWApi(apiURL, httpClient);
}
/**
* @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(api.action("clientlogin")
.param("rememberMe", "1")
.param("username", username)
.param("password", password)
.param("logintoken", 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(api.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 api.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")) {
api.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;
}
@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 api.action("query")
.param("prop", "imageinfo")
.param("titles", "File:" + fileName)
.get()
.getNodes("/api/query/pages/page/imageinfo").size() > 0;
}
@Override
@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()
.getString("/api/edit/@result");
}
@Override
public String findThumbnailByFilename(String filename) throws IOException {
return api.action("query")
.param("format", "xml")
.param("prop", "imageinfo")
.param("iiprop", "url")
.param("iiurlwidth", THUMB_SIZE)
.param("titles", filename)
.get()
.getString("/api/query/pages/page/imageinfo/ii/@thumburl");
}
@Override
@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
@NonNull
public List<String> searchCategories(int searchCatsLimit, String filterValue) throws IOException {
List<ApiResult> categoryNodes = api.action("query")
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", searchCatsLimit)
.param("srsearch", filterValue)
.get()
.getNodes("/api/query/search/p/@title");
if (categoryNodes == null) {
return Collections.emptyList();
}
List<String> categories = new ArrayList<>();
for (ApiResult categoryNode : categoryNodes) {
String cat = categoryNode.getDocument().getTextContent();
String catString = cat.replace("Category:", "");
categories.add(catString);
}
return categories;
}
@Override
@NonNull
public List<String> allCategories(int searchCatsLimit, String filterValue) throws IOException {
ArrayList<ApiResult> categoryNodes = api.action("query")
.param("list", "allcategories")
.param("acprefix", filterValue)
.param("aclimit", searchCatsLimit)
.get()
.getNodes("/api/query/allcategories/c");
if (categoryNodes == null) {
return Collections.emptyList();
}
List<String> categories = new ArrayList<>();
for (ApiResult categoryNode : categoryNodes) {
categories.add(categoryNode.getDocument().getTextContent());
}
return categories;
}
@Override
@NonNull
public List<String> searchTitles(int searchCatsLimit, String title) throws IOException {
ArrayList<ApiResult> categoryNodes = api.action("query")
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", searchCatsLimit)
.param("srsearch", title)
.get()
.getNodes("/api/query/search/p/@title");
if (categoryNodes == null) {
return Collections.emptyList();
}
List<String> titleCategories = new ArrayList<>();
for (ApiResult categoryNode : categoryNodes) {
String cat = categoryNode.getDocument().getTextContent();
String catString = cat.replace("Category:", "");
titleCategories.add(catString);
}
return titleCategories;
}
@Override
@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")
.param("leuser", user)
.param("lelimit", limit);
if (!TextUtils.isEmpty(lastModified)) {
builder.param("leend", lastModified);
}
if (!TextUtils.isEmpty(queryContinue)) {
builder.param("lestart", queryContinue);
}
ApiResult result = builder.get();
return new LogEventResult(
getLogEventsFromResult(result),
result.getString("/api/query-continue/logevents/@lestart"));
}
@NonNull
private ArrayList<LogEventResult.LogEvent> getLogEventsFromResult(ApiResult result) {
ArrayList<ApiResult> uploads = result.getNodes("/api/query/logevents/item");
Timber.d("%d results!", uploads.size());
ArrayList<LogEventResult.LogEvent> 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
@Nullable
public String revisionsByFilename(String filename) throws IOException {
return api.action("query")
.param("prop", "revisions")
.param("rvprop", "timestamp|content")
.param("titles", filename)
.get()
.getString("/api/query/pages/page/revisions/rev");
}
@Override
public boolean existingFile(String fileSha1) throws IOException {
return api.action("query")
.param("format", "xml")
.param("list", "allimages")
.param("aisha1", fileSha1)
.get()
.getNodes("/api/query/allimages/img").size() > 0;
}
@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;
}
@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);
}
});
Log.e("WTF", "Result: "+result.toString());
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(resultStatus, dateUploaded, canonicalFilename, imageUrl);
}
}
}

View file

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

View file

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

View file

@ -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<LogEvent> logEvents;
private final String queryContinue;
LogEventResult(@NonNull List<LogEvent> logEvents, String queryContinue) {
this.logEvents = logEvents;
this.queryContinue = queryContinue;
}
@NonNull
public List<LogEvent> 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;
}
}
}

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.mwapi;
import android.os.AsyncTask;
import fr.free.nrw.commons.CommonsApplication;
class LogTask extends AsyncTask<LogBuilder, Void, Boolean> {
@Override
protected Boolean doInBackground(LogBuilder... logBuilders) {
return CommonsApplication.getInstance().getMWApi().logEvents(logBuilders);
}
}

View file

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

View file

@ -0,0 +1,58 @@
package fr.free.nrw.commons.mwapi;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
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;
boolean fileExistsWithName(String fileName) throws IOException;
String findThumbnailByFilename(String filename) 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<String> searchCategories(int searchCatsLimit, String filterValue) throws IOException;
@NonNull
List<String> allCategories(int searchCatsLimit, String filter) throws IOException;
@NonNull
List<String> 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);
}
}

View file

@ -0,0 +1,43 @@
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(String resultStatus, Date dateUploaded, String canonicalFilename, String imageUrl) {
this.resultStatus = resultStatus;
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;
}
}

View file

@ -6,15 +6,12 @@ import android.content.Intent;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import org.mediawiki.api.ApiResult;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MWApi;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -49,27 +46,18 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
@Override @Override
protected Boolean doInBackground(Void... voids) { 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 // https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba
boolean fileExists;
try { try {
result = api.action("query") String fileSha1 = this.fileSha1;
.param("format", "xml") fileExists = api.existingFile(fileSha1);
.param("list", "allimages")
.param("aisha1", fileSha1)
.get();
Timber.d("Searching Commons API for existing file: %s", result);
} catch (IOException e) { } catch (IOException e) {
Timber.e(e, "IO Exception: "); Timber.e(e, "IO Exception: ");
return false; return false;
} }
ArrayList<ApiResult> 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); Timber.d("File already exists in Commons: %s", fileExists);
return fileExists; return fileExists;
} }

View file

@ -7,7 +7,6 @@ import android.location.Location;
import android.location.LocationListener; import android.location.LocationListener;
import android.location.LocationManager; import android.location.LocationManager;
import android.media.ExifInterface; import android.media.ExifInterface;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;

View file

@ -20,12 +20,10 @@ import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.Toast; import android.widget.Toast;
import butterknife.ButterKnife;
import java.util.ArrayList; import java.util.ArrayList;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.EventLog;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity; 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.ModificationsContentProvider;
import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.modifications.TemplateRemoveModifier; import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.EventLog;
import timber.log.Timber; import timber.log.Timber;
public class MultipleShareActivity public class MultipleShareActivity

View file

@ -6,7 +6,6 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.graphics.drawable.VectorDrawableCompat; import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.text.Editable; import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;

View file

@ -32,7 +32,6 @@ import java.util.List;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.EventLog;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.AuthenticatedActivity; 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.ModificationsContentProvider;
import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.modifications.TemplateRemoveModifier; import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.EventLog;
import timber.log.Timber; import timber.log.Timber;
/** /**

View file

@ -32,9 +32,9 @@ import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
import butterknife.OnItemSelected; import butterknife.OnItemSelected;
import butterknife.OnTouch; import butterknife.OnTouch;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.settings.Prefs;
import timber.log.Timber; import timber.log.Timber;
public class SingleUploadFragment extends Fragment { public class SingleUploadFragment extends Fragment {

View file

@ -18,9 +18,9 @@ import java.util.Date;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.settings.Prefs;
import timber.log.Timber; import timber.log.Timber;
public class UploadController { public class UploadController {

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
@ -13,23 +14,25 @@ import android.support.v4.app.NotificationCompat;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import android.widget.Toast; import android.widget.Toast;
import fr.free.nrw.commons.*;
import org.mediawiki.api.ApiResult;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; 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.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import in.yuvi.http.fluent.ProgressListener; import fr.free.nrw.commons.mwapi.EventLog;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.UploadResult;
import timber.log.Timber; import timber.log.Timber;
public class UploadService extends HandlerService<Contribution> { public class UploadService extends HandlerService<Contribution> {
@ -64,7 +67,7 @@ public class UploadService extends HandlerService<Contribution> {
super("UploadService"); super("UploadService");
} }
private class NotificationUpdateProgressListener implements ProgressListener { private class NotificationUpdateProgressListener implements MediaWikiApi.ProgressListener {
String notificationTag; String notificationTag;
boolean notificationTitleChanged; boolean notificationTitleChanged;
@ -175,10 +178,10 @@ public class UploadService extends HandlerService<Contribution> {
return START_REDELIVER_INTENT; return START_REDELIVER_INTENT;
} }
@SuppressLint("StringFormatInvalid")
private void uploadContribution(Contribution contribution) { private void uploadContribution(Contribution contribution) {
MWApi api = app.getMWApi(); MediaWikiApi api = app.getMWApi();
ApiResult result;
InputStream file = null; InputStream file = null;
String notificationTag = contribution.getLocalUri().toString(); String notificationTag = contribution.getLocalUri().toString();
@ -235,32 +238,27 @@ public class UploadService extends HandlerService<Contribution> {
getString(R.string.upload_progress_notification_title_finishing, contribution.getDisplayTitle()), getString(R.string.upload_progress_notification_title_finishing, contribution.getDisplayTitle()),
contribution 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; curProgressNotification = null;
String resultStatus = result.getString("/api/upload/@result"); String resultStatus = uploadResult.getResultStatus();
if(!resultStatus.equals("Success")) { if(!resultStatus.equals("Success")) {
String errorCode = result.getString("/api/error/@code");
showFailedNotification(contribution); showFailedNotification(contribution);
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT) EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT)
.param("username", app.getCurrentAccount().name) .param("username", app.getCurrentAccount().name)
.param("source", contribution.getSource()) .param("source", contribution.getSource())
.param("multiple", contribution.getMultiple()) .param("multiple", contribution.getMultiple())
.param("result", errorCode) .param("result", uploadResult.getErrorCode())
.param("filename", contribution.getFilename()) .param("filename", contribution.getFilename())
.log(); .log();
} else { } else {
Date dateUploaded = null; contribution.setFilename(uploadResult.getCanonicalFilename());
dateUploaded = Utils.parseMWDate(result.getString("/api/upload/imageinfo/@timestamp")); contribution.setImageUrl(uploadResult.getImageUrl());
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.setState(Contribution.STATE_COMPLETED); contribution.setState(Contribution.STATE_COMPLETED);
contribution.setDateUploaded(dateUploaded); contribution.setDateUploaded(uploadResult.getDateUploaded());
contribution.save(); contribution.save();
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT) EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT)
@ -274,7 +272,6 @@ public class UploadService extends HandlerService<Contribution> {
} catch(IOException e) { } catch(IOException e) {
Timber.d("I have a network fuckup"); Timber.d("I have a network fuckup");
showFailedNotification(contribution); showFailedNotification(contribution);
return;
} finally { } finally {
if ( filename != null ) { if ( filename != null ) {
unfinishedUploads.remove(filename); unfinishedUploads.remove(filename);
@ -288,8 +285,9 @@ public class UploadService extends HandlerService<Contribution> {
} }
} }
@SuppressLint("StringFormatInvalid")
private void showFailedNotification(Contribution contribution) { 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) .setSmallIcon(R.drawable.ic_launcher)
.setAutoCancel(true) .setAutoCancel(true)
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0)) .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0))
@ -304,7 +302,7 @@ public class UploadService extends HandlerService<Contribution> {
} }
private String findUniqueFilename(String fileName) throws IOException { private String findUniqueFilename(String fileName) throws IOException {
MWApi api = app.getMWApi(); MediaWikiApi api = app.getMWApi();
String sequenceFileName; String sequenceFileName;
for ( int sequenceNumber = 1; true; sequenceNumber++ ) { for ( int sequenceNumber = 1; true; sequenceNumber++ ) {
if (sequenceNumber == 1) { if (sequenceNumber == 1) {
@ -320,7 +318,7 @@ public class UploadService extends HandlerService<Contribution> {
sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2"); sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2");
} }
} }
if ( fileExistsWithName(api, sequenceFileName) || unfinishedUploads.contains(sequenceFileName) ) { if ( api.fileExistsWithName(sequenceFileName) || unfinishedUploads.contains(sequenceFileName) ) {
continue; continue;
} else { } else {
break; break;
@ -328,15 +326,4 @@ public class UploadService extends HandlerService<Contribution> {
} }
return sequenceFileName; 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;
}
} }

View file

@ -1,9 +1,9 @@
package fr.free.nrw.commons.utils; package fr.free.nrw.commons.utils;
import fr.free.nrw.commons.location.LatLng;
import java.text.NumberFormat; import java.text.NumberFormat;
import fr.free.nrw.commons.location.LatLng;
public class LengthUtils { public class LengthUtils {
/** Returns a formatted distance string between two points. /** Returns a formatted distance string between two points.
* @param point1 LatLng type point1 * @param point1 LatLng type point1

View file

@ -0,0 +1,227 @@
package fr.free.nrw.commons.mwapi;
import android.os.Build;
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;
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;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class ApacheHttpClientMediaWikiApiTest {
private ApacheHttpClientMediaWikiApi testObject;
private MockWebServer server;
@Before
public void setUp() throws Exception {
server = new MockWebServer();
testObject = new ApacheHttpClientMediaWikiApi("http://" + server.getHostName() + ":" + 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("<?xml version=\"1.0\"?><api batchcomplete=\"\"><query><tokens logintoken=\"baz\" /></query></api>"));
server.enqueue(new MockResponse().setBody("<?xml version=\"1.0\"?><api><clientlogin status=\"FAIL\" message=\"Incorrect password entered.&#10;Please try again.\" messagecode=\"wrongpassword\" /></api>"));
String result = testObject.login("foo", "bar");
RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "POST");
Map<String, String> 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("<?xml version=\"1.0\"?><api batchcomplete=\"\"><query><tokens logintoken=\"baz\" /></query></api>"));
server.enqueue(new MockResponse().setBody("<?xml version=\"1.0\"?><api><clientlogin status=\"PASS\" username=\"foo\" /></api>"));
String result = testObject.login("foo", "bar");
RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "POST");
Map<String, String> 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("<?xml version=\"1.0\"?><api batchcomplete=\"\"><query><tokens logintoken=\"baz\" /></query></api>"));
server.enqueue(new MockResponse().setBody("<?xml version=\"1.0\"?><api><clientlogin status=\"PASS\" username=\"foo\" /></api>"));
String result = testObject.login("foo", "bar", "2fa");
RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "POST");
Map<String, String> 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("<?xml version=\"1.0\"?><api><query><userinfo id=\"10\" name=\"foo\"/></query></api>"));
boolean result = testObject.validateLogin();
RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "GET");
Map<String, String> 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("<?xml version=\"1.0\"?><api><query><userinfo id=\"0\" name=\"foo\"/></query></api>"));
boolean result = testObject.validateLogin();
RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "GET");
Map<String, String> 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("<?xml version=\"1.0\"?><api><tokens edittoken=\"baz\" /></api>"));
String result = testObject.getEditToken();
RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "GET");
Map<String, String> 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("<?xml version=\"1.0\"?><api batchcomplete=\"\"><query> <normalized><n from=\"File:foo\" to=\"File:Foo\" /></normalized><pages><page _idx=\"-1\" ns=\"6\" title=\"File:Foo\" missing=\"\" imagerepository=\"\" /></pages></query></api>"));
boolean result = testObject.fileExistsWithName("foo");
RecordedRequest request = assertBasicRequestParameters(server, "GET");
Map<String, String> 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<String, String> parseQueryParams(RecordedRequest request) {
Map<String, String> result = new HashMap<>();
HttpUrl url = request.getRequestUrl();
Set<String> params = url.queryParameterNames();
for (String name : params) {
result.put(name, url.queryParameter(name));
}
return result;
}
private Map<String, String> parseBody(String body) throws UnsupportedEncodingException {
String[] props = body.split("&");
Map<String, String> result = new HashMap<>();
for (String prop : props) {
String[] pair = prop.split("=");
result.put(pair[0], URLDecoder.decode(pair[1], "utf-8"));
}
return result;
}
}

View file

@ -7,6 +7,7 @@ buildscript {
dependencies { dependencies {
classpath "com.android.tools.build:gradle:${project.gradleVersion}" classpath "com.android.tools.build:gradle:${project.gradleVersion}"
classpath 'com.dicedmelon.gradle:jacoco-android:0.1.1' classpath 'com.dicedmelon.gradle:jacoco-android:0.1.1'
classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.7.1'
} }
} }

View file

@ -1,6 +1,6 @@
gradleVersion = 2.3.0 gradleVersion = 2.3.0
supportLibVersion = 25.2.0 supportLibVersion = 25.3.1
compileSdkVersion = android-25 compileSdkVersion = android-25
buildToolsVersion = 25.0.1 buildToolsVersion = 25.0.1
@ -11,6 +11,7 @@ targetSdkVersion = 25
android.useDeprecatedNdk=true android.useDeprecatedNdk=true
# Library dependencies # Library dependencies
BUTTERKNIFE_VERSION=8.4.0 BUTTERKNIFE_VERSION=8.6.0
GUAVA_VERSION=19.0 GUAVA_VERSION=19.0
org.gradle.jvmargs=-Xmx1536M