Merge pull request #1 from commons-app/master

Merge upstream master
This commit is contained in:
Mikel 2017-07-18 21:55:25 +01:00 committed by GitHub
commit 032c7873e5
67 changed files with 1239 additions and 676 deletions

View file

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

View file

@ -9,9 +9,8 @@ import android.support.test.espresso.matcher.ViewMatchers;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.view.View;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -20,9 +19,6 @@ import java.util.Map;
import fr.free.nrw.commons.settings.SettingsActivity;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class SettingsActivityTest {
@ -65,8 +61,8 @@ public class SettingsActivityTest {
@Test
public void oneLicenseIsChecked() {
// click "License" (the first item)
Espresso.onData(anything())
.inAdapterView(findPreferenceList())
Espresso.onData(Matchers.anything())
.inAdapterView(ViewMatchers.withId(android.R.id.list))
.atPosition(0)
.perform(ViewActions.click());
@ -78,8 +74,8 @@ public class SettingsActivityTest {
@Test
public void afterClickingCcby4ItWillStay() {
// click "License" (the first item)
Espresso.onData(anything())
.inAdapterView(findPreferenceList())
Espresso.onData(Matchers.anything())
.inAdapterView(ViewMatchers.withId(android.R.id.list))
.atPosition(0)
.perform(ViewActions.click());
@ -89,8 +85,8 @@ public class SettingsActivityTest {
).perform(ViewActions.click());
// click "License" (the first item)
Espresso.onData(anything())
.inAdapterView(findPreferenceList())
Espresso.onData(Matchers.anything())
.inAdapterView(ViewMatchers.withId(android.R.id.list))
.atPosition(0)
.perform(ViewActions.click());
@ -100,12 +96,4 @@ public class SettingsActivityTest {
ViewMatchers.withText(R.string.license_name_cc_by_four)
));
}
private static Matcher<View> findPreferenceList() {
return allOf(
ViewMatchers.isDescendantOfA(ViewMatchers.withId(R.id.settingsFragment)),
ViewMatchers.withResourceName("list"),
ViewMatchers.hasFocus()
);
}
}
}

View file

@ -8,9 +8,11 @@ import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.ui.widget.HtmlTextView;
public class AboutActivity extends NavigationBaseActivity {
@BindView(R.id.about_version) TextView versionText;
@BindView(R.id.about_license) HtmlTextView aboutLicenseText;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -19,6 +21,9 @@ public class AboutActivity extends NavigationBaseActivity {
ButterKnife.bind(this);
String aboutText = getString(R.string.about_license, getString(R.string.trademarked_name));
aboutLicenseText.setHtmlText(aboutText);
versionText.setText(BuildConfig.VERSION_NAME);
initDrawer();
}

View file

@ -8,41 +8,30 @@ import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.database.sqlite.SQLiteDatabase;
import android.preference.PreferenceManager;
import android.support.v4.util.LruCache;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.stetho.Stetho;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.category.Category;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.nearby.NearbyPlaces;
import com.squareup.leakcanary.LeakCanary;
import org.acra.ACRA;
import org.acra.ReportingInteractionMode;
import org.acra.annotation.ReportsCrashes;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreProtocolPNames;
import java.io.File;
import java.io.IOException;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.category.Category;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.utils.FileUtils;
import timber.log.Timber;
@ -76,9 +65,8 @@ public class CommonsApplication extends Application {
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
private static CommonsApplication instance = null;
private AbstractHttpClient httpClient = null;
private MWApi api = null;
LruCache<String, String> thumbnailUrlCache = new LruCache<>(1024);
private MediaWikiApi api = null;
private LruCache<String, String> thumbnailUrlCache = new LruCache<>(1024);
private CacheController cacheData = null;
private DBOpenHelper dbOpenHelper = null;
private NearbyPlaces nearbyPlaces = null;
@ -98,35 +86,13 @@ public class CommonsApplication extends Application {
return instance;
}
public AbstractHttpClient getHttpClient() {
if (httpClient == null) {
httpClient = newHttpClient();
}
return httpClient;
}
private AbstractHttpClient newHttpClient() {
BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
params.setParameter(CoreProtocolPNames.USER_AGENT, "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE);
return new DefaultHttpClient(cm, params);
}
public MWApi getMWApi() {
public MediaWikiApi getMWApi() {
if (api == null) {
api = newMWApi();
api = new ApacheHttpClientMediaWikiApi(API_URL);
}
return api;
}
private MWApi newMWApi() {
return new MWApi(API_URL, getHttpClient());
}
public CacheController getCacheData() {
if (cacheData == null) {
cacheData = new CacheController();
@ -174,9 +140,6 @@ public class CommonsApplication extends Application {
Fresco.initialize(this);
// Initialize EventLogging
EventLog.setApp(this);
//For caching area -> categories
cacheData = new CacheController();
}

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

View file

@ -3,10 +3,9 @@ package fr.free.nrw.commons;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import org.mediawiki.api.ApiResult;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
private static final String THUMB_SIZE = "640";
protected final Media media;
public MediaThumbnailFetchTask(@NonNull Media media) {
@ -16,15 +15,8 @@ class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
@Override
protected String doInBackground(String... params) {
try {
MWApi api = CommonsApplication.getInstance().getMWApi();
ApiResult result =api.action("query")
.param("format", "xml")
.param("prop", "imageinfo")
.param("iiprop", "url")
.param("iiurlwidth", THUMB_SIZE)
.param("titles", params[0])
.get();
return result.getString("/api/query/pages/page/imageinfo/ii/@thumburl");
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
return api.findThumbnailByFilename(params[0]);
} catch (Exception e) {
// Do something better!
}

View file

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

View file

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

View file

@ -12,6 +12,7 @@ import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import timber.log.Timber;
public abstract class AuthenticatedActivity extends NavigationBaseActivity {

View file

@ -19,9 +19,10 @@ import android.widget.Toast;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import timber.log.Timber;
@ -60,7 +61,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
usernameEdit.addTextChangedListener(textWatcher);
passwordEdit.addTextChangedListener(textWatcher);
twoFactorEdit.addTextChangedListener(textWatcher);
passwordEdit.setOnEditorActionListener( newLoginInputActionListener() );
passwordEdit.setOnEditorActionListener(newLoginInputActionListener());
loginButton.setOnClickListener(new View.OnClickListener() {
@Override
@ -150,7 +151,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
private LoginTask getLoginTask() {
return new LoginTask(
this,
canonicializeUsername( usernameEdit.getText().toString() ),
canonicializeUsername(usernameEdit.getText().toString()),
passwordEdit.getText().toString(),
twoFactorEdit.getText().toString()
);
@ -161,16 +162,16 @@ public class LoginActivity extends AccountAuthenticatorActivity {
* @param username String
* @return String canonicial username
*/
private String canonicializeUsername( String username ) {
private String canonicializeUsername(String username) {
return new PageTitle(username).getText();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
@ -185,20 +186,20 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
public void askUserForTwoFactorAuth() {
if(BuildConfig.DEBUG) {
if (BuildConfig.DEBUG) {
twoFactorEdit.setVisibility(View.VISIBLE);
showUserToastAndCancelDialog( R.string.login_failed_2fa_needed );
}else{
showUserToastAndCancelDialog( R.string.login_failed_2fa_not_supported );
showUserToastAndCancelDialog(R.string.login_failed_2fa_needed);
} else {
showUserToastAndCancelDialog(R.string.login_failed_2fa_not_supported);
}
}
public void showUserToastAndCancelDialog( int resId ) {
showUserToast( resId );
public void showUserToastAndCancelDialog(int resId) {
showUserToast(resId);
progressDialog.cancel();
}
private void showUserToast( int resId ) {
private void showUserToast(int resId) {
Toast.makeText(this, resId, Toast.LENGTH_LONG).show();
}

View file

@ -9,8 +9,8 @@ import android.os.Bundle;
import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.EventLog;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.mwapi.EventLog;
import timber.log.Timber;
class LoginTask extends AsyncTask<String, String, String> {
@ -67,7 +67,7 @@ class LoginTask extends AsyncTask<String, String, String> {
if (result.equals("PASS")) {
handlePassResult();
} else {
handleOtherResults( result );
handleOtherResults(result);
}
}
@ -88,38 +88,38 @@ class LoginTask extends AsyncTask<String, String, String> {
}
}
AccountUtil.createAccount( response, username, password );
AccountUtil.createAccount(response, username, password);
loginActivity.startMainActivity();
}
/**
* Match known failure message codes and provide messages
* Match known failure message codes and provide messages.
* @param result String
*/
private void handleOtherResults( String result ) {
private void handleOtherResults(String result) {
if (result.equals("NetworkFailure")) {
// Matches NetworkFailure which is created by the doInBackground method
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_network );
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_network);
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
// Matches nosuchuser, nosuchusershort, noname
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_username );
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_username);
loginActivity.emptySensitiveEditFields();
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
// Matches wrongpassword, wrongpasswordempty
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_password );
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_password);
loginActivity.emptySensitiveEditFields();
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
// Matches unknown throttle error codes
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_throttled );
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_throttled);
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
// Matches login-userblocked
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_blocked );
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_blocked);
} else if (result.equals("2FA")) {
loginActivity.askUserForTwoFactorAuth();
} else {
// Occurs with unhandled login failure codes
Timber.d("Login failed with reason: %s", result);
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_generic );
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_generic);
}
}
}

View file

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

View file

@ -5,17 +5,17 @@ import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class WikiAccountAuthenticatorService extends Service{
public class WikiAccountAuthenticatorService extends Service {
private static WikiAccountAuthenticator wikiAccountAuthenticator = null;
@Override
public IBinder onBind(Intent intent) {
if (!intent.getAction().equals(AccountManager.ACTION_AUTHENTICATOR_INTENT)) {
return null;
return null;
}
if(wikiAccountAuthenticator == null) {
if (wikiAccountAuthenticator == null) {
wikiAccountAuthenticator = new WikiAccountAuthenticator(this);
}
return wikiAccountAuthenticator.getIBinder();

View file

@ -74,14 +74,14 @@ public class CacheController {
double offset = 100;
//Coordinate offsets in radians
double dLat = offset/EARTH_RADIUS;
double dLon = offset/(EARTH_RADIUS*Math.cos(Math.PI*lat/180));
double dLat = offset / EARTH_RADIUS;
double dLon = offset / (EARTH_RADIUS * Math.cos(Math.PI * lat / 180));
//OffsetPosition, decimal degrees
yPlus = lat + dLat * 180/Math.PI;
yMinus = lat - dLat * 180/Math.PI;
xPlus = lon + dLon * 180/Math.PI;
xMinus = lon - dLon * 180/Math.PI;
yPlus = lat + dLat * 180 / Math.PI;
yMinus = lat - dLat * 180 / Math.PI;
xPlus = lon + dLon * 180 / Math.PI;
xMinus = lon - dLon * 180 / Math.PI;
Timber.d("Search within: xMinus=%s, yMinus=%s, xPlus=%s, yPlus=%s",
xMinus, yMinus, xPlus, yPlus);
}

View file

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

View file

@ -60,12 +60,12 @@ public class Category {
public void save() {
try {
if(contentUri == null) {
if (contentUri == null) {
contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues());
} else {
client.update(contentUri, toContentValues(), null, null);
}
} catch(RemoteException e) {
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
@ -121,23 +121,23 @@ public class Category {
}
public static void onUpdate(SQLiteDatabase db, int from, int to) {
if(from == to) {
if (from == to) {
return;
}
if(from < 4) {
if (from < 4) {
// doesn't exist yet
from++;
onUpdate(db, from, to);
return;
}
if(from == 4) {
if (from == 4) {
// table added in version 5
onCreate(db);
from++;
onUpdate(db, from, to);
return;
}
if(from == 5) {
if (from == 5) {
from++;
onUpdate(db, from, to);
return;

View file

@ -3,15 +3,14 @@ package fr.free.nrw.commons.category;
import android.os.AsyncTask;
import android.view.View;
import fr.free.nrw.commons.MWApi;
import org.mediawiki.api.ApiResult;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
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
* 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;
CategorizationFragment catFragment;
private CategorizationFragment catFragment;
public MethodAUpdater(CategorizationFragment catFragment) {
MethodAUpdater(CategorizationFragment 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
* and previous year
* Rationale: https://github.com/commons-app/apps-android-commons/issues/47
*
* @param items Unfiltered list of categories
* @return Filtered category list
*/
private ArrayList<String> filterYears(ArrayList<String> items) {
private List<String> filterYears(List<String> items) {
Iterator<String> iterator;
@ -60,12 +60,12 @@ public class MethodAUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
Timber.d("Previous year: %s", prevYearInString);
//Copy to Iterator to prevent ConcurrentModificationException when removing item
for(iterator = items.iterator(); iterator.hasNext();) {
for (iterator = items.iterator(); iterator.hasNext(); ) {
String s = iterator.next();
//Check if s contains a 4-digit word anywhere within the string (.* is wildcard)
//And that s does not equal the current year or previous year
if(s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString)) {
if (s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString)) {
Timber.d("Filtering out year %s", s);
iterator.remove();
}
@ -76,37 +76,22 @@ public class MethodAUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
}
@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
MWApi api = CommonsApplication.getInstance().getMWApi();
ApiResult result;
ArrayList<String> categories = new ArrayList<>();
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
List<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=
try {
result = api.action("query")
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", catFragment.SEARCH_CATS_LIMIT)
.param("srsearch", filter)
.get();
Timber.d("Method A URL filter %s", result);
categories = api.searchCategories(CategorizationFragment.SEARCH_CATS_LIMIT, filter);
Timber.d("Method A URL filter %s", categories);
} catch (IOException e) {
Timber.e(e, "IO Exception: ");
//Return empty arraylist
return categories;
}
ArrayList<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");
return new ArrayList<>(filterYears(categories));
}

View file

@ -4,15 +4,14 @@ import android.os.AsyncTask;
import android.text.TextUtils;
import android.view.View;
import fr.free.nrw.commons.MWApi;
import org.mediawiki.api.ApiResult;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
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
* the results.
*/
public class PrefixUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
public class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
private String filter;
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
* and previous year
* Rationale: https://github.com/commons-app/apps-android-commons/issues/47
*
* @param items Unfiltered list of categories
* @return Filtered category list
*/
private ArrayList<String> filterYears(ArrayList<String> items) {
private List<String> filterIrrelevantResults(List<String> items) {
Iterator<String> iterator;
@ -62,15 +62,18 @@ public class PrefixUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
Timber.d("Previous year: %s", prevYearInString);
//Copy to Iterator to prevent ConcurrentModificationException when removing item
for(iterator = items.iterator(); iterator.hasNext();) {
for (iterator = items.iterator(); iterator.hasNext();) {
String s = iterator.next();
//Check if s contains a 4-digit word anywhere within the string (.* is wildcard)
//And that s does not equal the current year or previous year
if(s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString)) {
Timber.d("Filtering out year %s", s);
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
if ((s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString))
|| s.matches("(.*)needing(.*)")||s.matches("(.*)taken on(.*)")) {
Timber.d("Filtering out irrelevant result: %s", s);
iterator.remove();
}
}
Timber.d("Items: %s", items);
@ -78,45 +81,35 @@ public class PrefixUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
}
@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(TextUtils.isEmpty(filter)) {
if (TextUtils.isEmpty(filter)) {
ArrayList<String> mergedItems = new ArrayList<>(catFragment.mergeItems());
Timber.d("Merged items, waiting for filter");
return new ArrayList<>(filterYears(mergedItems));
return new ArrayList<>(filterIrrelevantResults(mergedItems));
}
//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));
Timber.d("Found cache items, waiting for filter");
return new ArrayList<>(filterYears(cachedItems));
return new ArrayList<>(filterIrrelevantResults(cachedItems));
}
//otherwise if user has typed something in that isn't in cache, search API for matching categories
//URL: https://commons.wikimedia.org/w/api.php?action=query&list=allcategories&acprefix=filter&aclimit=25
MWApi api = CommonsApplication.getInstance().getMWApi();
ApiResult result;
ArrayList<String> categories = new ArrayList<>();
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
List<String> categories = new ArrayList<>();
try {
result = api.action("query")
.param("list", "allcategories")
.param("acprefix", filter)
.param("aclimit", catFragment.SEARCH_CATS_LIMIT)
.get();
Timber.d("Prefix URL filter %s", result);
categories = api.allCategories(CategorizationFragment.SEARCH_CATS_LIMIT, this.filter);
Timber.d("Prefix URL filter %s", categories);
} catch (IOException e) {
Timber.e(e, "IO Exception: ");
//Return empty arraylist
return categories;
}
ArrayList<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");
return new ArrayList<>(filterYears(categories));
return new ArrayList<>(filterIrrelevantResults(categories));
}
}

View file

@ -2,13 +2,12 @@ package fr.free.nrw.commons.category;
import android.os.AsyncTask;
import fr.free.nrw.commons.MWApi;
import org.mediawiki.api.ApiResult;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
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
* 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 String title;
public TitleCategories(String title) {
TitleCategories(String title) {
this.title = title;
}
@ -32,39 +31,23 @@ public class TitleCategories extends AsyncTask<Void, Void, ArrayList<String>> {
}
@Override
protected ArrayList<String> doInBackground(Void... voids) {
protected List<String> doInBackground(Void... voids) {
MWApi api = CommonsApplication.getInstance().getMWApi();
ApiResult result;
ArrayList<String> items = new ArrayList<>();
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
List<String> titleCategories = new ArrayList<>();
//URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch=
try {
result = api.action("query")
.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);
titleCategories = api.searchTitles(SEARCH_CATS_LIMIT, this.title);
} catch (IOException e) {
Timber.e(e, "IO Exception: ");
//Return empty arraylist
return items;
return titleCategories;
}
ArrayList<ApiResult> categoryNodes = result.getNodes("/api/query/search/p/@title");
for(ApiResult categoryNode: categoryNodes) {
String cat = categoryNode.getDocument().getTextContent();
String catString = cat.replace("Category:", "");
items.add(catString);
}
Timber.d("Title cat query results: %s", titleCategories);
Timber.d("Title cat query results: %s", items);
return items;
return titleCategories;
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -28,13 +28,26 @@ import android.view.View;
import android.view.ViewGroup;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.EventLog;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.mwapi.EventLog;
public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPageChangeListener {
public interface MediaDetailProvider {
Media getMediaAtPosition(int i);
int getTotalMediaCount();
void notifyDatasetChanged();
void registerDataSetObserver(DataSetObserver observer);
void unregisterDataSetObserver(DataSetObserver observer);
}
private ViewPager pager;
private Boolean editable;
private CommonsApplication app;
@ -48,14 +61,6 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
this.editable = editable;
}
public interface MediaDetailProvider {
Media getMediaAtPosition(int i);
int getTotalMediaCount();
void notifyDatasetChanged();
void registerDataSetObserver(DataSetObserver observer);
void unregisterDataSetObserver(DataSetObserver observer);
}
//FragmentStatePagerAdapter allows user to swipe across collection of images (no. of images undetermined)
private class MediaDetailAdapter extends FragmentStatePagerAdapter {
@ -65,7 +70,7 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
@Override
public Fragment getItem(int i) {
if(i == 0) {
if (i == 0) {
// See bug https://code.google.com/p/android/issues/detail?id=27526
pager.postDelayed(new Runnable() {
@Override
@ -120,7 +125,7 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState != null) {
if (savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable");
}
app = CommonsApplication.getInstance();
@ -206,13 +211,13 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if(!editable) { // Disable menu options for editable views
if (!editable) { // Disable menu options for editable views
menu.clear(); // see http://stackoverflow.com/a/8495697/17865
inflater.inflate(R.menu.fragment_image_detail, menu);
if(pager != null) {
if (pager != null) {
MediaDetailProvider provider = (MediaDetailProvider)getActivity();
Media m = provider.getMediaAtPosition(pager.getCurrentItem());
if(m != null) {
if (m != null) {
// Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
menu.findItem(R.id.menu_retry_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_cancel_current_image).setEnabled(false).setVisible(false);

View file

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

@ -16,6 +16,17 @@ import fr.free.nrw.commons.utils.UriDeserializer;
import fr.free.nrw.commons.utils.UriSerializer;
public class NearbyBaseMarker extends BaseMarkerOptions<NearbyMarker, NearbyBaseMarker> {
public static final Parcelable.Creator<NearbyBaseMarker> CREATOR = new Parcelable.Creator<NearbyBaseMarker>() {
public NearbyBaseMarker createFromParcel(Parcel in) {
return new NearbyBaseMarker(in);
}
public NearbyBaseMarker[] newArray(int size) {
return new NearbyBaseMarker[size];
}
};
private Place place;
NearbyBaseMarker() {
@ -74,15 +85,4 @@ public class NearbyBaseMarker extends BaseMarkerOptions<NearbyMarker, NearbyBase
dest.writeString(title);
dest.writeString(gson.toJson(place));
}
public static final Parcelable.Creator<NearbyBaseMarker> CREATOR
= new Parcelable.Creator<NearbyBaseMarker>() {
public NearbyBaseMarker createFromParcel(Parcel in) {
return new NearbyBaseMarker(in);
}
public NearbyBaseMarker[] newArray(int size) {
return new NearbyBaseMarker[size];
}
};
}

View file

@ -19,4 +19,8 @@ public class HtmlTextView extends AppCompatTextView {
setMovementMethod(LinkMovementMethod.getInstance());
setText(Utils.fromHtml(getText().toString()));
}
public void setHtmlText(String newText) {
setText(Utils.fromHtml(newText));
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -32,7 +32,6 @@ import java.util.List;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.EventLog;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
@ -42,6 +41,7 @@ import fr.free.nrw.commons.modifications.CategoryModifier;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.EventLog;
import timber.log.Timber;
/**
@ -209,16 +209,6 @@ public class ShareActivity
protected void onAuthCookieAcquired(String authCookie) {
app.getMWApi().setAuthCookie(authCookie);
SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView");
categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization");
if(shareView == null && categorizationFragment == null) {
shareView = new SingleUploadFragment();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.single_upload_fragment_container, shareView, "shareView")
.commitAllowingStateLoss();
}
uploadController.prepareService();
}
@Override
@ -311,6 +301,18 @@ public class ShareActivity
}
}
performPreuploadProcessingOfFile();
SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView");
categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization");
if(shareView == null && categorizationFragment == null) {
shareView = new SingleUploadFragment();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.single_upload_fragment_container, shareView, "shareView")
.commitAllowingStateLoss();
}
uploadController.prepareService();
}
@Override

View file

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

View file

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

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@ -13,23 +14,25 @@ import android.support.v4.app.NotificationCompat;
import android.webkit.MimeTypeMap;
import android.widget.Toast;
import fr.free.nrw.commons.*;
import org.mediawiki.api.ApiResult;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import 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;
public class UploadService extends HandlerService<Contribution> {
@ -64,7 +67,7 @@ public class UploadService extends HandlerService<Contribution> {
super("UploadService");
}
private class NotificationUpdateProgressListener implements ProgressListener {
private class NotificationUpdateProgressListener implements MediaWikiApi.ProgressListener {
String notificationTag;
boolean notificationTitleChanged;
@ -83,12 +86,12 @@ public class UploadService extends HandlerService<Contribution> {
@Override
public void onProgress(long transferred, long total) {
Timber.d("Uploaded %d of %d", transferred, total);
if(!notificationTitleChanged) {
if (!notificationTitleChanged) {
curProgressNotification.setContentTitle(notificationProgressTitle);
notificationTitleChanged = true;
contribution.setState(Contribution.STATE_IN_PROGRESS);
}
if(transferred == total) {
if (transferred == total) {
// Completed!
curProgressNotification.setContentTitle(notificationFinishingTitle);
curProgressNotification.setProgress(0, 100, true);
@ -121,7 +124,7 @@ public class UploadService extends HandlerService<Contribution> {
@Override
protected void handle(int what, Contribution contribution) {
switch(what) {
switch (what) {
case ACTION_UPLOAD_FILE:
//FIXME: Google Photos bug
uploadContribution(contribution);
@ -159,7 +162,7 @@ public class UploadService extends HandlerService<Contribution> {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if(intent.getAction().equals(ACTION_START_SERVICE) && freshStart) {
if (intent.getAction().equals(ACTION_START_SERVICE) && freshStart) {
ContentValues failedValues = new ContentValues();
failedValues.put(Contribution.Table.COLUMN_STATE, Contribution.STATE_FAILED);
@ -175,10 +178,10 @@ public class UploadService extends HandlerService<Contribution> {
return START_REDELIVER_INTENT;
}
@SuppressLint("StringFormatInvalid")
private void uploadContribution(Contribution contribution) {
MWApi api = app.getMWApi();
MediaWikiApi api = app.getMWApi();
ApiResult result;
InputStream file = null;
String notificationTag = contribution.getLocalUri().toString();
@ -186,7 +189,7 @@ public class UploadService extends HandlerService<Contribution> {
try {
//FIXME: Google Photos bug
file = this.getContentResolver().openInputStream(contribution.getLocalUri());
} catch(FileNotFoundException e) {
} catch (FileNotFoundException e) {
Timber.d("File not found");
Toast fileNotFound = Toast.makeText(this, R.string.upload_failed, Toast.LENGTH_LONG);
fileNotFound.show();
@ -217,9 +220,9 @@ public class UploadService extends HandlerService<Contribution> {
filename = findUniqueFilename(filename);
unfinishedUploads.add(filename);
}
if(!api.validateLogin()) {
if (!api.validateLogin()) {
// Need to revalidate!
if(app.revalidateAuthToken()) {
if (app.revalidateAuthToken()) {
Timber.d("Successfully revalidated token!");
} else {
Timber.d("Unable to revalidate :(");
@ -235,32 +238,27 @@ public class UploadService extends HandlerService<Contribution> {
getString(R.string.upload_progress_notification_title_finishing, contribution.getDisplayTitle()),
contribution
);
result = api.upload(filename, file, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), notificationUpdater);
UploadResult uploadResult = api.uploadFile(filename, file, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), notificationUpdater);
Timber.d("Response is %s", Utils.getStringFromDOM(result.getDocument()));
Timber.d("Response is %s", uploadResult.toString());
curProgressNotification = null;
String resultStatus = result.getString("/api/upload/@result");
if(!resultStatus.equals("Success")) {
String errorCode = result.getString("/api/error/@code");
String resultStatus = uploadResult.getResultStatus();
if (!resultStatus.equals("Success")) {
showFailedNotification(contribution);
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT)
.param("username", app.getCurrentAccount().name)
.param("source", contribution.getSource())
.param("multiple", contribution.getMultiple())
.param("result", errorCode)
.param("result", uploadResult.getErrorCode())
.param("filename", contribution.getFilename())
.log();
} else {
Date dateUploaded = null;
dateUploaded = Utils.parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename
String imageUrl = result.getString("/api/upload/imageinfo/@url");
contribution.setFilename(canonicalFilename);
contribution.setImageUrl(imageUrl);
contribution.setFilename(uploadResult.getCanonicalFilename());
contribution.setImageUrl(uploadResult.getImageUrl());
contribution.setState(Contribution.STATE_COMPLETED);
contribution.setDateUploaded(dateUploaded);
contribution.setDateUploaded(uploadResult.getDateUploaded());
contribution.save();
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT)
@ -271,16 +269,15 @@ public class UploadService extends HandlerService<Contribution> {
.param("result", "success")
.log();
}
} catch(IOException e) {
} catch (IOException e) {
Timber.d("I have a network fuckup");
showFailedNotification(contribution);
return;
} finally {
if ( filename != null ) {
if (filename != null) {
unfinishedUploads.remove(filename);
}
toUpload--;
if(toUpload == 0) {
if (toUpload == 0) {
// Sync modifications right after all uplaods are processed
ContentResolver.requestSync((CommonsApplication.getInstance()).getCurrentAccount(), ModificationsContentProvider.AUTHORITY, new Bundle());
stopForeground(true);
@ -288,6 +285,7 @@ public class UploadService extends HandlerService<Contribution> {
}
}
@SuppressLint("StringFormatInvalid")
private void showFailedNotification(Contribution contribution) {
Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
@ -304,9 +302,9 @@ public class UploadService extends HandlerService<Contribution> {
}
private String findUniqueFilename(String fileName) throws IOException {
MWApi api = app.getMWApi();
MediaWikiApi api = app.getMWApi();
String sequenceFileName;
for ( int sequenceNumber = 1; true; sequenceNumber++ ) {
for (int sequenceNumber = 1; true; sequenceNumber++) {
if (sequenceNumber == 1) {
sequenceFileName = fileName;
} else {
@ -320,23 +318,11 @@ public class UploadService extends HandlerService<Contribution> {
sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2");
}
}
if ( fileExistsWithName(api, sequenceFileName) || unfinishedUploads.contains(sequenceFileName) ) {
continue;
} else {
if (!api.fileExistsWithName(sequenceFileName)
&& !unfinishedUploads.contains(sequenceFileName)) {
break;
}
}
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;
import fr.free.nrw.commons.location.LatLng;
import java.text.NumberFormat;
import fr.free.nrw.commons.location.LatLng;
public class LengthUtils {
/** Returns a formatted distance string between two points.
* @param point1 LatLng type point1

View file

@ -189,4 +189,5 @@
<string name="no_description_found">nun s\'atoparon descripciones</string>
<string name="nearby_info_menu_commons_article">Artículu en Commons</string>
<string name="nearby_info_menu_wikidata_article">Elementu de WikiData</string>
<string name="error_while_cache">Error al poner les fotos na caché</string>
</resources>

View file

@ -42,6 +42,7 @@
<string name="login_failed_password">Ne c\'haller ket kevreañ - Gwiriit ho ker tremen, mar plij</string>
<string name="login_failed_throttled">Re a daolioù-esae. Klaskit en-dro a-benn ur pennadig amzer.</string>
<string name="login_failed_blocked">Hon digarezit, prennet eo bet an implijer-mañ e Commons</string>
<string name="login_failed_2fa_needed">Rankout a rit reiñ ho kod dilesa gant daou faktor.</string>
<string name="login_failed_generic">C\'hwitet eo ar c\'hevreañ</string>
<string name="share_upload_button">Pellgargañ</string>
<string name="multiple_share_base_title">Envel ar rikoù-mañ</string>
@ -129,7 +130,7 @@
<string name="welcome_final_text">Ha soñjal a rit eo mat ?</string>
<string name="welcome_final_button_text">Ya !</string>
<string name="detail_panel_cats_label">Rummadoù</string>
<string name="detail_panel_cats_loading" fuzzy="true">O kargañ…</string>
<string name="detail_panel_cats_loading">O kargañ…</string>
<string name="detail_panel_cats_none">Hini ebet diuzet</string>
<string name="detail_description_empty">Deskrivadur ebet</string>
<string name="detail_license_empty">Aotre-implijout dizanv</string>
@ -155,7 +156,20 @@
<string name="_2fa_code">Kod 2FA</string>
<string name="maximum_limit">Bevenn uc\'hek</string>
<string name="maximum_limit_alert">Ne c\'haller ket diskwel ouzhpenn 500</string>
<string name="login_failed_2fa_not_supported">Ne vez ket embreget an dilesadur gant daou faktor evit ar mare.</string>
<string name="logout_verification">Sur oc\'h e fell deoc\'h digevreañ ?</string>
<string name="commons_logo">Logo Commons</string>
<string name="no_image_found">N\'eus bet kavet skeudenn ebet</string>
<string name="upload_image">Kargañ ur skeudenn</string>
<string name="welcome_image_mount_zao">Menez Zao</string>
<string name="welcome_image_llamas">Lamaed</string>
<string name="welcome_image_rainbow_bridge">Pont Gwareg-ar-glav</string>
<string name="welcome_image_tulip">Tulipez</string>
<string name="welcome_image_no_selfies">Emboltred ebet</string>
<string name="welcome_image_proprietary">Skeudenn brevez</string>
<string name="welcome_image_welcome_wikipedia">Donemat e Wikipedia</string>
<string name="welcome_image_welcome_copyright">Gwirioù oberour donemat</string>
<string name="welcome_image_sydney_opera_house">Ti Opera Sydney</string>
<string name="cancel">Nullañ</string>
<string name="navigation_drawer_open">Digeriñ</string>
<string name="navigation_drawer_close">Serriñ</string>
@ -166,4 +180,9 @@
<string name="navigation_item_settings">Arventennoù</string>
<string name="navigation_item_feedback">Evezhiadennoù</string>
<string name="navigation_item_logout">Digevreañ</string>
<string name="navigation_item_info">Tutorial</string>
<string name="nearby_needs_permissions">Ne c\'haller ket diskwel al lec\'hioù tost ma ne rannit ket ho lec\'hiadur</string>
<string name="no_description_found">N\'eus bet kavet deskrivadur ebet</string>
<string name="nearby_info_menu_commons_article">Pennad Commons</string>
<string name="nearby_info_menu_wikidata_article">Elfenn Wikidata</string>
</resources>

View file

@ -189,4 +189,5 @@
<string name="no_description_found">Keine Beschreibung gefunden</string>
<string name="nearby_info_menu_commons_article">Commons-Artikel</string>
<string name="nearby_info_menu_wikidata_article">Wikidata-Objekt</string>
<string name="error_while_cache">Fehler beim Zwischenspeichern der Bilder</string>
</resources>

View file

@ -184,4 +184,5 @@
<string name="nearby_needs_permissions">אי־אפשר להציג מקומות בסביבה ללא הרשאות מיקום</string>
<string name="no_description_found">לא נמצא תיאור</string>
<string name="nearby_info_menu_wikidata_article">פריט ויקינתונים</string>
<string name="error_while_cache">שגיאה במשירת תמונות במטמון</string>
</resources>

View file

@ -180,4 +180,5 @@
<string name="no_description_found">설명이 없습니다</string>
<string name="nearby_info_menu_commons_article">공용 문서</string>
<string name="nearby_info_menu_wikidata_article">위키데이터 항목</string>
<string name="error_while_cache">그림 캐시 처리 오류</string>
</resources>

View file

@ -189,4 +189,5 @@
<string name="no_description_found">не најдов описи</string>
<string name="nearby_info_menu_commons_article">Статија на Ризницата</string>
<string name="nearby_info_menu_wikidata_article">Предмет на Википодатоците</string>
<string name="error_while_cache">Грешка при меѓускладирање на сликите</string>
</resources>

View file

@ -189,4 +189,5 @@
<string name="no_description_found">gnun-a descrission trovà</string>
<string name="nearby_info_menu_commons_article">Artìcol ëd Comun</string>
<string name="nearby_info_menu_wikidata_article">Element ëd WikiData</string>
<string name="error_while_cache">Eror antramentre ch\'as butavo le plance an memòria local</string>
</resources>

View file

@ -193,4 +193,5 @@
<string name="no_description_found">описание не найдено</string>
<string name="nearby_info_menu_commons_article">Статья на Викискладе</string>
<string name="nearby_info_menu_wikidata_article">Элемент Викиданных</string>
<string name="error_while_cache">Ошибка при кэшировании картинок</string>
</resources>

View file

@ -189,4 +189,5 @@
<string name="no_description_found">teu manggihan pedaran</string>
<string name="nearby_info_menu_commons_article">Artikel Common</string>
<string name="nearby_info_menu_wikidata_article">item Wikidata</string>
<string name="error_while_cache">Kasalahan nalika muat gambar</string>
</resources>

View file

@ -189,4 +189,5 @@
<string name="no_description_found">ingen beskrivning hittades</string>
<string name="nearby_info_menu_commons_article">Commons-artikel</string>
<string name="nearby_info_menu_wikidata_article">Wikidata-objekt</string>
<string name="error_while_cache">Fel uppstod när bilder cachelagras</string>
</resources>

View file

@ -4,21 +4,36 @@
<string name="username">ಸದಸ್ಯೆರ್ನ ಪುದರ್</string>
<string name="password">ಪ್ರವೇಸೊ ಪದೊ</string>
<string name="login">ಲಾಗ್ ಇನ್</string>
<string name="signup">ಖಾತೆ ಸುರು ಮಲ್ಪುಲೆ</string>
<string name="logging_in_title">ಇರ್ ಲಾಗ್-ಇನ್ ಅವೊಂತುಲ್ಲಾರ್</string>
<string name="logging_in_message">ದಯಾದಿತ್ ವಂತೆ ಕಾಪುಲೆ…</string>
<string name="login_success">ಲಾಗಿನ್ ಅಂಡ್!</string>
<string name="login_failed">ಲಾಗಿನ್ ಅಯಿಜಾತ್ತ!</string>
<string name="upload_failed">ಈ ಕಡತ ತಿಕ್ಕಿಜಿ. ದಯಮಲ್ತ್ ಕುಡೊಂಜಿ ಕಡತೊನು ಪ್ರಯತ್ನ ಮಲ್ಪುಲೆ.</string>
<string name="authentication_failed">ದೃಢೀಕರಣ ಸರಿ ಆಯಿಜಿ!</string>
<string name="uploading_started">ದಿಂಜಪುನಾ ಸುರು ಅಂಡ್!</string>
<string name="upload_completed_notification_title">%1$s ಅಪ್ಲೋಡ್ ಆಂಡ್!</string>
<string name="upload_completed_notification_text">ಇರೆನ ಅಪ್ಲೋಡ್ ತೂಯೆರೆ ಒತ್ತುಲೆ</string>
<string name="upload_progress_notification_title_start">%1$s ಅಪ್ಲೋಡ್ ಸುರು ಆವೊಂದುಂಡು</string>
<string name="upload_progress_notification_title_in_progress">%1$s ಅಪ್ಲೋಡ್ ಆವೊಂದುಂಡು</string>
<string name="upload_progress_notification_title_finishing">%1$s ಅಪ್ಲೋಡ್ ಕೈದ್ ಆವೊಂದುಂಡು.</string>
<string name="upload_failed_notification_title">%1$s ಅಪ್ಲೋಡ್ ಸರಿ ಆತಿಜಿ</string>
<string name="upload_failed_notification_subtitle">ತುಯಾರ ಮೆಲ್ಲ ಒತ್ತುಲೆ</string>
<plurals name="uploads_pending_notification_indicator">
<item quantity="one">%d ಕಡತ ಅಪ್ಲೊಡ್ ಆವೊಂದುಂಡು</item>
<item quantity="other">%d ಕಡತೊಲು ಅಪ್ಲೋಡ್ ಆವೊಂದುಂಡು</item>
</plurals>
<string name="title_activity_contributions" fuzzy="true">ಎನ್ನ ದಿಂಜಯೀನಾ ವಿಚಾರೊಳು</string>
<string name="contribution_state_queued">ದಿಂಜೊಂತುಂಡು</string>
<string name="contribution_state_failed">ದಿಂಜಿಜಿ</string>
<string name="contribution_state_in_progress">%1$d%% ಮುಗಿಂಡ್</string>
<string name="contribution_state_starting">ದಿಂಜೊಂತುಂಡು…</string>
<string name="menu_from_gallery">ಛಾಯಾಂಕಣತ್</string>
<string name="menu_from_camera">ಪಟ ದೆಪ್ಪುಲೇ</string>
<string name="menu_nearby">ಕೈತಲ್‍ದ</string>
<string name="provider_contributions">ಎನ್ನ ದಿಂಜಯೀನಾ ವಿಚಾರೊಲು</string>
<string name="menu_share">ಪಟ್ಟುಲೆ</string>
<string name="menu_open_in_browser">ಬ್ರೌಸರ್‌ಡ್ ತೂಲೆ</string>
<string name="share_title_hint">ತರೆಬರವು</string>
<string name="share_description_hint">ವಿವರಣೆ</string>
<string name="login_failed_generic">ಲಾಗಿನ್ ಆಯಾರಾ ಅಯಿಜಿ</string>

View file

@ -189,4 +189,5 @@
<string name="no_description_found">找不到描述</string>
<string name="nearby_info_menu_commons_article">共享資源條目</string>
<string name="nearby_info_menu_wikidata_article">維基數據項目</string>
<string name="error_while_cache">在快取圖片時發生錯誤</string>
</resources>

View file

@ -189,4 +189,5 @@
<string name="no_description_found">找不到描述</string>
<string name="nearby_info_menu_commons_article">共享资源条目</string>
<string name="nearby_info_menu_wikidata_article">维基数据项</string>
<string name="error_while_cache">缓存图片时出错</string>
</resources>

View file

@ -79,7 +79,8 @@ Tap this message (or hit back) to skip this step.</string>
<string name="title_activity_settings">Settings</string>
<string name="title_activity_signup">Sign Up</string>
<string name="menu_about">About</string>
<string name="about_license">Open Source software released under the &lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\"&gt;Apache License v2&lt;/a&gt;. Wikimedia Commons and its logo are trademarks of the Wikimedia Foundation and are used with the permission of the Wikimedia Foundation. We are not endorsed by or affiliated with the Wikimedia Foundation.</string>
<string name="about_license">Open Source software released under the &lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\"&gt;Apache License v2&lt;/a&gt;. %1$s and its logo are trademarks of the Wikimedia Foundation and are used with the permission of the Wikimedia Foundation. We are not endorsed by or affiliated with the Wikimedia Foundation.</string>
<string name="trademarked_name" translatable="false">Wikimedia Commons</string>
<string name="about_improve">&lt;a href=\"https://github.com/commons-app/apps-android-commons\"&gt;Source&lt;/a&gt; and &lt;a href=\"https://commons-app.github.io/\"&gt;website&lt;/a&gt; on GitHub. Create a new &lt;a href=\"https://github.com/commons-app/apps-android-commons/issues\"&gt;GitHub issue&lt;/a&gt; for bug reports and suggestions.</string>
<string name="about_privacy_policy">&lt;a href=\"https://wikimediafoundation.org/wiki/Privacy_policy\"&gt;Privacy policy&lt;/a&gt;</string>
<string name="about_credits">&lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\"&gt;Credits&lt;/a&gt;</string>

View file

@ -1,12 +1,12 @@
package fr.free.nrw.commons;
import static org.hamcrest.CoreMatchers.is;
import fr.free.nrw.commons.location.LatLng;
import org.junit.Assert;
import org.junit.Test;
import fr.free.nrw.commons.location.LatLng;
import static org.hamcrest.CoreMatchers.is;
public class LatLngTests {
@Test public void testZeroZero() {
LatLng place = new LatLng(0, 0, 0);

View file

@ -1,12 +1,12 @@
package fr.free.nrw.commons;
import static org.hamcrest.CoreMatchers.is;
import org.junit.Assert;
import org.junit.Test;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.LengthUtils;
import org.junit.Assert;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
public class LengthUtilsTest {
@Test public void testZeroDistance() {

View file

@ -1,10 +1,10 @@
package fr.free.nrw.commons;
import static org.hamcrest.CoreMatchers.is;
import org.junit.Assert;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
public class UtilsFixExtensionTest {
@Test public void jpegResultsInJpg() {

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

@ -22,10 +22,11 @@ import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class NearbyAdapterFactoryTest {
private static final Place PLACE = new Place("name", Place.Description.AIRPORT,

View file

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

View file

@ -1,17 +1,17 @@
gradleVersion = 2.3.0
supportLibVersion = 25.2.0
supportLibVersion = 25.3.1
compileSdkVersion = android-25
buildToolsVersion = 25.0.1
minSdkVersion = 15
# Cannot target API 24+ until https://github.com/commons-app/apps-android-commons/issues/457 fixed
targetSdkVersion = 23
targetSdkVersion = 25
android.useDeprecatedNdk=true
# Library dependencies
BUTTERKNIFE_VERSION=8.4.0
BUTTERKNIFE_VERSION=8.6.0
GUAVA_VERSION=19.0
org.gradle.jvmargs=-Xmx1536M