diff --git a/app/build.gradle b/app/build.gradle
index c05f4861f..bc9833416 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,7 +1,6 @@
plugins {
id 'com.github.triplet.play' version '2.2.1' apply false
}
-
apply from: '../gitutils.gradle'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
@@ -31,8 +30,8 @@ dependencies {
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1'
implementation 'com.facebook.fresco:fresco:1.13.0'
implementation 'com.drewnoakes:metadata-extractor:2.11.0'
- implementation 'com.dmitrybrant:wikimedia-android-data-client:0.0.18'
implementation 'org.apache.commons:commons-lang3:3.8.1'
+ implementation 'com.dmitrybrant:wikimedia-android-data-client:0.0.25'
// UI
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
@@ -195,6 +194,7 @@ android {
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.org\""
+ buildConfigField "String", "WIKIDATA_URL", "\"https://wikidata.org\""
buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\""
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
@@ -226,6 +226,7 @@ android {
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\""
+ buildConfigField "String", "WIKIDATA_URL", "\"https://wikidata.org\""
buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\""
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b2e434bd4..ad88b720c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -152,7 +152,8 @@
+ android:exported="true"
+ android:process=":sync">
@@ -161,17 +162,6 @@
android:name="android.content.SyncAdapter"
android:resource="@xml/contributions_sync_adapter" />
-
-
-
-
-
-
-
-
+
CREATOR = new Creator() {
@Override
public Media createFromParcel(Parcel parcel) {
@@ -156,9 +158,9 @@ public class Media implements Parcelable {
page.title(),
"",
0,
- safeParseDate(metadata.dateTimeOriginal().value()),
- safeParseDate(metadata.dateTime().value()),
- StringUtil.fromHtml(metadata.artist().value()).toString()
+ safeParseDate(metadata.dateTime()),
+ safeParseDate(metadata.dateTime()),
+ StringUtil.fromHtml(metadata.artist()).toString()
);
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
@@ -170,17 +172,17 @@ public class Media implements Parcelable {
language = "default";
}
- media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription().value()));
- media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories().value()));
- String latitude = metadata.gpsLatitude().value();
- String longitude = metadata.gpsLongitude().value();
+ media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription()));
+ media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories()));
+ String latitude = metadata.getGpsLatitude();
+ String longitude = metadata.getGpsLongitude();
if (!StringUtils.isBlank(latitude) && !StringUtils.isBlank(longitude)) {
LatLng latLng = new LatLng(Double.parseDouble(latitude), Double.parseDouble(longitude), 0);
media.setCoordinates(latLng);
}
- media.setLicenseInformation(metadata.licenseShortName().value(), metadata.licenseUrl().value());
+ media.setLicenseInformation(metadata.licenseShortName(), metadata.licenseUrl());
return media;
}
diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java
index 4779e0455..c2e301826 100644
--- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java
+++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java
@@ -1,9 +1,11 @@
package fr.free.nrw.commons;
+import androidx.core.text.HtmlCompat;
+
import javax.inject.Inject;
import javax.inject.Singleton;
-import androidx.core.text.HtmlCompat;
+import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.Single;
@@ -19,12 +21,15 @@ import timber.log.Timber;
public class MediaDataExtractor {
private final MediaWikiApi mediaWikiApi;
private final OkHttpJsonApiClient okHttpJsonApiClient;
+ private final MediaClient mediaClient;
@Inject
public MediaDataExtractor(MediaWikiApi mwApi,
- OkHttpJsonApiClient okHttpJsonApiClient) {
+ OkHttpJsonApiClient okHttpJsonApiClient,
+ MediaClient mediaClient) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.mediaWikiApi = mwApi;
+ this.mediaClient = mediaClient;
}
/**
@@ -35,7 +40,7 @@ public class MediaDataExtractor {
*/
public Single fetchMediaDetails(String filename) {
Single mediaSingle = getMediaFromFileName(filename);
- Single pageExistsSingle = mediaWikiApi.pageExists("Commons:Deletion_requests/" + filename);
+ Single pageExistsSingle = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + filename);
Single discussionSingle = getDiscussion(filename);
return Single.zip(mediaSingle, pageExistsSingle, discussionSingle, (media, deletionStatus, discussion) -> {
media.setDiscussion(discussion);
@@ -52,7 +57,7 @@ public class MediaDataExtractor {
* @return return data rich Media object
*/
public Single getMediaFromFileName(String filename) {
- return okHttpJsonApiClient.getMedia(filename, false);
+ return mediaClient.getMedia(filename);
}
/**
diff --git a/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.java b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.java
new file mode 100644
index 000000000..e2adfb5e6
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.java
@@ -0,0 +1,58 @@
+package fr.free.nrw.commons.actions;
+
+import org.wikipedia.csrf.CsrfTokenClient;
+import org.wikipedia.dataclient.Service;
+
+import io.reactivex.Observable;
+import timber.log.Timber;
+
+public class PageEditClient {
+
+ private final CsrfTokenClient csrfTokenClient;
+ private final PageEditInterface pageEditInterface;
+ private final Service service;
+
+ public PageEditClient(CsrfTokenClient csrfTokenClient,
+ PageEditInterface pageEditInterface,
+ Service service) {
+ this.csrfTokenClient = csrfTokenClient;
+ this.pageEditInterface = pageEditInterface;
+ this.service = service;
+ }
+
+ public Observable edit(String pageTitle, String text, String summary) {
+ try {
+ return pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking())
+ .map(editResponse -> editResponse.edit().editSucceeded());
+ } catch (Throwable throwable) {
+ return Observable.just(false);
+ }
+ }
+
+ public Observable appendEdit(String pageTitle, String appendText, String summary) {
+ try {
+ return pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking())
+ .map(editResponse -> editResponse.edit().editSucceeded());
+ } catch (Throwable throwable) {
+ return Observable.just(false);
+ }
+ }
+
+ public Observable prependEdit(String pageTitle, String prependText, String summary) {
+ try {
+ return pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking())
+ .map(editResponse -> editResponse.edit().editSucceeded());
+ } catch (Throwable throwable) {
+ return Observable.just(false);
+ }
+ }
+
+ public Observable addEditTag(long revisionId, String tagName, String reason) {
+ try {
+ return service.addEditTag(String.valueOf(revisionId), tagName, reason, csrfTokenClient.getTokenBlocking())
+ .map(mwPostResponse -> mwPostResponse.getSuccessVal());
+ } catch (Throwable throwable) {
+ return Observable.just(-1);
+ }
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.java b/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.java
new file mode 100644
index 000000000..537ec4d4f
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.java
@@ -0,0 +1,41 @@
+package fr.free.nrw.commons.actions;
+
+import androidx.annotation.NonNull;
+
+import org.wikipedia.edit.Edit;
+
+import io.reactivex.Observable;
+import retrofit2.http.Field;
+import retrofit2.http.FormUrlEncoded;
+import retrofit2.http.Headers;
+import retrofit2.http.POST;
+
+import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
+
+public interface PageEditInterface {
+
+ @FormUrlEncoded
+ @Headers("Cache-Control: no-cache")
+ @POST(MW_API_PREFIX + "action=edit")
+ @NonNull
+ Observable postEdit(@NonNull @Field("title") String title,
+ @NonNull @Field("summary") String summary,
+ @NonNull @Field("text") String text,
+ @NonNull @Field("token") String token);
+
+ @FormUrlEncoded
+ @Headers("Cache-Control: no-cache")
+ @POST(MW_API_PREFIX + "action=edit")
+ @NonNull Observable postAppendEdit(@NonNull @Field("title") String title,
+ @NonNull @Field("summary") String summary,
+ @NonNull @Field("appendtext") String text,
+ @NonNull @Field("token") String token);
+
+ @FormUrlEncoded
+ @Headers("Cache-Control: no-cache")
+ @POST(MW_API_PREFIX + "action=edit")
+ @NonNull Observable postPrependEdit(@NonNull @Field("title") String title,
+ @NonNull @Field("summary") String summary,
+ @NonNull @Field("prependtext") String text,
+ @NonNull @Field("token") String token);
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.java b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.java
new file mode 100644
index 000000000..c4f96e7eb
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.java
@@ -0,0 +1,36 @@
+package fr.free.nrw.commons.actions;
+
+import org.wikipedia.csrf.CsrfTokenClient;
+import org.wikipedia.dataclient.Service;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import fr.free.nrw.commons.CommonsApplication;
+import io.reactivex.Observable;
+
+@Singleton
+public class ThanksClient {
+
+ private final CsrfTokenClient csrfTokenClient;
+ private final Service service;
+
+ @Inject
+ public ThanksClient(@Named("commons-csrf") CsrfTokenClient csrfTokenClient,
+ @Named("commons-service") Service service) {
+ this.csrfTokenClient = csrfTokenClient;
+ this.service = service;
+ }
+
+ public Observable thank(long revisionId) {
+ try {
+ return service.thank(String.valueOf(revisionId), null,
+ csrfTokenClient.getTokenBlocking(),
+ CommonsApplication.getInstance().getUserAgent())
+ .map(mwQueryResponse -> mwQueryResponse.getSuccessVal() == 1);
+ } catch (Throwable throwable) {
+ return Observable.just(false);
+ }
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java
index 65e211c83..7616b4515 100644
--- a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java
+++ b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java
@@ -3,8 +3,8 @@ package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
-import androidx.annotation.Nullable;
+import androidx.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig;
import timber.log.Timber;
@@ -12,10 +12,8 @@ public class AccountUtil {
public static final String AUTH_COOKIE = "authCookie";
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
- private final Context context;
- public AccountUtil(Context context) {
- this.context = context;
+ public AccountUtil() {
}
/**
@@ -49,5 +47,4 @@ public class AccountUtil {
private static AccountManager accountManager(Context context) {
return AccountManager.get(context);
}
-
}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java
index 3bb1ab9a1..426a84630 100644
--- a/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java
@@ -12,48 +12,19 @@ import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
-import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
-
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
@Inject
protected SessionManager sessionManager;
@Inject
MediaWikiApi mediaWikiApi;
- private String authCookie;
-
- protected void requestAuthToken() {
- if (authCookie != null) {
- onAuthCookieAcquired(authCookie);
- return;
- }
- authCookie = sessionManager.getAuthCookie();
- if (authCookie != null) {
- onAuthCookieAcquired(authCookie);
- }
- }
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
-
- if (savedInstanceState != null) {
- authCookie = savedInstanceState.getString(AUTH_COOKIE);
- }
-
showBlockStatus();
}
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putString(AUTH_COOKIE, authCookie);
- }
-
- protected abstract void onAuthCookieAcquired(String authCookie);
-
- protected abstract void onAuthFailure();
-
/**
* Makes API call to check if user is blocked from Commons. If the user is blocked, a snackbar
* is created to notify the user
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java
index 8487e9a95..6915d075a 100644
--- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java
@@ -1,9 +1,6 @@
package fr.free.nrw.commons.auth;
-import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity;
-import android.accounts.AccountAuthenticatorResponse;
-import android.accounts.AccountManager;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
@@ -20,14 +17,6 @@ import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
-import com.google.android.material.textfield.TextInputLayout;
-
-import java.io.IOException;
-import java.util.Locale;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
@@ -35,6 +24,17 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.app.NavUtils;
import androidx.core.content.ContextCompat;
+
+import com.google.android.material.textfield.TextInputLayout;
+
+import org.wikipedia.AppAdapter;
+import org.wikipedia.dataclient.WikiSite;
+import org.wikipedia.login.LoginClient;
+import org.wikipedia.login.LoginResult;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
@@ -52,16 +52,17 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.ViewUtil;
-import io.reactivex.Observable;
+import io.reactivex.Completable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.functions.Action;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
-import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
+import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_WIKI_SITE;
public class LoginActivity extends AccountAuthenticatorActivity {
@@ -71,10 +72,17 @@ public class LoginActivity extends AccountAuthenticatorActivity {
@Inject
SessionManager sessionManager;
+ @Inject
+ @Named(NAMED_COMMONS_WIKI_SITE)
+ WikiSite commonsWikiSite;
+
@Inject
@Named("default_preferences")
JsonKvStore applicationKvStore;
+ @Inject
+ LoginClient loginClient;
+
@BindView(R.id.login_button)
Button loginButton;
@@ -104,13 +112,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
private LoginTextWatcher textWatcher = new LoginTextWatcher();
private CompositeDisposable compositeDisposable = new CompositeDisposable();
- private Boolean loginCurrentlyInProgress = false;
- private Boolean errorMessageShown = false;
- private String resultantError;
- private static final String RESULTANT_ERROR = "resultantError";
- private static final String ERROR_MESSAGE_SHOWN = "errorMessageShown";
- private static final String LOGGING_IN = "loggingIn";
-
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -211,10 +212,8 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
if (sessionManager.getCurrentAccount() != null
- && sessionManager.isUserLoggedIn()
- && sessionManager.getCachedAuthCookie() != null) {
+ && sessionManager.isUserLoggedIn()) {
applicationKvStore.putBoolean("login_skipped", false);
- sessionManager.revalidateAuthToken();
startMainActivity();
}
@@ -244,7 +243,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
@OnClick(R.id.login_button)
public void performLogin() {
- loginCurrentlyInProgress = true;
Timber.d("Login to start!");
final String username = usernameEdit.getText().toString();
final String rawUsername = usernameEdit.getText().toString().trim();
@@ -252,23 +250,37 @@ public class LoginActivity extends AccountAuthenticatorActivity {
String twoFactorCode = twoFactorEdit.getText().toString();
showLoggingProgressBar();
- compositeDisposable.add(Observable.fromCallable(() -> login(username, password, twoFactorCode))
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(result -> handleLogin(username, rawUsername, password, result)));
+ doLogin(username, password, twoFactorCode);
}
- private String login(String username, String password, String twoFactorCode) {
- try {
- if (twoFactorCode.isEmpty()) {
- return mwApi.login(username, password);
- } else {
- return mwApi.login(username, password, twoFactorCode);
+ private void doLogin(String username, String password, String twoFactorCode) {
+ progressDialog.show();
+
+ Action action = () -> {
+ try {
+ loginClient.loginBlocking(commonsWikiSite, username, password, twoFactorCode);
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
}
- } catch (IOException e) {
- // Do something better!
- return "NetworkFailure";
- }
+ };
+
+ compositeDisposable.add(Completable.fromAction(action)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> onLoginSuccess(username, password),
+ error -> {
+ if (error instanceof LoginClient.LoginFailedException) {
+ LoginClient.LoginFailedException exception = (LoginClient.LoginFailedException) error;
+ if (exception.getMessage().equals("2FA")) {
+ askUserForTwoFactorAuth();
+ }
+ }
+ if (!progressDialog.isShowing()) {
+ return;
+ }
+ progressDialog.dismiss();
+ showMessageAndCancelDialog(R.string.error_occurred);
+ }));
}
/**
@@ -281,18 +293,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
finish();
}
- private void handleLogin(String username, String rawUsername, String password, String result) {
- Timber.d("Login done!");
- if (result.equals("PASS")) {
- handlePassResult(username, rawUsername, password);
- } else {
- loginCurrentlyInProgress = false;
- errorMessageShown = true;
- resultantError = result;
- handleOtherResults(result);
- }
- }
-
private void showLoggingProgressBar() {
progressDialog = new ProgressDialog(this);
progressDialog.setIndeterminate(true);
@@ -302,67 +302,19 @@ public class LoginActivity extends AccountAuthenticatorActivity {
progressDialog.show();
}
- private void handlePassResult(String username, String rawUsername, String password) {
+ private void onLoginSuccess(String username, String password) {
+ if (!progressDialog.isShowing()) {
+ // no longer attached to activity!
+ return;
+ }
+ sessionManager.setUserLoggedIn(true);
+ LoginResult loginResult = new LoginResult(commonsWikiSite, "PASS", username, password, "");
+ AppAdapter.get().updateAccount(loginResult);
+ progressDialog.dismiss();
showSuccessAndDismissDialog();
- requestAuthToken();
- AccountAuthenticatorResponse response = null;
-
- Bundle extras = getIntent().getExtras();
- if (extras != null) {
- Timber.d("Bundle of extras: %s", extras);
- response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
- if (response != null) {
- Bundle authResult = new Bundle();
- authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username);
- authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, BuildConfig.ACCOUNT_TYPE);
- response.onResult(authResult);
- }
- }
-
- sessionManager.createAccount(response, username, rawUsername, password);
startMainActivity();
}
- protected void requestAuthToken() {
- AccountManager accountManager = AccountManager.get(this);
- Account curAccount = sessionManager.getCurrentAccount();
- if (curAccount != null) {
- accountManager.setAuthToken(curAccount, AUTH_TOKEN_TYPE, mwApi.getAuthCookie());
- }
- }
-
- /**
- * Match known failure message codes and provide messages.
- *
- * @param result String
- */
- private void handleOtherResults(String result) {
- if (result.equals("NetworkFailure")) {
- // Matches NetworkFailure which is created by the doInBackground method
- showMessageAndCancelDialog(R.string.login_failed_network);
- } else if (result.toLowerCase(Locale.getDefault()).contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
- // Matches nosuchuser, nosuchusershort, noname
- showMessageAndCancelDialog(R.string.login_failed_wrong_credentials);
- emptySensitiveEditFields();
- } else if (result.toLowerCase(Locale.getDefault()).contains("wrongpassword".toLowerCase())) {
- // Matches wrongpassword, wrongpasswordempty
- showMessageAndCancelDialog(R.string.login_failed_wrong_credentials);
- emptySensitiveEditFields();
- } else if (result.toLowerCase(Locale.getDefault()).contains("throttle".toLowerCase())) {
- // Matches unknown throttle error codes
- showMessageAndCancelDialog(R.string.login_failed_throttled);
- } else if (result.toLowerCase(Locale.getDefault()).contains("userblocked".toLowerCase())) {
- // Matches login-userblocked
- showMessageAndCancelDialog(R.string.login_failed_blocked);
- } else if (result.equals("2FA")) {
- askUserForTwoFactorAuth();
- } else {
- // Occurs with unhandled login failure codes
- Timber.d("Login failed with reason: %s", result);
- showMessageAndCancelDialog(R.string.login_failed_generic);
- }
- }
-
@Override
protected void onStart() {
super.onStart();
@@ -402,30 +354,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
return getDelegate().getMenuInflater();
}
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putBoolean(LOGGING_IN, loginCurrentlyInProgress);
- outState.putBoolean(ERROR_MESSAGE_SHOWN, errorMessageShown);
- outState.putString(RESULTANT_ERROR, resultantError);
- }
-
- @Override
- protected void onRestoreInstanceState(Bundle savedInstanceState) {
- super.onRestoreInstanceState(savedInstanceState);
- loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGGING_IN, false);
- errorMessageShown = savedInstanceState.getBoolean(ERROR_MESSAGE_SHOWN, false);
- if (loginCurrentlyInProgress) {
- performLogin();
- }
- if (errorMessageShown) {
- resultantError = savedInstanceState.getString(RESULTANT_ERROR);
- if (resultantError != null) {
- handleOtherResults(resultantError);
- }
- }
- }
-
public void askUserForTwoFactorAuth() {
progressDialog.dismiss();
twoFactorContainer.setVisibility(VISIBLE);
@@ -440,16 +368,18 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
}
+ public void showMessageAndCancelDialog(String error) {
+ showMessage(error, R.color.secondaryDarkColor);
+ if (progressDialog != null) {
+ progressDialog.cancel();
+ }
+ }
+
public void showSuccessAndDismissDialog() {
showMessage(R.string.login_success, R.color.primaryDarkColor);
progressDialog.dismiss();
}
- public void emptySensitiveEditFields() {
- passwordEdit.setText("");
- twoFactorEdit.setText("");
- }
-
public void startMainActivity() {
NavigationBaseActivity.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP);
finish();
@@ -461,6 +391,12 @@ public class LoginActivity extends AccountAuthenticatorActivity {
errorMessageContainer.setVisibility(VISIBLE);
}
+ private void showMessage(String message, @ColorRes int colorResId) {
+ errorMessage.setText(message);
+ errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
+ errorMessageContainer.setVisibility(VISIBLE);
+ }
+
private AppCompatDelegate getDelegate() {
if (delegate == null) {
delegate = AppCompatDelegate.create(this, null);
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java
index 7462869b1..04519e128 100644
--- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java
+++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java
@@ -1,14 +1,16 @@
package fr.free.nrw.commons.auth;
import android.accounts.Account;
-import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
-import android.content.ContentResolver;
import android.content.Context;
-import android.os.Bundle;
+import android.os.Build;
+import android.text.TextUtils;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import org.wikipedia.login.LoginResult;
+
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
@@ -20,10 +22,6 @@ import io.reactivex.Completable;
import io.reactivex.Observable;
import timber.log.Timber;
-import static android.accounts.AccountManager.ERROR_CODE_REMOTE_EXCEPTION;
-import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
-import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
-
/**
* Manage the current logged in user session.
*/
@@ -34,7 +32,6 @@ public class SessionManager {
private Account currentAccount; // Unlike a savings account... ;-)
private JsonKvStore defaultKvStore;
private static final String KEY_RAWUSERNAME = "rawusername";
- private Bundle userdata = new Bundle();
@Inject
public SessionManager(Context context,
@@ -46,43 +43,40 @@ public class SessionManager {
this.defaultKvStore = defaultKvStore;
}
- /**
- * Creata a new account
- *
- * @param response
- * @param username
- * @param rawusername
- * @param password
- */
- public void createAccount(@Nullable AccountAuthenticatorResponse response,
- String username, String rawusername, String password) {
-
- Account account = new Account(username, BuildConfig.ACCOUNT_TYPE);
- userdata.putString(KEY_RAWUSERNAME, rawusername);
- boolean created = accountManager().addAccountExplicitly(account, password, userdata);
-
- Timber.d("account creation " + (created ? "successful" : "failure"));
-
- if (created) {
- if (response != null) {
- Bundle bundle = new Bundle();
- bundle.putString(KEY_ACCOUNT_NAME, username);
- bundle.putString(KEY_ACCOUNT_TYPE, BuildConfig.ACCOUNT_TYPE);
-
-
- response.onResult(bundle);
- }
-
- } else {
- if (response != null) {
- response.onError(ERROR_CODE_REMOTE_EXCEPTION, "");
- }
- Timber.d("account creation failure");
+ private boolean createAccount(@NonNull String userName, @NonNull String password) {
+ Account account = getCurrentAccount();
+ if (account == null || TextUtils.isEmpty(account.name) || !account.name.equals(userName)) {
+ removeAccount();
+ account = new Account(userName, BuildConfig.ACCOUNT_TYPE);
+ return accountManager().addAccountExplicitly(account, password, null);
}
+ return true;
+ }
- // FIXME: If the user turns it off, it shouldn't be auto turned back on
- ContentResolver.setSyncAutomatically(account, BuildConfig.CONTRIBUTION_AUTHORITY, true); // Enable sync by default!
- ContentResolver.setSyncAutomatically(account, BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default!
+ private void removeAccount() {
+ Account account = getCurrentAccount();
+ if (account != null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
+ accountManager().removeAccountExplicitly(account);
+ } else {
+ //noinspection deprecation
+ accountManager().removeAccount(account, null, null);
+ }
+ }
+ }
+
+ public void updateAccount(LoginResult result) {
+ boolean accountCreated = createAccount(result.getUserName(), result.getPassword());
+ if (accountCreated) {
+ setPassword(result.getPassword());
+ }
+ }
+
+ private void setPassword(@NonNull String password) {
+ Account account = getCurrentAccount();
+ if (account != null) {
+ accountManager().setPassword(account, password);
+ }
}
/**
@@ -107,7 +101,7 @@ public class SessionManager {
}
@Nullable
- public String getRawUserName() {
+ private String getRawUserName() {
Account account = getCurrentAccount();
return account == null ? null : accountManager().getUserData(account, KEY_RAWUSERNAME);
}
@@ -127,46 +121,14 @@ public class SessionManager {
return AccountManager.get(context);
}
- public Boolean revalidateAuthToken() {
- AccountManager accountManager = AccountManager.get(context);
- Account curAccount = getCurrentAccount();
-
- if (curAccount == null) {
- return false; // This should never happen
- }
-
- accountManager.invalidateAuthToken(BuildConfig.ACCOUNT_TYPE, null);
- String authCookie = getAuthCookie();
-
- if (authCookie == null) {
- return false;
- }
-
- mediaWikiApi.setAuthCookie(authCookie);
- return true;
- }
-
- public String getAuthCookie() {
- if (!isUserLoggedIn()) {
- Timber.e("User is not logged in");
- return null;
- } else {
- String authCookie = getCachedAuthCookie();
- if (authCookie == null) {
- Timber.e("Auth cookie is null even after login");
- }
- return authCookie;
- }
- }
-
- public String getCachedAuthCookie() {
- return defaultKvStore.getString("getAuthCookie", null);
- }
-
public boolean isUserLoggedIn() {
return defaultKvStore.getBoolean("isUserLoggedIn", false);
}
+ void setUserLoggedIn(boolean isLoggedIn) {
+ defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn);
+ }
+
public void forceLogin(Context context) {
if (context != null) {
LoginActivity.startYourself(context);
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java
index be6ffb7f5..695da9cfd 100644
--- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java
+++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java
@@ -10,6 +10,7 @@ import javax.inject.Singleton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.bookmarks.Bookmark;
+import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
@@ -19,15 +20,14 @@ import io.reactivex.functions.Function;
@Singleton
public class BookmarkPicturesController {
- private final OkHttpJsonApiClient okHttpJsonApiClient;
+ private final MediaClient mediaClient;
private final BookmarkPicturesDao bookmarkDao;
private List currentBookmarks;
@Inject
- public BookmarkPicturesController(OkHttpJsonApiClient okHttpJsonApiClient,
- BookmarkPicturesDao bookmarkDao) {
- this.okHttpJsonApiClient = okHttpJsonApiClient;
+ public BookmarkPicturesController(MediaClient mediaClient, BookmarkPicturesDao bookmarkDao) {
+ this.mediaClient = mediaClient;
this.bookmarkDao = bookmarkDao;
currentBookmarks = new ArrayList<>();
}
@@ -47,7 +47,7 @@ public class BookmarkPicturesController {
private Observable getMediaFromBookmark(Bookmark bookmark) {
Media dummyMedia = new Media("");
- return okHttpJsonApiClient.getMedia(bookmark.getMediaName(), false)
+ return mediaClient.getMedia(bookmark.getMediaName())
.map(media -> media == null ? dummyMedia : media)
.onErrorReturn(throwable -> dummyMedia)
.toObservable();
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java
index 21c39bf97..5e62e000c 100644
--- a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java
@@ -23,6 +23,7 @@ public class CategoriesModel{
private static final int SEARCH_CATS_LIMIT = 25;
private final MediaWikiApi mwApi;
+ private final CategoryClient categoryClient;
private final CategoryDao categoryDao;
private final JsonKvStore directKvStore;
@@ -32,9 +33,11 @@ public class CategoriesModel{
@Inject GpsCategoryModel gpsCategoryModel;
@Inject
public CategoriesModel(MediaWikiApi mwApi,
+ CategoryClient categoryClient,
CategoryDao categoryDao,
@Named("default_preferences") JsonKvStore directKvStore) {
this.mwApi = mwApi;
+ this.categoryClient = categoryClient;
this.categoryDao = categoryDao;
this.directKvStore = directKvStore;
this.categoriesCache = new HashMap<>();
@@ -134,8 +137,8 @@ public class CategoriesModel{
}
//otherwise, search API for matching categories
- return mwApi
- .allCategories(term, SEARCH_CATS_LIMIT)
+ return categoryClient
+ .searchCategoriesForPrefix(term, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
@@ -198,7 +201,7 @@ public class CategoriesModel{
* @return
*/
private Observable getTitleCategories(String title) {
- return mwApi.searchTitles(title, SEARCH_CATS_LIMIT)
+ return categoryClient.searchCategories(title, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.java
new file mode 100644
index 000000000..329c3635a
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.java
@@ -0,0 +1,125 @@
+package fr.free.nrw.commons.category;
+
+
+import androidx.annotation.NonNull;
+
+import org.wikipedia.dataclient.mwapi.MwQueryPage;
+import org.wikipedia.dataclient.mwapi.MwQueryResponse;
+import org.wikipedia.dataclient.mwapi.MwQueryResult;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import io.reactivex.Observable;
+import timber.log.Timber;
+
+/**
+ * Category Client to handle custom calls to Commons MediaWiki APIs
+ */
+@Singleton
+public class CategoryClient {
+
+ private final CategoryInterface CategoryInterface;
+
+ @Inject
+ public CategoryClient(CategoryInterface CategoryInterface) {
+ this.CategoryInterface = CategoryInterface;
+ }
+
+ /**
+ * Searches for categories containing the specified string.
+ *
+ * @param filter The string to be searched
+ * @param itemLimit How many results are returned
+ * @param offset Starts returning items from the nth result. If offset is 9, the response starts with the 9th item of the search result
+ * @return
+ */
+ public Observable searchCategories(String filter, int itemLimit, int offset) {
+ return responseToCategoryName(CategoryInterface.searchCategories(filter, itemLimit, offset));
+
+ }
+
+ /**
+ * Searches for categories containing the specified string.
+ *
+ * @param filter The string to be searched
+ * @param itemLimit How many results are returned
+ * @return
+ */
+ public Observable searchCategories(String filter, int itemLimit) {
+ return searchCategories(filter, itemLimit, 0);
+
+ }
+
+ /**
+ * Searches for categories starting with the specified string.
+ *
+ * @param prefix The prefix to be searched
+ * @param itemLimit How many results are returned
+ * @param offset Starts returning items from the nth result. If offset is 9, the response starts with the 9th item of the search result
+ * @return
+ */
+ public Observable searchCategoriesForPrefix(String prefix, int itemLimit, int offset) {
+ return responseToCategoryName(CategoryInterface.searchCategoriesForPrefix(prefix, itemLimit, offset));
+ }
+
+ /**
+ * Searches for categories starting with the specified string.
+ *
+ * @param prefix The prefix to be searched
+ * @param itemLimit How many results are returned
+ * @return
+ */
+ public Observable searchCategoriesForPrefix(String prefix, int itemLimit) {
+ return searchCategoriesForPrefix(prefix, itemLimit, 0);
+ }
+
+
+ /**
+ * The method takes categoryName as input and returns a List of Subcategories
+ * It uses the generator query API to get the subcategories in a category, 500 at a time.
+ *
+ * @param categoryName Category name as defined on commons
+ * @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
+ */
+ public Observable getSubCategoryList(String categoryName) {
+ return responseToCategoryName(CategoryInterface.getSubCategoryList(categoryName));
+ }
+
+ /**
+ * The method takes categoryName as input and returns a List of parent categories
+ * It uses the generator query API to get the parent categories of a category, 500 at a time.
+ *
+ * @param categoryName Category name as defined on commons
+ * @return
+ */
+ @NonNull
+ public Observable getParentCategoryList(String categoryName) {
+ return responseToCategoryName(CategoryInterface.getParentCategoryList(categoryName));
+ }
+
+ /**
+ * Internal function to reduce code reuse. Extracts the categories returned from MwQueryResponse.
+ *
+ * @param responseObservable The query response observable
+ * @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
+ */
+ private Observable responseToCategoryName(Observable responseObservable) {
+ return responseObservable
+ .flatMap(mwQueryResponse -> {
+ MwQueryResult query;
+ List pages;
+ if ((query = mwQueryResponse.query()) == null ||
+ (pages = query.pages()) == null) {
+ Timber.d("No categories returned.");
+ return Observable.empty();
+ } else
+ return Observable.fromIterable(pages);
+ })
+ .map(MwQueryPage::title)
+ .doOnEach(s -> Timber.d("Category returned: %s", s))
+ .map(cat -> cat.replace("Category:", ""));
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java
deleted file mode 100644
index 5ab5ea6b3..000000000
--- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package fr.free.nrw.commons.category;
-
-import java.util.List;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-import fr.free.nrw.commons.Media;
-import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
-import io.reactivex.Single;
-
-@Singleton
-public class CategoryImageController {
-
- private OkHttpJsonApiClient okHttpJsonApiClient;
-
- @Inject
- public CategoryImageController(OkHttpJsonApiClient okHttpJsonApiClient) {
- this.okHttpJsonApiClient = okHttpJsonApiClient;
- }
-
- /**
- * Takes a category name as input and calls the API to get a list of images for that category
- * @param categoryName
- * @return
- */
- public Single> getCategoryImages(String categoryName) {
- return okHttpJsonApiClient.getMediaList("category", categoryName);
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java
deleted file mode 100644
index cd97511cd..000000000
--- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package fr.free.nrw.commons.category;
-
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-public class CategoryImageUtils {
-
- /**
- * The method iterates over the child nodes to return a list of Subcategory name
- * sorted alphabetically
- * @param childNodes
- * @return
- */
- public static List getSubCategoryList(NodeList childNodes) {
- List subCategories = new ArrayList<>();
- for (int i = 0; i < childNodes.getLength(); i++) {
- Node node = childNodes.item(i);
- subCategories.add(getFileName(node));
- }
- Collections.sort(subCategories);
- return subCategories;
- }
-
- /**
- * Extracts the filename of the uploaded image
- * @param document
- * @return
- */
- private static String getFileName(Node document) {
- Element element = (Element) document;
- return element.getAttribute("title");
- }
-
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java
index 001f817b3..9006745d4 100644
--- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java
@@ -2,20 +2,19 @@ package fr.free.nrw.commons.category;
import android.content.Context;
import android.content.Intent;
-import android.database.DataSetObserver;
import android.os.Bundle;
-import androidx.fragment.app.FragmentManager;
-import androidx.fragment.app.FragmentTransaction;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+
import butterknife.ButterKnife;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
@@ -28,7 +27,7 @@ import fr.free.nrw.commons.theme.NavigationBaseActivity;
*/
public class CategoryImagesActivity
- extends AuthenticatedActivity
+ extends NavigationBaseActivity
implements FragmentManager.OnBackStackChangedListener,
MediaDetailPagerFragment.MediaDetailProvider,
AdapterView.OnItemClickListener{
@@ -38,16 +37,6 @@ public class CategoryImagesActivity
private CategoryImagesListFragment categoryImagesListFragment;
private MediaDetailPagerFragment mediaDetails;
- @Override
- protected void onAuthCookieAcquired(String authCookie) {
-
- }
-
- @Override
- protected void onAuthFailure() {
-
- }
-
/**
* This method is called on backPressed of anyFragment in the activity.
* We are changing the icon here from back to hamburger icon.
@@ -69,7 +58,6 @@ public class CategoryImagesActivity
supportFragmentManager = getSupportFragmentManager();
setCategoryImagesFragment();
supportFragmentManager.addOnBackStackChangedListener(this);
- requestAuthToken();
initDrawer();
setPageTitle();
}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java
index 2a846413d..2e4f6e0a1 100644
--- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java
@@ -27,6 +27,7 @@ import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.explore.categories.ExploreActivity;
import fr.free.nrw.commons.kvstore.JsonKvStore;
+import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
@@ -56,7 +57,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
private boolean isLoading = true;
private String categoryName = null;
- @Inject CategoryImageController controller;
+ @Inject MediaClient mediaClient;
@Inject
@Named("default_preferences")
JsonKvStore categoryKvStore;
@@ -116,7 +117,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
isLoading = true;
progressBar.setVisibility(VISIBLE);
- compositeDisposable.add(controller.getCategoryImages(categoryName)
+ compositeDisposable.add(mediaClient.getMediaListFromCategory(categoryName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
@@ -222,7 +223,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
}
progressBar.setVisibility(VISIBLE);
- compositeDisposable.add(controller.getCategoryImages(categoryName)
+ compositeDisposable.add(mediaClient.getMediaListFromCategory(categoryName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryInterface.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryInterface.java
new file mode 100644
index 000000000..f03506ed4
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryInterface.java
@@ -0,0 +1,47 @@
+package fr.free.nrw.commons.category;
+
+import org.wikipedia.dataclient.mwapi.MwQueryResponse;
+
+import io.reactivex.Observable;
+import retrofit2.http.GET;
+import retrofit2.http.Query;
+
+/**
+ * Interface for interacting with Commons category related APIs
+ */
+public interface CategoryInterface {
+
+ /**
+ * Searches for categories with the specified name.
+ *
+ * @param filter The string to be searched
+ * @param itemLimit How many results are returned
+ * @return
+ */
+ @GET("w/api.php?action=query&format=json&formatversion=2"
+ + "&generator=search&gsrnamespace=14")
+ Observable searchCategories(@Query("gsrsearch") String filter,
+ @Query("gsrlimit") int itemLimit, @Query("gsroffset") int offset);
+
+ /**
+ * Searches for categories starting with the specified prefix.
+ *
+ * @param prefix The string to be searched
+ * @param itemLimit How many results are returned
+ * @return
+ */
+ @GET("w/api.php?action=query&format=json&formatversion=2"
+ + "&generator=allcategories")
+ Observable searchCategoriesForPrefix(@Query("gacprefix") String prefix,
+ @Query("gaclimit") int itemLimit, @Query("gacoffset") int offset);
+
+ @GET("w/api.php?action=query&format=json&formatversion=2"
+ + "&generator=categorymembers&gcmtype=subcat"
+ + "&prop=info&gcmlimit=500")
+ Observable getSubCategoryList(@Query("gcmtitle") String categoryName);
+
+ @GET("w/api.php?action=query&format=json&formatversion=2"
+ + "&generator=categories&prop=info&gcllimit=500")
+ Observable getParentCategoryList(@Query("titles") String categoryName);
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/SubCategoryListFragment.java b/app/src/main/java/fr/free/nrw/commons/category/SubCategoryListFragment.java
index 40570b5f8..379caf4c1 100644
--- a/app/src/main/java/fr/free/nrw/commons/category/SubCategoryListFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/category/SubCategoryListFragment.java
@@ -53,7 +53,7 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
TextView categoriesNotFoundView;
private String categoryName = null;
- @Inject MediaWikiApi mwApi;
+ @Inject CategoryClient categoryClient;
private RVRendererAdapter categoriesAdapter;
private boolean isParentCategory = true;
@@ -86,7 +86,7 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
}
/**
- * Checks for internet connection and then initializes the recycler view with 25 categories of the searched query
+ * Checks for internet connection and then initializes the recycler view with all(max 500) categories of the searched query
* Clearing categoryAdapter every time new keyword is searched so that user can see only new results
*/
public void initSubCategoryList() {
@@ -96,17 +96,19 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
return;
}
progressBar.setVisibility(View.VISIBLE);
- if (!isParentCategory){
- compositeDisposable.add(Observable.fromCallable(() -> mwApi.getSubCategoryList(categoryName))
+ if (isParentCategory) {
+ compositeDisposable.add(categoryClient.getParentCategoryList("Category:"+categoryName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .collect(ArrayList::new, ArrayList::add)
.subscribe(this::handleSuccess, this::handleError));
- }else {
- compositeDisposable.add(Observable.fromCallable(() -> mwApi.getParentCategoryList(categoryName))
+ } else {
+ compositeDisposable.add(categoryClient.getSubCategoryList("Category:"+categoryName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .collect(ArrayList::new, ArrayList::add)
.subscribe(this::handleSuccess, this::handleError));
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java
index 12c9ae602..56feafacd 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java
@@ -85,8 +85,8 @@ public class ContributionsFragment
private ContributionsListFragment contributionsListFragment;
private MediaDetailPagerFragment mediaDetailPagerFragment;
- public static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
- public static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
+ private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
+ static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
@BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView;
@BindView(R.id.campaigns_view) CampaignView campaignView;
@@ -257,7 +257,7 @@ public class ContributionsFragment
operations on first time fragment attached to an activity. Then they will be retained
until fragment life time ends.
*/
- if (((MainActivity)getActivity()).isAuthCookieAcquired && !isFragmentAttachedBefore) {
+ if (!isFragmentAttachedBefore) {
onAuthCookieAcquired(((MainActivity)getActivity()).uploadServiceIntent);
isFragmentAttachedBefore = true;
@@ -268,7 +268,7 @@ public class ContributionsFragment
* Replace FrameLayout with ContributionsListFragment, user will see contributions list. Creates
* new one if null.
*/
- public void showContributionsListFragment() {
+ private void showContributionsListFragment() {
// show tabs on contribution list is visible
((MainActivity) getActivity()).showTabs();
// show nearby card view on contributions list is visible
@@ -289,7 +289,7 @@ public class ContributionsFragment
* Replace FrameLayout with MediaDetailPagerFragment, user will see details of selected media.
* Creates new one if null.
*/
- public void showMediaDetailPagerFragment() {
+ private void showMediaDetailPagerFragment() {
// hide tabs on media detail view is visible
((MainActivity)getActivity()).hideTabs();
// hide nearby card view on media detail is visible
@@ -308,7 +308,7 @@ public class ContributionsFragment
* Called when onAuthCookieAcquired is called on authenticated parent activity
* @param uploadServiceIntent
*/
- public void onAuthCookieAcquired(Intent uploadServiceIntent) {
+ void onAuthCookieAcquired(Intent uploadServiceIntent) {
// Since we call onAuthCookieAcquired method from onAttach, isAdded is still false. So don't use it
if (getActivity() != null) { // If fragment is attached to parent activity
@@ -324,7 +324,7 @@ public class ContributionsFragment
* mediaDetailPagerFragment, and preserve previous state in back stack.
* Called when user selects a contribution.
*/
- public void showDetail(int i) {
+ private void showDetail(int i) {
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
mediaDetailPagerFragment = new MediaDetailPagerFragment();
showMediaDetailPagerFragment();
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java
index f37debe7b..455e365d4 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java
@@ -1,8 +1,5 @@
package fr.free.nrw.commons.contributions;
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
-
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -13,21 +10,29 @@ import android.view.animation.AnimationUtils;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.LayoutManager;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
import butterknife.BindView;
import butterknife.ButterKnife;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
-import javax.inject.Inject;
-import javax.inject.Named;
+import fr.free.nrw.commons.wikidata.WikidataClient;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
/**
* Created by root on 01.06.2018.
@@ -53,6 +58,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
@Inject @Named("default_preferences") JsonKvStore kvStore;
@Inject ContributionController controller;
+ @Inject
+ WikidataClient wikidataClient;
private Animation fab_close;
private Animation fab_open;
@@ -163,6 +170,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
/**
* Responsible to set progress bar invisible and visible
+ *
* @param shouldShow True when contributions list should be hidden.
*/
public void showProgress(boolean shouldShow) {
@@ -170,7 +178,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
}
public void showNoContributionsUI(boolean shouldShow) {
- noContributionsYet.setVisibility(shouldShow?VISIBLE:GONE);
+ noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
public void onDataSetChanged() {
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java
index 56946018c..90e27af33 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java
@@ -2,6 +2,7 @@ package fr.free.nrw.commons.contributions;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
+import android.content.ContentResolver;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -12,23 +13,23 @@ import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
-import com.google.android.material.tabs.TabLayout;
-
-import java.util.List;
-
-import javax.inject.Inject;
-
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
+
+import com.google.android.material.tabs.TabLayout;
+
+import java.util.List;
+
+import javax.inject.Inject;
+
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.nearby.NearbyFragment;
@@ -37,15 +38,15 @@ import fr.free.nrw.commons.notification.Notification;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.notification.NotificationController;
import fr.free.nrw.commons.quiz.QuizChecker;
+import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.upload.UploadService;
-import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static android.content.ContentResolver.requestSync;
-public class MainActivity extends AuthenticatedActivity implements FragmentManager.OnBackStackChangedListener {
+public class MainActivity extends NavigationBaseActivity implements FragmentManager.OnBackStackChangedListener {
@Inject
SessionManager sessionManager;
@@ -63,7 +64,6 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
public Intent uploadServiceIntent;
- public boolean isAuthCookieAcquired = false;
public ContributionsActivityPagerAdapter contributionsActivityPagerAdapter;
public static final int CONTRIBUTIONS_TAB_POSITION = 0;
@@ -82,10 +82,10 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
setContentView(R.layout.activity_contributions);
ButterKnife.bind(this);
- requestAuthToken();
initDrawer();
setTitle(getString(R.string.navigation_item_home)); // Should I create a new string variable with another name instead?
+ initMain();
if (savedInstanceState != null ) {
onOrientationChanged = true; // Will be used in nearby fragment to determine significant update of map
@@ -103,16 +103,15 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
outState.putInt("viewPagerCurrentItem", viewPager.getCurrentItem());
}
- @Override
- protected void onAuthCookieAcquired(String authCookie) {
- // Do a sync everytime we get here!
+ private void initMain() {
+ //Do not remove this, this triggers the sync service
+ ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(),BuildConfig.CONTRIBUTION_AUTHORITY,true);
requestSync(sessionManager.getCurrentAccount(), BuildConfig.CONTRIBUTION_AUTHORITY, new Bundle());
uploadServiceIntent = new Intent(this, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
startService(uploadServiceIntent);
addTabsAndFragments();
- isAuthCookieAcquired = true;
if (contributionsActivityPagerAdapter.getItem(0) != null) {
((ContributionsFragment)contributionsActivityPagerAdapter.getItem(0)).onAuthCookieAcquired(uploadServiceIntent);
}
@@ -232,14 +231,9 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
}
}
- @Override
- protected void onAuthFailure() {
-
- }
-
@Override
public void onBackPressed() {
- DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
+ DrawerLayout drawer = findViewById(R.id.drawer_layout);
String contributionsFragmentTag = ((ContributionsActivityPagerAdapter) viewPager.getAdapter()).makeFragmentName(R.id.pager, 0);
String nearbyFragmentTag = ((ContributionsActivityPagerAdapter) viewPager.getAdapter()).makeFragmentName(R.id.pager, 1);
if (drawer.isDrawerOpen(GravityCompat.START)) {
@@ -305,7 +299,7 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
@SuppressLint("CheckResult")
private void setNotificationCount() {
- compositeDisposable.add(Observable.fromCallable(() -> notificationController.getNotifications(false))
+ compositeDisposable.add(notificationController.getNotifications(false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::initNotificationViews,
diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java
index 449361474..b90acd95d 100644
--- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java
+++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java
@@ -9,7 +9,6 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
import fr.free.nrw.commons.category.CategoryDao;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
-import fr.free.nrw.commons.modifications.ModifierSequenceDao;
public class DBOpenHelper extends SQLiteOpenHelper {
@@ -27,7 +26,6 @@ public class DBOpenHelper extends SQLiteOpenHelper {
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
ContributionDao.Table.onCreate(sqLiteDatabase);
- ModifierSequenceDao.Table.onCreate(sqLiteDatabase);
CategoryDao.Table.onCreate(sqLiteDatabase);
BookmarkPicturesDao.Table.onCreate(sqLiteDatabase);
BookmarkLocationsDao.Table.onCreate(sqLiteDatabase);
@@ -37,7 +35,6 @@ public class DBOpenHelper extends SQLiteOpenHelper {
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
ContributionDao.Table.onUpdate(sqLiteDatabase, from, to);
- ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to);
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
BookmarkPicturesDao.Table.onUpdate(sqLiteDatabase, from, to);
BookmarkLocationsDao.Table.onUpdate(sqLiteDatabase, from, to);
diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java
index a5d7e7ef7..3cf6c6f95 100644
--- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java
+++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java
@@ -11,19 +11,22 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Locale;
+import java.util.concurrent.Callable;
import javax.inject.Inject;
+import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.auth.SessionManager;
-import fr.free.nrw.commons.mwapi.MediaWikiApi;
+import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.notification.NotificationHelper;
import fr.free.nrw.commons.review.ReviewController;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
+import io.reactivex.Observable;
import io.reactivex.Single;
+import io.reactivex.SingleSource;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
@@ -35,20 +38,20 @@ import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_D
*/
@Singleton
public class DeleteHelper {
- private final MediaWikiApi mwApi;
- private final SessionManager sessionManager;
private final NotificationHelper notificationHelper;
+ private final PageEditClient pageEditClient;
private final ViewUtilWrapper viewUtil;
+ private final String username;
@Inject
- public DeleteHelper(MediaWikiApi mwApi,
- SessionManager sessionManager,
- NotificationHelper notificationHelper,
- ViewUtilWrapper viewUtil) {
- this.mwApi = mwApi;
- this.sessionManager = sessionManager;
+ public DeleteHelper(NotificationHelper notificationHelper,
+ @Named("commons-page-edit") PageEditClient pageEditClient,
+ ViewUtilWrapper viewUtil,
+ @Named("username") String username) {
this.notificationHelper = notificationHelper;
+ this.pageEditClient = pageEditClient;
this.viewUtil = viewUtil;
+ this.username = username;
}
/**
@@ -59,10 +62,11 @@ public class DeleteHelper {
* @return
*/
public Single makeDeletion(Context context, Media media, String reason) {
- viewUtil.showShortToast(context, context.getString((R.string.delete_helper_make_deletion_toast), media.getDisplayTitle()));
- return Single.fromCallable(() -> delete(media, reason))
- .flatMap(result -> Single.fromCallable(() ->
- showDeletionNotification(context, media, result)));
+ viewUtil.showShortToast(context, "Trying to nominate " + media.getDisplayTitle() + " for deletion");
+
+ return delete(media, reason)
+ .flatMapSingle(result -> Single.just(showDeletionNotification(context, media, result)))
+ .firstOrError();
}
/**
@@ -71,14 +75,9 @@ public class DeleteHelper {
* @param reason
* @return
*/
- private boolean delete(Media media, String reason) {
- String editToken;
- String authCookie;
+ private Observable delete(Media media, String reason) {
+ Timber.d("thread is delete %s", Thread.currentThread().getName());
String summary = "Nominating " + media.getFilename() + " for deletion.";
-
- authCookie = sessionManager.getAuthCookie();
- mwApi.setAuthCookie(authCookie);
-
Calendar calendar = Calendar.getInstance();
String fileDeleteString = "{{delete|reason=" + reason +
"|subpage=" + media.getFilename() +
@@ -99,26 +98,23 @@ public class DeleteHelper {
String userPageString = "\n{{subst:idw|" + media.getFilename() +
"}} ~~~~";
- try {
- editToken = mwApi.getEditToken();
-
- if(editToken == null) {
- return false;
- }
-
- mwApi.prependEdit(editToken, fileDeleteString + "\n",
- media.getFilename(), summary);
- mwApi.edit(editToken, subpageString + "\n",
- "Commons:Deletion_requests/" + media.getFilename(), summary);
- mwApi.appendEdit(editToken, logPageString + "\n",
- "Commons:Deletion_requests/" + date, summary);
- mwApi.appendEdit(editToken, userPageString + "\n",
- "User_Talk:" + media.getCreator(), summary);
- } catch (Exception e) {
- Timber.e(e);
- return false;
- }
- return true;
+ return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary)
+ .flatMap(result -> {
+ if (result) {
+ return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);
+ }
+ throw new RuntimeException("Failed to nominate for deletion");
+ }).flatMap(result -> {
+ if (result) {
+ return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary);
+ }
+ throw new RuntimeException("Failed to nominate for deletion");
+ }).flatMap(result -> {
+ if (result) {
+ return pageEditClient.appendEdit("User_Talk:" + username, userPageString + "\n", summary);
+ }
+ throw new RuntimeException("Failed to nominate for deletion");
+ });
}
private boolean showDeletionNotification(Context context, Media media, boolean result) {
@@ -191,7 +187,12 @@ public class DeleteHelper {
}
}
- makeDeletion(context, media, reason)
+ Timber.d("thread is askReasonAndExecute %s", Thread.currentThread().getName());
+
+ String finalReason = reason;
+
+ Single.defer((Callable>) () ->
+ makeDeletion(context, media, finalReason))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(aBoolean -> {
diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java
index 22ac5a40e..242f889df 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java
@@ -1,6 +1,5 @@
package fr.free.nrw.commons.di;
-import fr.free.nrw.commons.contributions.ContributionsModule;
import javax.inject.Singleton;
import dagger.Component;
@@ -10,8 +9,8 @@ import dagger.android.support.AndroidSupportInjectionModule;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionViewHolder;
+import fr.free.nrw.commons.contributions.ContributionsModule;
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
-import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
import fr.free.nrw.commons.nearby.PlaceRenderer;
import fr.free.nrw.commons.review.ReviewController;
import fr.free.nrw.commons.settings.SettingsFragment;
@@ -36,8 +35,6 @@ public interface CommonsApplicationComponent extends AndroidInjector mwApi.searchCategory(query,queryList.size()))
+ compositeDisposable.add(categoryClient.searchCategories(query,25)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.doOnSubscribe(disposable -> saveQuery(query))
+ .collect(ArrayList::new, ArrayList::add)
.subscribe(this::handleSuccess, this::handleError));
}
/**
- * Adds more results to existing search results
+ * Adds 25 more results to existing search results
*/
public void addCategoriesToList(String query) {
+ if(isLoadingCategories) return;
+ isLoadingCategories=true;
this.query = query;
bottomProgressBar.setVisibility(View.VISIBLE);
progressBar.setVisibility(GONE);
- compositeDisposable.add(Observable.fromCallable(() -> mwApi.searchCategory(query,queryList.size()))
+ compositeDisposable.add(categoryClient.searchCategories(query,25, queryList.size())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .collect(ArrayList::new, ArrayList::add)
.subscribe(this::handlePaginationSuccess, this::handleError));
}
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
- * @param mediaList
*/
private void handlePaginationSuccess(List mediaList) {
queryList.addAll(mediaList);
@@ -169,6 +176,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
bottomProgressBar.setVisibility(GONE);
categoriesAdapter.addAll(mediaList);
categoriesAdapter.notifyDataSetChanged();
+ isLoadingCategories=false;
}
@@ -176,7 +184,6 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
- * @param mediaList
*/
private void handleSuccess(List mediaList) {
queryList = mediaList;
@@ -194,7 +201,6 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
/**
* Logs and handles API error scenario
- * @param throwable
*/
private void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading queried categories");
@@ -213,7 +219,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
private void initErrorView() {
progressBar.setVisibility(GONE);
categoriesNotFoundView.setVisibility(VISIBLE);
- categoriesNotFoundView.setText(getString(R.string.categories_not_found, query));
+ categoriesNotFoundView.setText(getString(R.string.categories_not_found));
}
/**
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java
index eb32cfb64..895b2ff13 100644
--- a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java
@@ -24,7 +24,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
-import com.pedrogomez.renderers.RVRendererAdapter;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
@@ -32,18 +31,11 @@ import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.kvstore.JsonKvStore;
-import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
+import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import javax.inject.Inject;
-import javax.inject.Named;
import timber.log.Timber;
import static android.view.View.GONE;
@@ -69,7 +61,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
@Inject RecentSearchesDao recentSearchesDao;
@Inject
- OkHttpJsonApiClient okHttpJsonApiClient;
+ MediaClient mediaClient;
@Inject
@Named("default_preferences")
JsonKvStore defaultKvStore;
@@ -148,7 +140,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
bottomProgressBar.setVisibility(GONE);
queryList.clear();
imagesAdapter.clear();
- compositeDisposable.add(okHttpJsonApiClient.getMediaList("search", query)
+ compositeDisposable.add(mediaClient.getMediaListFromSearch(query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
@@ -165,7 +157,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
this.query = query;
bottomProgressBar.setVisibility(View.VISIBLE);
progressBar.setVisibility(GONE);
- compositeDisposable.add(okHttpJsonApiClient.getMediaList("search", query)
+ compositeDisposable.add(mediaClient.getMediaListFromSearch(query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
@@ -228,7 +220,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
private void initErrorView() {
progressBar.setVisibility(GONE);
imagesNotFoundView.setVisibility(VISIBLE);
- imagesNotFoundView.setText(getString(R.string.images_not_found, query));
+ imagesNotFoundView.setText(getString(R.string.images_not_found));
}
/**
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java
new file mode 100644
index 000000000..58e014d42
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java
@@ -0,0 +1,164 @@
+package fr.free.nrw.commons.media;
+
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.wikipedia.dataclient.mwapi.MwQueryPage;
+import org.wikipedia.dataclient.mwapi.MwQueryResponse;
+import org.wikipedia.dataclient.mwapi.MwQueryResult;
+
+import java.util.Date;
+import org.wikipedia.dataclient.mwapi.MwQueryResponse;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.utils.CommonsDateUtil;
+import io.reactivex.Observable;
+import io.reactivex.Single;
+import okhttp3.HttpUrl;
+import okhttp3.Request;
+import okhttp3.Response;
+import timber.log.Timber;
+import io.reactivex.Observable;
+import io.reactivex.Single;
+
+/**
+ * Media Client to handle custom calls to Commons MediaWiki APIs
+ */
+@Singleton
+public class MediaClient {
+
+ private final MediaInterface mediaInterface;
+
+ //OkHttpJsonApiClient used JsonKvStore for this. I don't know why.
+ private Map> continuationStore;
+
+ @Inject
+ public MediaClient(MediaInterface mediaInterface) {
+ this.mediaInterface = mediaInterface;
+ this.continuationStore = new HashMap<>();
+ }
+
+ /**
+ * Checks if a page exists on Commons
+ * The same method can be used to check for file or talk page
+ *
+ * @param title File:Test.jpg or Commons:Deletion_requests/File:Test1.jpeg
+ */
+ public Single checkPageExistsUsingTitle(String title) {
+ return mediaInterface.checkPageExistsUsingTitle(title)
+ .map(mwQueryResponse -> mwQueryResponse
+ .query().firstPage().pageId() > 0)
+ .singleOrError();
+ }
+
+ /**
+ * Take the fileSha and returns whether a file with a matching SHA exists or not
+ *
+ * @param fileSha SHA of the file to be checked
+ */
+ public Single checkFileExistsUsingSha(String fileSha) {
+ return mediaInterface.checkFileExistsUsingSha(fileSha)
+ .map(mwQueryResponse -> mwQueryResponse
+ .query().allImages().size() > 0)
+ .singleOrError();
+ }
+
+ /**
+ * This method takes the category as input and returns a list of Media objects filtered using image generator query
+ * It uses the generator query API to get the images searched using a query, 10 at a time.
+ *
+ * @param category the search category. Must start with "Category:"
+ * @return
+ */
+ public Single> getMediaListFromCategory(String category) {
+ return responseToMediaList(
+ continuationStore.containsKey("category_" + category) ?
+ mediaInterface.getMediaListFromCategory(category, 10, continuationStore.get("category_" + category)) : //if true
+ mediaInterface.getMediaListFromCategory(category, 10, Collections.emptyMap()),
+ "category_" + category); //if false
+
+ }
+
+ /**
+ * This method takes a keyword as input and returns a list of Media objects filtered using image generator query
+ * It uses the generator query API to get the images searched using a query, 10 at a time.
+ *
+ * @param keyword the search keyword
+ * @return
+ */
+ public Single> getMediaListFromSearch(String keyword) {
+ return responseToMediaList(
+ continuationStore.containsKey("search_" + keyword) ?
+ mediaInterface.getMediaListFromSearch(keyword, 10, continuationStore.get("search_" + keyword)) : //if true
+ mediaInterface.getMediaListFromSearch(keyword, 10, Collections.emptyMap()), //if false
+ "search_" + keyword);
+
+ }
+
+ private Single> responseToMediaList(Observable response, String key) {
+ return response.flatMap(mwQueryResponse -> {
+ if (null == mwQueryResponse
+ || null == mwQueryResponse.query()
+ || null == mwQueryResponse.query().pages()) {
+ return Observable.empty();
+ }
+ continuationStore.put(key, mwQueryResponse.continuation());
+ return Observable.fromIterable(mwQueryResponse.query().pages());
+ })
+ .map(Media::from)
+ .collect(ArrayList::new, List::add);
+ }
+
+ /**
+ * Fetches Media object from the imageInfo API
+ *
+ * @param titles the tiles to be searched for. Can be filename or template name
+ * @return
+ */
+ public Single getMedia(String titles) {
+ return mediaInterface.getMedia(titles)
+ .flatMap(mwQueryResponse -> {
+ if (null == mwQueryResponse
+ || null == mwQueryResponse.query()
+ || null == mwQueryResponse.query().firstPage()) {
+ return Observable.empty();
+ }
+ return Observable.just(mwQueryResponse.query().firstPage());
+ })
+ .map(Media::from)
+ .single(Media.EMPTY);
+ }
+
+ /**
+ * The method returns the picture of the day
+ *
+ * @return Media object corresponding to the picture of the day
+ */
+ @NonNull
+ public Single getPictureOfTheDay() {
+ String date = CommonsDateUtil.getIso8601DateFormatShort().format(new Date());
+ Timber.d("Current date is %s", date);
+ String template = "Template:Potd/" + date;
+ return mediaInterface.getMediaWithGenerator(template)
+ .flatMap(mwQueryResponse -> {
+ if (null == mwQueryResponse
+ || null == mwQueryResponse.query()
+ || null == mwQueryResponse.query().firstPage()) {
+ return Observable.empty();
+ }
+ return Observable.just(mwQueryResponse.query().firstPage());
+ })
+ .map(Media::from)
+ .single(Media.EMPTY);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java
new file mode 100644
index 000000000..c54e33e62
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java
@@ -0,0 +1,89 @@
+package fr.free.nrw.commons.media;
+
+import org.jetbrains.annotations.NotNull;
+import org.wikipedia.dataclient.mwapi.MwQueryResponse;
+
+import java.util.Map;
+
+import io.reactivex.Observable;
+import retrofit2.http.GET;
+import retrofit2.http.Query;
+import retrofit2.http.QueryMap;
+
+/**
+ * Interface for interacting with Commons media related APIs
+ */
+public interface MediaInterface {
+ /**
+ * Checks if a page exists or not.
+ *
+ * @param title the title of the page to be checked
+ * @return
+ */
+ @GET("w/api.php?action=query&format=json&formatversion=2")
+ Observable checkPageExistsUsingTitle(@Query("titles") String title);
+
+ /**
+ * Check if file exists
+ *
+ * @param aisha1 the SHA of the media file to be checked
+ * @return
+ */
+ @GET("w/api.php?action=query&format=json&formatversion=2&list=allimages")
+ Observable checkFileExistsUsingSha(@Query("aisha1") String aisha1);
+
+ /**
+ * This method retrieves a list of Media objects filtered using image generator query
+ *
+ * @param category the category name. Must start with "Category:"
+ * @param itemLimit how many images are returned
+ * @param continuation the continuation string from the previous query or empty map
+ * @return
+ */
+ @GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
+ "&generator=categorymembers&gcmtype=file&gcmsort=timestamp&gcmdir=desc" + //Category parameters
+ "&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=640" + //Media property parameters
+ "&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal" +
+ "|Artist|LicenseShortName|LicenseUrl")
+ Observable getMediaListFromCategory(@Query("gcmtitle") String category, @Query("gcmlimit") int itemLimit, @QueryMap Map continuation);
+
+ /**
+ * This method retrieves a list of Media objects filtered using image generator query
+ *
+ * @param keyword the searched keyword
+ * @param itemLimit how many images are returned
+ * @param continuation the continuation string from the previous query
+ * @return
+ */
+ @GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
+ "&generator=search&gsrwhat=text&gsrnamespace=6" + //Search parameters
+ "&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=640" + //Media property parameters
+ "&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal" +
+ "|Artist|LicenseShortName|LicenseUrl")
+ Observable getMediaListFromSearch(@Query("gsrsearch") String keyword, @Query("gsrlimit") int itemLimit, @QueryMap Map continuation);
+
+ /**
+ * Fetches Media object from the imageInfo API
+ *
+ * @param title the tiles to be searched for. Can be filename or template name
+ * @return
+ */
+ @GET("w/api.php?action=query&format=json&formatversion=2" +
+ "&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=640" +
+ "&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal" +
+ "|Artist|LicenseShortName|LicenseUrl")
+ Observable getMedia(@Query("titles") String title);
+
+ /**
+ * Fetches Media object from the imageInfo API
+ * Passes an image generator parameter
+ *
+ * @param title the tiles to be searched for. Can be filename or template name
+ * @return
+ */
+ @GET("w/api.php?action=query&format=json&formatversion=2&generator=images" +
+ "&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=640" +
+ "&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal" +
+ "|Artist|LicenseShortName|LicenseUrl")
+ Observable getMediaWithGenerator(@Query("titles") String title);
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/CategoryModifier.java b/app/src/main/java/fr/free/nrw/commons/modifications/CategoryModifier.java
deleted file mode 100644
index 657207eca..000000000
--- a/app/src/main/java/fr/free/nrw/commons/modifications/CategoryModifier.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package fr.free.nrw.commons.modifications;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-public class CategoryModifier extends PageModifier {
-
- public static String PARAM_CATEGORIES = "categories";
-
- public static String MODIFIER_NAME = "CategoriesModifier";
-
- public CategoryModifier(String... categories) {
- super(MODIFIER_NAME);
- JSONArray categoriesArray = new JSONArray();
- for (String category: categories) {
- categoriesArray.put(category);
- }
- try {
- params.putOpt(PARAM_CATEGORIES, categoriesArray);
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- }
-
- public CategoryModifier(JSONObject data) {
- super(MODIFIER_NAME);
- this.params = data;
- }
-
- @Override
- public String doModification(String pageName, String pageContents) {
- JSONArray categories;
- categories = params.optJSONArray(PARAM_CATEGORIES);
-
- StringBuilder categoriesString = new StringBuilder();
- for (int i = 0; i < categories.length(); i++) {
- String category = categories.optString(i);
- categoriesString.append("\n[[Category:").append(category).append("]]");
- }
- return pageContents + categoriesString.toString();
- }
-
- @Override
- public String getEditSummary() {
- return "Added " + params.optJSONArray(PARAM_CATEGORIES).length() + " categories.";
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java
deleted file mode 100644
index a51a48210..000000000
--- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java
+++ /dev/null
@@ -1,161 +0,0 @@
-package fr.free.nrw.commons.modifications;
-
-import android.content.ContentValues;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteQueryBuilder;
-import android.net.Uri;
-import androidx.annotation.NonNull;
-import android.text.TextUtils;
-
-import javax.inject.Inject;
-
-import fr.free.nrw.commons.BuildConfig;
-import fr.free.nrw.commons.data.DBOpenHelper;
-import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
-import timber.log.Timber;
-
-import static fr.free.nrw.commons.modifications.ModifierSequenceDao.Table.TABLE_NAME;
-
-public class ModificationsContentProvider extends CommonsDaggerContentProvider {
-
- private static final int MODIFICATIONS = 1;
- private static final int MODIFICATIONS_ID = 2;
-
- public static final String BASE_PATH = "modifications";
-
- public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.MODIFICATION_AUTHORITY + "/" + BASE_PATH);
-
- private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
- static {
- uriMatcher.addURI(BuildConfig.MODIFICATION_AUTHORITY, BASE_PATH, MODIFICATIONS);
- uriMatcher.addURI(BuildConfig.MODIFICATION_AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID);
- }
-
- public static Uri uriForId(int id) {
- return Uri.parse(BASE_URI.toString() + "/" + id);
- }
-
- @Inject DBOpenHelper dbOpenHelper;
-
- @Override
- public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
- SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
- queryBuilder.setTables(TABLE_NAME);
-
- int uriType = uriMatcher.match(uri);
-
- switch (uriType) {
- case MODIFICATIONS:
- break;
- default:
- throw new IllegalArgumentException("Unknown URI" + uri);
- }
-
- SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
-
- Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
- cursor.setNotificationUri(getContext().getContentResolver(), uri);
-
- return cursor;
- }
-
- @Override
- public String getType(@NonNull Uri uri) {
- return null;
- }
-
- @Override
- public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
- int uriType = uriMatcher.match(uri);
- SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
- long id;
- switch (uriType) {
- case MODIFICATIONS:
- id = sqlDB.insert(TABLE_NAME, null, contentValues);
- break;
- default:
- throw new IllegalArgumentException("Unknown URI: " + uri);
- }
- getContext().getContentResolver().notifyChange(uri, null);
- return Uri.parse(BASE_URI + "/" + id);
- }
-
- @Override
- public int delete(@NonNull Uri uri, String s, String[] strings) {
- int uriType = uriMatcher.match(uri);
- SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
- switch (uriType) {
- case MODIFICATIONS_ID:
- String id = uri.getLastPathSegment();
- sqlDB.delete(TABLE_NAME,
- "_id = ?",
- new String[] { id }
- );
- return 1;
- default:
- throw new IllegalArgumentException("Unknown URI: " + uri);
- }
- }
-
- @Override
- public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
- Timber.d("Hello, bulk insert! (ModificationsContentProvider)");
- int uriType = uriMatcher.match(uri);
- SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
- sqlDB.beginTransaction();
- switch (uriType) {
- case MODIFICATIONS:
- for (ContentValues value: values) {
- Timber.d("Inserting! %s", value);
- sqlDB.insert(TABLE_NAME, null, value);
- }
- break;
- default:
- throw new IllegalArgumentException("Unknown URI: " + uri);
- }
- sqlDB.setTransactionSuccessful();
- sqlDB.endTransaction();
- getContext().getContentResolver().notifyChange(uri, null);
- return values.length;
- }
-
- @Override
- public int update(@NonNull Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
- /*
- SQL Injection warnings: First, note that we're not exposing this to the outside world (exported="false")
- Even then, we should make sure to sanitize all user input appropriately. Input that passes through ContentValues
- should be fine. So only issues are those that pass in via concating.
-
- In here, the only concat created argument is for id. It is cast to an int, and will error out otherwise.
- */
- int uriType = uriMatcher.match(uri);
- SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
- int rowsUpdated;
- switch (uriType) {
- case MODIFICATIONS:
- rowsUpdated = sqlDB.update(TABLE_NAME,
- contentValues,
- selection,
- selectionArgs);
- break;
- case MODIFICATIONS_ID:
- int id = Integer.valueOf(uri.getLastPathSegment());
-
- if (TextUtils.isEmpty(selection)) {
- rowsUpdated = sqlDB.update(TABLE_NAME,
- contentValues,
- ModifierSequenceDao.Table.COLUMN_ID + " = ?",
- new String[] { String.valueOf(id) } );
- } else {
- throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID");
- }
- break;
- default:
- throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType);
- }
- getContext().getContentResolver().notifyChange(uri, null);
- return rowsUpdated;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java
deleted file mode 100644
index 6043f6d03..000000000
--- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java
+++ /dev/null
@@ -1,145 +0,0 @@
-package fr.free.nrw.commons.modifications;
-
-import android.accounts.Account;
-import android.content.AbstractThreadedSyncAdapter;
-import android.content.ContentProviderClient;
-import android.content.Context;
-import android.content.SyncResult;
-import android.database.Cursor;
-import android.os.Bundle;
-import android.os.RemoteException;
-
-import java.io.IOException;
-
-import javax.inject.Inject;
-
-import fr.free.nrw.commons.BuildConfig;
-import fr.free.nrw.commons.auth.SessionManager;
-import fr.free.nrw.commons.contributions.Contribution;
-import fr.free.nrw.commons.contributions.ContributionDao;
-import fr.free.nrw.commons.di.ApplicationlessInjection;
-import fr.free.nrw.commons.mwapi.MediaWikiApi;
-import timber.log.Timber;
-
-public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
-
- @Inject MediaWikiApi mwApi;
- @Inject ContributionDao contributionDao;
- @Inject ModifierSequenceDao modifierSequenceDao;
- @Inject
- SessionManager sessionManager;
-
- public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
- super(context, autoInitialize);
- }
-
- @Override
- 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!
- ApplicationlessInjection
- .getInstance(getContext()
- .getApplicationContext())
- .getCommonsApplicationComponent()
- .inject(this);
-
- Cursor allModifications;
- try {
- allModifications = contentProviderClient.query(ModificationsContentProvider.BASE_URI, null, null, null, null);
- } catch (RemoteException e) {
- throw new RuntimeException(e);
- }
-
- // Exit early if nothing to do
- if (allModifications == null || allModifications.getCount() == 0) {
- Timber.d("No modifications to perform");
- return;
- }
-
- String authCookie = sessionManager.getAuthCookie();
- if (isNullOrWhiteSpace(authCookie)) {
- Timber.d("Could not authenticate :(");
- return;
- }
-
- mwApi.setAuthCookie(authCookie);
- String editToken;
-
- try {
- editToken = mwApi.getEditToken();
- } catch (IOException e) {
- Timber.d("Can not retreive edit token!");
- return;
- }
-
- allModifications.moveToFirst();
-
- Timber.d("Found %d modifications to execute", allModifications.getCount());
-
- ContentProviderClient contributionsClient = null;
- try {
- contributionsClient = getContext().getContentResolver().acquireContentProviderClient(BuildConfig.CONTRIBUTION_AUTHORITY);
-
- while (!allModifications.isAfterLast()) {
- ModifierSequence sequence = modifierSequenceDao.fromCursor(allModifications);
- Contribution contrib;
- Cursor contributionCursor;
-
- if (contributionsClient == null) {
- Timber.e("ContributionsClient is null. This should not happen!");
- return;
- }
-
- try {
- contributionCursor = contributionsClient.query(sequence.getMediaUri(), null, null, null, null);
- } catch (RemoteException e) {
- throw new RuntimeException(e);
- }
-
- if (contributionCursor != null) {
- contributionCursor.moveToFirst();
- }
-
- contrib = contributionDao.fromCursor(contributionCursor);
-
- if (contrib != null && contrib.getState() == Contribution.STATE_COMPLETED) {
- String pageContent;
- try {
- pageContent = mwApi.revisionsByFilename(contrib.getFilename());
- } catch (IOException e) {
- Timber.d("Network messed up on modifications sync!");
- continue;
- }
-
- Timber.d("Page content is %s", pageContent);
- String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent);
-
- String editResult;
- try {
- editResult = mwApi.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
- } catch (IOException e) {
- Timber.d("Network messed up on modifications sync!");
- continue;
- }
-
- Timber.d("Response is %s", editResult);
-
- if (!"Success".equals(editResult)) {
- // FIXME: Log this somewhere else
- Timber.d("Non success result! %s", editResult);
- } else {
- modifierSequenceDao.delete(sequence);
- }
- }
- allModifications.moveToNext();
- }
- } finally {
- if (contributionsClient != null) {
- contributionsClient.release();
- }
- }
- }
-
- private boolean isNullOrWhiteSpace(String value) {
- return value == null || value.trim().isEmpty();
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncService.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncService.java
deleted file mode 100644
index bf6878622..000000000
--- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncService.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package fr.free.nrw.commons.modifications;
-
-import android.app.Service;
-import android.content.Intent;
-import android.os.IBinder;
-
-public class ModificationsSyncService extends Service {
-
- private static final Object sSyncAdapterLock = new Object();
-
- private static ModificationsSyncAdapter sSyncAdapter = null;
-
- @Override
- public void onCreate() {
- super.onCreate();
- synchronized (sSyncAdapterLock) {
- if (sSyncAdapter == null) {
- sSyncAdapter = new ModificationsSyncAdapter(this, true);
- }
- }
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return sSyncAdapter.getSyncAdapterBinder();
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java
deleted file mode 100644
index f563b1a7b..000000000
--- a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package fr.free.nrw.commons.modifications;
-
-import android.net.Uri;
-
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import java.util.ArrayList;
-
-public class ModifierSequence {
- private Uri mediaUri;
- private ArrayList modifiers;
- private Uri contentUri;
-
- public ModifierSequence(Uri mediaUri) {
- this.mediaUri = mediaUri;
- modifiers = new ArrayList<>();
- }
-
- ModifierSequence(Uri mediaUri, JSONObject data) {
- this(mediaUri);
- JSONArray modifiersJSON = data.optJSONArray("modifiers");
- for (int i = 0; i < modifiersJSON.length(); i++) {
- modifiers.add(PageModifier.fromJSON(modifiersJSON.optJSONObject(i)));
- }
- }
-
- Uri getMediaUri() {
- return mediaUri;
- }
-
- public void queueModifier(PageModifier modifier) {
- modifiers.add(modifier);
- }
-
- String executeModifications(String pageName, String pageContents) {
- for (PageModifier modifier: modifiers) {
- pageContents = modifier.doModification(pageName, pageContents);
- }
- return pageContents;
- }
-
- String getEditSummary() {
- StringBuilder editSummary = new StringBuilder();
- for (PageModifier modifier: modifiers) {
- editSummary.append(modifier.getEditSummary()).append(" ");
- }
- editSummary.append("Using [[COM:MOA|Commons Mobile App]]");
- return editSummary.toString();
- }
-
- ArrayList getModifiers() {
- return modifiers;
- }
-
- Uri getContentUri() {
- return contentUri;
- }
-
- void setContentUri(Uri contentUri) {
- this.contentUri = contentUri;
- }
-
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java
deleted file mode 100644
index 957656a24..000000000
--- a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java
+++ /dev/null
@@ -1,124 +0,0 @@
-package fr.free.nrw.commons.modifications;
-
-import android.content.ContentProviderClient;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.net.Uri;
-import android.os.RemoteException;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Provider;
-
-public class ModifierSequenceDao {
-
- private final Provider clientProvider;
-
- @Inject
- public ModifierSequenceDao(@Named("modification") Provider clientProvider) {
- this.clientProvider = clientProvider;
- }
-
- public void save(ModifierSequence sequence) {
- ContentProviderClient db = clientProvider.get();
- try {
- if (sequence.getContentUri() == null) {
- sequence.setContentUri(db.insert(ModificationsContentProvider.BASE_URI, toContentValues(sequence)));
- } else {
- db.update(sequence.getContentUri(), toContentValues(sequence), null, null);
- }
- } catch (RemoteException e) {
- throw new RuntimeException(e);
- } finally {
- db.release();
- }
- }
-
- public void delete(ModifierSequence sequence) {
- ContentProviderClient db = clientProvider.get();
- try {
- db.delete(sequence.getContentUri(), null, null);
- } catch (RemoteException e) {
- throw new RuntimeException(e);
- } finally {
- db.release();
- }
- }
-
- ModifierSequence fromCursor(Cursor cursor) {
- // Hardcoding column positions!
- ModifierSequence ms;
- try {
- ms = new ModifierSequence(Uri.parse(cursor.getString(cursor.getColumnIndex(Table.COLUMN_MEDIA_URI))),
- new JSONObject(cursor.getString(cursor.getColumnIndex(Table.COLUMN_DATA))));
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- ms.setContentUri( ModificationsContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))));
-
- return ms;
- }
-
- private JSONObject toJSON(ModifierSequence sequence) {
- JSONObject data = new JSONObject();
- try {
- JSONArray modifiersJSON = new JSONArray();
- for (PageModifier modifier: sequence.getModifiers()) {
- modifiersJSON.put(modifier.toJSON());
- }
- data.put("modifiers", modifiersJSON);
- return data;
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- }
-
- private ContentValues toContentValues(ModifierSequence sequence) {
- ContentValues cv = new ContentValues();
- cv.put(Table.COLUMN_MEDIA_URI, sequence.getMediaUri().toString());
- cv.put(Table.COLUMN_DATA, toJSON(sequence).toString());
- return cv;
- }
-
- public static class Table {
- static final String TABLE_NAME = "modifications";
-
- static final String COLUMN_ID = "_id";
- static final String COLUMN_MEDIA_URI = "mediauri";
- static final String COLUMN_DATA = "data";
-
- // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
- public static final String[] ALL_FIELDS = {
- COLUMN_ID,
- COLUMN_MEDIA_URI,
- COLUMN_DATA
- };
-
- static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
-
- static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
- + "_id INTEGER PRIMARY KEY,"
- + "mediauri STRING,"
- + "data STRING"
- + ");";
-
- public static void onCreate(SQLiteDatabase db) {
- db.execSQL(CREATE_TABLE_STATEMENT);
- }
-
- public static void onUpdate(SQLiteDatabase db, int from, int to) {
- db.execSQL(DROP_TABLE_STATEMENT);
- onCreate(db);
- }
-
- public static void onDelete(SQLiteDatabase db) {
- db.execSQL(DROP_TABLE_STATEMENT);
- onCreate(db);
- }
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/PageModifier.java b/app/src/main/java/fr/free/nrw/commons/modifications/PageModifier.java
deleted file mode 100644
index 8bc278b0f..000000000
--- a/app/src/main/java/fr/free/nrw/commons/modifications/PageModifier.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package fr.free.nrw.commons.modifications;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-public abstract class PageModifier {
-
- public static PageModifier fromJSON(JSONObject data) {
- String name = data.optString("name");
- if (name.equals(CategoryModifier.MODIFIER_NAME)) {
- return new CategoryModifier(data.optJSONObject("data"));
- } else if (name.equals(TemplateRemoveModifier.MODIFIER_NAME)) {
- return new TemplateRemoveModifier(data.optJSONObject("data"));
- }
-
- return null;
- }
-
- protected String name;
- protected JSONObject params;
-
- protected PageModifier(String name) {
- this.name = name;
- params = new JSONObject();
- }
-
- public abstract String doModification(String pageName, String pageContents);
-
- public abstract String getEditSummary();
-
- public JSONObject toJSON() {
- JSONObject data = new JSONObject();
- try {
- data.putOpt("name", name);
- data.put("data", params);
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- return data;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/TemplateRemoveModifier.java b/app/src/main/java/fr/free/nrw/commons/modifications/TemplateRemoveModifier.java
deleted file mode 100644
index cddcfbacf..000000000
--- a/app/src/main/java/fr/free/nrw/commons/modifications/TemplateRemoveModifier.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package fr.free.nrw.commons.modifications;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class TemplateRemoveModifier extends PageModifier {
-
- public static final String MODIFIER_NAME = "TemplateRemoverModifier";
-
- public static final String PARAM_TEMPLATE_NAME = "template";
-
- public static final Pattern PATTERN_TEMPLATE_OPEN = Pattern.compile("\\{\\{");
- public static final Pattern PATTERN_TEMPLATE_CLOSE = Pattern.compile("\\}\\}");
-
- public TemplateRemoveModifier(String templateName) {
- super(MODIFIER_NAME);
- try {
- params.putOpt(PARAM_TEMPLATE_NAME, templateName);
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- }
-
- public TemplateRemoveModifier(JSONObject data) {
- super(MODIFIER_NAME);
- this.params = data;
- }
-
- @Override
- public String doModification(String pageName, String pageContents) {
- String templateRawName = params.optString(PARAM_TEMPLATE_NAME);
- // Wikitext title normalizing rules. Spaces and _ equivalent
- // They also 'condense' - any number of them reduce to just one (just like HTML)
- String templateNormalized = templateRawName.trim().replaceAll("(\\s|_)+", "(\\s|_)+");
-
- // Not supporting {{ inside and HTML comments yet
- // (Thanks to marktraceur for reminding me of the HTML comments exception)
- Pattern templateStartPattern = Pattern.compile("\\{\\{" + templateNormalized, Pattern.CASE_INSENSITIVE);
- Matcher matcher = templateStartPattern.matcher(pageContents);
-
- while (matcher.find()) {
- int braceCount = 1;
- int startIndex = matcher.start();
- int curIndex = matcher.end();
- Matcher openMatch = PATTERN_TEMPLATE_OPEN.matcher(pageContents);
- Matcher closeMatch = PATTERN_TEMPLATE_CLOSE.matcher(pageContents);
-
- while (curIndex < pageContents.length()) {
- boolean openFound = openMatch.find(curIndex);
- boolean closeFound = closeMatch.find(curIndex);
-
- if (openFound && (!closeFound || openMatch.start() < closeMatch.start())) {
- braceCount++;
- curIndex = openMatch.end();
- } else if (closeFound) {
- braceCount--;
- curIndex = closeMatch.end();
- } else if (braceCount > 0) {
- // The template never closes, so...remove nothing
- curIndex = startIndex;
- break;
- }
-
- if (braceCount == 0) {
- // The braces have all been closed!
- break;
- }
- }
-
- // Strip trailing whitespace
- while (curIndex < pageContents.length()) {
- if (pageContents.charAt(curIndex) == ' ' || pageContents.charAt(curIndex) == '\n') {
- curIndex++;
- } else {
- break;
- }
- }
-
- // I am so going to hell for this, sigh
- pageContents = pageContents.substring(0, startIndex) + pageContents.substring(curIndex);
- matcher = templateStartPattern.matcher(pageContents);
- }
-
- return pageContents;
- }
-
- @Override
- public String getEditSummary() {
- return "Removed template " + params.optString(PARAM_TEMPLATE_NAME) + ".";
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java
index 8f5b4213f..b356184d8 100644
--- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java
+++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java
@@ -1,12 +1,11 @@
package fr.free.nrw.commons.mwapi;
-import android.content.Context;
-import android.net.Uri;
import android.text.TextUtils;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.google.gson.Gson;
-import org.apache.commons.lang3.StringUtils;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
@@ -17,34 +16,19 @@ 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.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
import org.wikipedia.util.DateUtil;
import java.io.IOException;
-import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-import java.util.concurrent.Callable;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.auth.AccountUtil;
-import fr.free.nrw.commons.category.CategoryImageUtils;
-import fr.free.nrw.commons.category.QueryContinue;
-import fr.free.nrw.commons.kvstore.JsonKvStore;
-import fr.free.nrw.commons.notification.Notification;
-import fr.free.nrw.commons.notification.NotificationUtils;
-import fr.free.nrw.commons.utils.ViewUtil;
-import io.reactivex.Observable;
+
+import fr.free.nrw.commons.BuildConfig;
+import fr.free.nrw.commons.CommonsApplication;
+
import io.reactivex.Single;
import timber.log.Timber;
@@ -54,19 +38,8 @@ import timber.log.Timber;
public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private AbstractHttpClient httpClient;
private CustomMwApi api;
- private CustomMwApi wikidataApi;
- private Context context;
- private JsonKvStore defaultKvStore;
- private Gson gson;
- private final String ERROR_CODE_BAD_TOKEN = "badtoken";
-
- public ApacheHttpClientMediaWikiApi(Context context,
- String apiURL,
- String wikidatApiURL,
- JsonKvStore defaultKvStore,
- Gson gson) {
- this.context = context;
+ public ApacheHttpClientMediaWikiApi(String apiURL) {
BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
@@ -79,217 +52,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
httpClient.addRequestInterceptor(NetworkInterceptors.getHttpRequestInterceptor());
}
api = new CustomMwApi(apiURL, httpClient);
- wikidataApi = new CustomMwApi(wikidatApiURL, httpClient);
- this.defaultKvStore = defaultKvStore;
- this.gson = gson;
- }
-
- /**
- * @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 loginToken = getLoginToken();
- Timber.d("Login token is %s", loginToken);
- return getErrorCodeToReturn(api.action("clientlogin")
- .param("rememberMe", "1")
- .param("username", username)
- .param("password", password)
- .param("logintoken", loginToken)
- .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 {
- String loginToken = getLoginToken();
- Timber.d("Login token is %s", loginToken);
- return getErrorCodeToReturn(api.action("clientlogin")
- .param("rememberMe", "true")
- .param("username", username)
- .param("password", password)
- .param("logintoken", loginToken)
- .param("logincontinue", "true")
- .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 loginCustomApiResult CustomApiResult 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(CustomApiResult loginCustomApiResult) {
- String status = loginCustomApiResult.getString("/api/clientlogin/@status");
- if (status.equals("PASS")) {
- api.isLoggedIn = true;
- setAuthCookieOnLogin(true);
- return status;
- } else if (status.equals("FAIL")) {
- setAuthCookieOnLogin(false);
- return loginCustomApiResult.getString("/api/clientlogin/@messagecode");
- } else if (
- status.equals("UI")
- && loginCustomApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
- && loginCustomApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
- ) {
- setAuthCookieOnLogin(false);
- return "2FA";
- }
-
- // UI, REDIRECT, RESTART
- return "genericerror-" + status;
- }
-
- private void setAuthCookieOnLogin(boolean isLoggedIn) {
- if (isLoggedIn) {
- defaultKvStore.putBoolean("isUserLoggedIn", true);
- defaultKvStore.putString("getAuthCookie", api.getAuthCookie());
- } else {
- defaultKvStore.putBoolean("isUserLoggedIn", false);
- defaultKvStore.remove("getAuthCookie");
- }
- }
-
- @Override
- public String getAuthCookie() {
- return api.getAuthCookie();
- }
-
- @Override
- public void setAuthCookie(String authCookie) {
- api.setAuthCookie(authCookie);
- }
-
- @Override
- public boolean validateLogin() throws IOException {
- boolean validateLoginResp = api.validateLogin();
- Timber.d("Validate login response is %s", validateLoginResp);
- return validateLoginResp;
- }
-
- @Override
- public String getEditToken() throws IOException {
- String editToken = api.action("query")
- .param("meta", "tokens")
- .post()
- .getString("/api/query/tokens/@csrftoken");
- Timber.d("MediaWiki edit token is %s", editToken);
- return editToken;
- }
-
- @Override
- public String getCentralAuthToken() throws IOException {
- CustomApiResult result = api.action("centralauthtoken").get();
- String centralAuthToken = result.getString("/api/centralauthtoken/@centralauthtoken");
-
- Timber.d("MediaWiki Central auth token is %s", centralAuthToken);
-
- if ((centralAuthToken == null || centralAuthToken.isEmpty())
- && "notLoggedIn".equals(result.getString("api/error/@code"))) {
- Timber.d("Central auth token isn't valid. Trying to fetch a fresh token");
- api.removeAllCookies();
- String loginResultCode = login(AccountUtil.getUserName(context), AccountUtil.getPassword(context));
- if (loginResultCode.equals("PASS")) {
- return getCentralAuthToken();
- } else if (loginResultCode.equals("2FA")) {
- Timber.e("Cannot refresh session for 2FA enabled user. Login required");
- } else {
- Timber.e("Error occurred in refreshing session. Error code is %s", loginResultCode);
- }
- } else {
- Timber.e("Error occurred while fetching auth token. Error code is %s and message is %s",
- result.getString("api/error/@code"),
- result.getString("api/error/@info"));
- }
- return centralAuthToken;
- }
-
- @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
- public Single pageExists(String pageName) {
- return Single.fromCallable(() -> Double.parseDouble(api.action("query")
- .param("titles", pageName)
- .get()
- .getString("/api/query/pages/page/@_idx")) != -1);
- }
-
- @Override
- public boolean thank(String editToken, long revision) throws IOException {
- CustomApiResult res = api.action("thank")
- .param("rev", revision)
- .param("token", editToken)
- .param("source", CommonsApplication.getInstance().getUserAgent())
- .post();
- String r = res.getString("/api/result/@success");
- // Does this correctly check the success/failure?
- // The docs https://www.mediawiki.org/wiki/Extension:Thanks seems unclear about that.
- return r.equals("success");
- }
-
- @Override
- @Nullable
- public String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException {
- return api.action("edit")
- .param("title", filename)
- .param("token", getEditToken())
- .param("text", processedPageContent)
- .param("summary", summary)
- .post()
- .getString("/api/edit/@result");
- }
-
-
-
- @Override
- @Nullable
- public String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException {
- return api.action("edit")
- .param("title", filename)
- .param("token", getEditToken())
- .param("appendtext", processedPageContent)
- .param("summary", summary)
- .post()
- .getString("/api/edit/@result");
- }
-
- @Override
- @Nullable
- public String prependEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException {
- return api.action("edit")
- .param("title", filename)
- .param("token", getEditToken())
- .param("prependtext", processedPageContent)
- .param("summary", summary)
- .post()
- .getString("/api/edit/@result");
}
@Override
@@ -321,188 +83,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
});
}
- @Override
- @NonNull
- public Observable searchCategories(String filterValue, int searchCatsLimit) {
- List categories = new ArrayList<>();
- return Single.fromCallable(() -> {
- List categoryNodes = null;
- try {
- 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");
- } catch (IOException e) {
- Timber.e(e, "Failed to obtain searchCategories");
- }
-
- if (categoryNodes == null) {
- return new ArrayList();
- }
-
- for (CustomApiResult categoryNode : categoryNodes) {
- String cat = categoryNode.getDocument().getTextContent();
- String catString = cat.replace("Category:", "");
- if (!categories.contains(catString)) {
- categories.add(catString);
- }
- }
-
- return categories;
- }).flatMapObservable(Observable::fromIterable);
- }
-
- @Override
- @NonNull
- public Observable allCategories(String filterValue, int searchCatsLimit) {
- return Single.fromCallable(() -> {
- ArrayList categoryNodes = null;
- try {
- categoryNodes = api.action("query")
- .param("list", "allcategories")
- .param("acprefix", filterValue)
- .param("aclimit", searchCatsLimit)
- .get()
- .getNodes("/api/query/allcategories/c");
- } catch (IOException e) {
- Timber.e(e, "Failed to obtain allCategories");
- }
-
- if (categoryNodes == null) {
- return new ArrayList();
- }
-
- List categories = new ArrayList<>();
- for (CustomApiResult categoryNode : categoryNodes) {
- categories.add(categoryNode.getDocument().getTextContent());
- }
-
- return categories;
- }).flatMapObservable(Observable::fromIterable);
- }
-
- @Override
- public String getWikidataCsrfToken() throws IOException {
- String wikidataCsrfToken = wikidataApi.action("query")
- .param("action", "query")
- .param("centralauthtoken", getCentralAuthToken())
- .param("meta", "tokens")
- .post()
- .getString("/api/query/tokens/@csrftoken");
- Timber.d("Wikidata csrf token is %s", wikidataCsrfToken);
- return wikidataCsrfToken;
- }
-
- /**
- * Creates a new claim using the wikidata API
- * https://www.mediawiki.org/wiki/Wikibase/API
- * @param entityId the wikidata entity to be edited
- * @param property the property to be edited, for eg P18 for images
- * @param snaktype the type of value stored for that property
- * @param value the actual value to be stored for the property, for eg filename in case of P18
- * @return returns revisionId if the claim is successfully created else returns null
- * @throws IOException
- */
- @Nullable
- @Override
- public String wikidataCreateClaim(String entityId, String property, String snaktype, String value) throws IOException {
- Timber.d("Filename is %s", value);
- CustomApiResult result = wikidataApi.action("wbcreateclaim")
- .param("entity", entityId)
- .param("centralauthtoken", getCentralAuthToken())
- .param("token", getWikidataCsrfToken())
- .param("snaktype", snaktype)
- .param("property", property)
- .param("value", value)
- .post();
-
- if (result == null || result.getNode("api") == null) {
- return null;
- }
-
- Node node = result.getNode("api").getDocument();
- Element element = (Element) node;
-
- if (element != null && element.getAttribute("success").equals("1")) {
- return result.getString("api/pageinfo/@lastrevid");
- } else {
- Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info"));
- }
- return null;
- }
-
- /**
- * Adds the wikimedia-commons-app tag to the edits made on wikidata
- * @param revisionId
- * @return
- * @throws IOException
- */
- @Nullable
- @Override
- public boolean addWikidataEditTag(String revisionId) throws IOException {
- CustomApiResult result = wikidataApi.action("tag")
- .param("revid", revisionId)
- .param("centralauthtoken", getCentralAuthToken())
- .param("token", getWikidataCsrfToken())
- .param("add", "wikimedia-commons-app")
- .param("reason", "Add tag for edits made using Android Commons app")
- .post();
-
- if (result == null || result.getNode("api") == null) {
- return false;
- }
-
- if ("success".equals(result.getString("api/tag/result/@status"))) {
- return true;
- } else {
- Timber.e("Error occurred in creating claim. Error code is: %s and message is %s",
- result.getString("api/error/@code"),
- result.getString("api/error/@info"));
- }
- return false;
- }
-
- @Override
- @NonNull
- public Observable searchTitles(String title, int searchCatsLimit) {
- return Single.fromCallable((Callable>) () -> {
- ArrayList categoryNodes;
-
- try {
- 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");
- } catch (IOException e) {
- Timber.e(e, "Failed to obtain searchTitles");
- return Collections.emptyList();
- }
-
- if (categoryNodes == null) {
- return Collections.emptyList();
- }
-
- List titleCategories = new ArrayList<>();
- for (CustomApiResult categoryNode : categoryNodes) {
- String cat = categoryNode.getDocument().getTextContent();
- String catString = cat.replace("Category:", "");
- titleCategories.add(catString);
- }
-
- return titleCategories;
- }).flatMapObservable(Observable::fromIterable);
- }
-
@Override
@NonNull
public LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException {
@@ -551,282 +131,9 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
.getString("/api/query/pages/page/revisions/rev");
}
- @Override
- @NonNull
- public List getNotifications(boolean archived) {
- CustomApiResult notificationNode = null;
- String notfilter;
- try {
- if (archived) {
- notfilter = "read";
- }else {
- notfilter = "!read";
- }
- String language=Locale.getDefault().getLanguage();
- if(StringUtils.isBlank(language)){
- //if no language is set we use the default user language defined on wikipedia
- language="user";
- }
- notificationNode = api.action("query")
- .param("notprop", "list")
- .param("format", "xml")
- .param("meta", "notifications")
- .param("notformat", "model")
- .param("notwikis", "wikidatawiki|commonswiki|enwiki")
- .param("notfilter", notfilter)
- .param("uselang", language)
- .get()
- .getNode("/api/query/notifications/list");
- } catch (IOException e) {
- Timber.e(e, "Failed to obtain searchCategories");
- }
-
- if (notificationNode == null
- || notificationNode.getDocument() == null
- || notificationNode.getDocument().getChildNodes() == null
- || notificationNode.getDocument().getChildNodes().getLength() == 0) {
- return new ArrayList<>();
- }
- NodeList childNodes = notificationNode.getDocument().getChildNodes();
- return NotificationUtils.getNotificationsFromList(context, childNodes);
- }
-
- @Override
- public boolean markNotificationAsRead(Notification notification) throws IOException {
- Timber.d("Trying to mark notification as read: %s", notification.toString());
- String result = api.action("echomarkread")
- .param("token", getEditToken())
- .param("list", notification.notificationId)
- .post()
- .getString("/api/query/echomarkread/@result");
-
- if (StringUtils.isBlank(result)) {
- return false;
- }
-
- return result.equals("success");
- }
-
/**
- * The method takes categoryName as input and returns a List of Subcategories
- * It uses the generator query API to get the subcategories in a category, 500 at a time.
- * Uses the query continue values for fetching paginated responses
- * @param categoryName Category name as defined on commons
- * @return
- */
- @Override
- @NonNull
- public List getSubCategoryList(String categoryName) {
- CustomApiResult apiResult = null;
- try {
- CustomMwApi.RequestBuilder requestBuilder = api.action("query")
- .param("generator", "categorymembers")
- .param("format", "xml")
- .param("gcmtype","subcat")
- .param("gcmtitle", categoryName)
- .param("prop", "info")
- .param("gcmlimit", "500")
- .param("iiprop", "url|extmetadata");
-
- apiResult = requestBuilder.get();
- } catch (IOException e) {
- Timber.e(e, "Failed to obtain searchCategories");
- }
-
- if (apiResult == null) {
- return new ArrayList<>();
- }
-
- CustomApiResult categoryImagesNode = apiResult.getNode("/api/query/pages");
- if (categoryImagesNode == null
- || categoryImagesNode.getDocument() == null
- || categoryImagesNode.getDocument().getChildNodes() == null
- || categoryImagesNode.getDocument().getChildNodes().getLength() == 0) {
- return new ArrayList<>();
- }
-
- NodeList childNodes = categoryImagesNode.getDocument().getChildNodes();
- return CategoryImageUtils.getSubCategoryList(childNodes);
- }
-
- /**
- * The method takes categoryName as input and returns a List of parent categories
- * It uses the generator query API to get the parent categories of a category, 500 at a time.
- * @param categoryName Category name as defined on commons
- * @return
- */
- @Override
- @NonNull
- public List getParentCategoryList(String categoryName) {
- CustomApiResult apiResult = null;
- try {
- CustomMwApi.RequestBuilder requestBuilder = api.action("query")
- .param("generator", "categories")
- .param("format", "xml")
- .param("titles", categoryName)
- .param("prop", "info")
- .param("cllimit", "500")
- .param("iiprop", "url|extmetadata");
-
- apiResult = requestBuilder.get();
- } catch (IOException e) {
- Timber.e(e, "Failed to obtain parent Categories");
- }
-
- if (apiResult == null) {
- return new ArrayList<>();
- }
-
- CustomApiResult categoryImagesNode = apiResult.getNode("/api/query/pages");
- if (categoryImagesNode == null
- || categoryImagesNode.getDocument() == null
- || categoryImagesNode.getDocument().getChildNodes() == null
- || categoryImagesNode.getDocument().getChildNodes().getLength() == 0) {
- return new ArrayList<>();
- }
-
- NodeList childNodes = categoryImagesNode.getDocument().getChildNodes();
- return CategoryImageUtils.getSubCategoryList(childNodes);
- }
-
- /**
- * This method takes search keyword as input and returns a list of categories objects filtered using search query
- * It uses the generator query API to get the categories searched using a query, 25 at a time.
- * @param query keyword to search categories on commons
- * @return
- */
- @Override
- @NonNull
- public List searchCategory(String query, int offset) {
- List categoryNodes = null;
- try {
- categoryNodes = api.action("query")
- .param("format", "xml")
- .param("list", "search")
- .param("srwhat", "text")
- .param("srnamespace", "14")
- .param("srlimit", "25")
- .param("sroffset",offset)
- .param("srsearch", query)
- .get()
- .getNodes("/api/query/search/p/@title");
- } catch (IOException e) {
- Timber.e(e, "Failed to obtain searchCategories");
- }
-
- if (categoryNodes == null) {
- return new ArrayList<>();
- }
-
- List categories = new ArrayList<>();
- for (CustomApiResult categoryNode : categoryNodes) {
- String catName = categoryNode.getDocument().getTextContent();
- categories.add(catName);
- }
- return categories;
- }
-
-
- /**
- * For APIs that return paginated responses, MediaWiki APIs uses the QueryContinue to facilitate fetching of subsequent pages
- * https://www.mediawiki.org/wiki/API:Raw_query_continue
- * After fetching images a page of image for a particular category, shared defaultKvStore are updated with the latest QueryContinue Values
- * @param keyword
- * @param queryContinue
- */
- private void setQueryContinueValues(String keyword, QueryContinue queryContinue) {
- defaultKvStore.putString(keyword, gson.toJson(queryContinue));
- }
-
- /**
- * Before making a paginated API call, this method is called to get the latest query continue values to be used
- * @param keyword
- * @return
- */
- @Nullable
- private QueryContinue getQueryContinueValues(String keyword) {
- String queryContinueString = defaultKvStore.getString(keyword, null);
- return gson.fromJson(queryContinueString, QueryContinue.class);
- }
-
- @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
- @NonNull
- public Single uploadFile(
- String filename,
- @NonNull InputStream file,
- long dataLength,
- Uri fileUri,
- Uri contentProviderUri,
- ProgressListener progressListener) {
- return Single.fromCallable(() -> {
- CustomApiResult result = api.uploadToStash(filename, file, dataLength, getEditToken(), progressListener::onProgress);
-
- Timber.wtf("Result: " + result.toString());
-
- String resultStatus = result.getString("/api/upload/@result");
- if (!resultStatus.equals("Success")) {
- String errorCode = result.getString("/api/error/@code");
- Timber.e(errorCode);
-
- if (errorCode.equals(ERROR_CODE_BAD_TOKEN)) {
- ViewUtil.showLongToast(context, R.string.bad_token_error_proposed_solution);
- }
- return new UploadStash(errorCode, resultStatus, filename, "");
- } else {
- String filekey = result.getString("/api/upload/@filekey");
- return new UploadStash("", resultStatus, filename, filekey);
- }
- });
- }
-
-
- @Override
- @NonNull
- public Single uploadFileFinalize(
- String filename,
- String filekey,
- String pageContents,
- String editSummary) throws IOException {
- return Single.fromCallable(() -> {
- CustomApiResult result = api.uploadFromStash(
- filename, filekey, pageContents, editSummary,
- getEditToken());
-
- Timber.d("Result: %s", result.toString());
-
- String resultStatus = result.getString("/api/upload/@result");
- if (!resultStatus.equals("Success")) {
- String errorCode = result.getString("/api/error/@code");
- Timber.e(errorCode);
-
- if (errorCode.equals(ERROR_CODE_BAD_TOKEN)) {
- ViewUtil.showLongToast(context, R.string.bad_token_error_proposed_solution);
- }
- return new UploadResult(resultStatus, errorCode);
- } else {
- Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
- String canonicalFilename = "File:" + result.getString("/api/upload/@filename")
- .replace("_", " ")
- .trim(); // Title vs Filename
- String imageUrl = result.getString("/api/upload/imageinfo/@url");
- return new UploadResult(resultStatus, dateUploaded, canonicalFilename, imageUrl);
- }
- });
- }
-
- /**
-
* Checks to see if a user is currently blocked from Commons
+ *
* @return whether or not the user is blocked from Commons
*/
@Override
diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java
index e38c4dc0f..a4330d102 100644
--- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java
+++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java
@@ -1,94 +1,22 @@
package fr.free.nrw.commons.mwapi;
-import android.net.Uri;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.List;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import fr.free.nrw.commons.notification.Notification;
-import io.reactivex.Observable;
+
+import java.io.IOException;
+
import io.reactivex.Single;
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;
-
- String getWikidataCsrfToken() throws IOException;
-
- String getCentralAuthToken() throws IOException;
-
- boolean fileExistsWithName(String fileName) throws IOException;
-
- Single pageExists(String pageName);
-
- List getSubCategoryList(String categoryName);
-
- List getParentCategoryList(String categoryName);
-
- @NonNull
- List searchCategory(String title, int offset);
-
- @NonNull
- Single uploadFile(String filename, InputStream file,
- long dataLength, Uri fileUri, Uri contentProviderUri,
- final ProgressListener progressListener);
-
- @NonNull
- Single uploadFileFinalize(String filename, String filekey,
- String pageContents, String editSummary) throws IOException;
- @Nullable
- String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
-
- @Nullable
- String prependEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
-
- @Nullable
- String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
-
- @Nullable
- String wikidataCreateClaim(String entityId, String property, String snaktype, String value) throws IOException;
-
- @Nullable
- boolean addWikidataEditTag(String revisionId) throws IOException;
-
Single parseWikicode(String source);
@NonNull
Single fetchMediaByFilename(String filename);
- @NonNull
- Observable searchCategories(String filterValue, int searchCatsLimit);
-
- @NonNull
- Observable allCategories(String filter, int searchCatsLimit);
-
- @NonNull
- List getNotifications(boolean archived) throws IOException;
-
- @NonNull
- boolean markNotificationAsRead(Notification notification) throws IOException;
-
- @NonNull
- Observable searchTitles(String title, int searchCatsLimit);
-
@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;
@@ -98,8 +26,6 @@ public interface MediaWikiApi {
// Single getCampaigns();
- boolean thank(String editToken, long revision) throws IOException;
-
interface ProgressListener {
void onProgress(long transferred, long total);
}
diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java
index 061351c1f..631452569 100644
--- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java
+++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java
@@ -46,15 +46,11 @@ import timber.log.Timber;
public class OkHttpJsonApiClient {
private static final String THUMB_SIZE = "640";
- public static final Type mapType = new TypeToken