From f0b1d68fbe84cc4a87853e356a22fb48df09e3d4 Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Sun, 7 Jul 2019 14:27:45 +0530 Subject: [PATCH 1/2] Consume login client from data client library (#2894) Fix actions for review client Use data client library for notifications With delete helper migrated to data client With wikidata edits With notifications and modifications migrated to data client With upload migrated to retrofit Delete unused code Reuse thank interface from the library --- app/build.gradle | 5 +- .../free/nrw/commons/CommonsAppAdapter.java | 5 +- .../nrw/commons/actions/PageEditClient.java | 58 +++ .../commons/actions/PageEditInterface.java | 41 ++ .../nrw/commons/actions/ThanksClient.java | 36 ++ .../fr/free/nrw/commons/auth/AccountUtil.java | 7 +- .../free/nrw/commons/auth/LoginActivity.java | 204 +++------ .../free/nrw/commons/auth/SessionManager.java | 93 ++-- .../commons/category/CategoryImageUtils.java | 39 -- .../ContributionsListFragment.java | 22 +- .../commons/contributions/MainActivity.java | 16 +- .../free/nrw/commons/delete/DeleteHelper.java | 85 ++-- .../commons/di/CommonsApplicationModule.java | 23 +- .../free/nrw/commons/di/NetworkingModule.java | 103 ++++- .../ModificationsSyncAdapter.java | 41 +- .../mwapi/ApacheHttpClientMediaWikiApi.java | 426 +----------------- .../free/nrw/commons/mwapi/MediaWikiApi.java | 56 +-- .../free/nrw/commons/mwapi/UploadResult.java | 90 ---- .../free/nrw/commons/mwapi/UploadStash.java | 70 --- .../commons/notification/Notification.java | 29 +- .../notification/NotificationActivity.java | 36 +- .../notification/NotificationClient.java | 45 ++ .../notification/NotificationController.java | 32 +- .../notification/NotificationUtils.java | 293 ------------ .../nrw/commons/review/ReviewController.java | 131 ++---- .../fr/free/nrw/commons/upload/FileUtils.java | 4 +- .../free/nrw/commons/upload/UploadClient.java | 67 +++ .../nrw/commons/upload/UploadInterface.java | 34 ++ .../nrw/commons/upload/UploadResponse.java | 13 + .../free/nrw/commons/upload/UploadResult.java | 39 ++ .../nrw/commons/upload/UploadService.java | 60 +-- .../nrw/commons/wikidata/WikidataClient.java | 40 ++ .../commons/wikidata/WikidataEditService.java | 52 +-- .../nrw/commons/delete/DeleteHelperTest.kt | 72 ++- .../mwapi/ApacheHttpClientMediaWikiApiTest.kt | 178 +------- .../commons/mwapi/OkHttpJsonApiClientTest.kt | 1 + 36 files changed, 881 insertions(+), 1665 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.java create mode 100644 app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.java create mode 100644 app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/UploadStash.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadInterface.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadResponse.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadResult.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataClient.java diff --git a/app/build.gradle b/app/build.gradle index f5e3cb3dc3..20ece90bfc 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' @@ -32,7 +31,7 @@ dependencies { implementation 'com.facebook.fresco:fresco:1.13.0' implementation 'com.drewnoakes:metadata-extractor:2.11.0' implementation 'org.apache.commons:commons-lang3:3.8.1' - implementation 'com.dmitrybrant:wikimedia-android-data-client:0.0.23' + 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/java/fr/free/nrw/commons/CommonsAppAdapter.java b/app/src/main/java/fr/free/nrw/commons/CommonsAppAdapter.java index e9f3422902..24a375246b 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsAppAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsAppAdapter.java @@ -61,7 +61,7 @@ public String getPassword() { @Override public void updateAccount(@NonNull LoginResult result) { - // TODO: sessionManager.updateAccount(result); + sessionManager.updateAccount(result); } @Override @@ -69,7 +69,8 @@ public SharedPreferenceCookieManager getCookies() { if (!preferences.contains(COOKIE_STORE_NAME)) { return null; } - return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class, preferences.getString(COOKIE_STORE_NAME, null)); + return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class, + preferences.getString(COOKIE_STORE_NAME, null)); } @Override 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 0000000000..e2adfb5e68 --- /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 0000000000..537ec4d4f7 --- /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 0000000000..c4f96e7eb0 --- /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 65e211c83e..7616b4515f 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 @@ 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 static String getPassword(Context context) { private static AccountManager accountManager(Context context) { return AccountManager.get(context); } - } 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 8487e9a950..bf1a34b491 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.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.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.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); @@ -214,7 +215,6 @@ protected void onResume() { && sessionManager.isUserLoggedIn() && sessionManager.getCachedAuthCookie() != null) { applicationKvStore.putBoolean("login_skipped", false); - sessionManager.revalidateAuthToken(); startMainActivity(); } @@ -244,7 +244,6 @@ protected void onDestroy() { @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 +251,37 @@ public void performLogin() { 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 +294,6 @@ private void performSkipLogin() { 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 +303,18 @@ private void showLoggingProgressBar() { progressDialog.show(); } - private void handlePassResult(String username, String rawUsername, String password) { - 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); - } + private void onLoginSuccess(String username, String password) { + if (!progressDialog.isShowing()) { + // no longer attached to activity! + return; } - - sessionManager.createAccount(response, username, rawUsername, password); + LoginResult loginResult = new LoginResult(commonsWikiSite, "PASS", username, password, ""); + AppAdapter.get().updateAccount(loginResult); + progressDialog.dismiss(); + showSuccessAndDismissDialog(); 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 MenuInflater getMenuInflater() { 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 void showMessageAndCancelDialog(@StringRes int resId) { } } + 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 @@ private void showMessage(@StringRes int resId, @ColorRes int colorResId) { 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 7462869b1a..882704f0ff 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.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 SessionManager(Context context, 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); - + 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; + } - response.onResult(bundle); + public 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); } + } + } - } else { - if (response != null) { - response.onError(ERROR_CODE_REMOTE_EXCEPTION, ""); - } - Timber.d("account creation failure"); + public void updateAccount(LoginResult result) { + boolean accountCreated = createAccount(result.getUserName(), result.getPassword()); + if (accountCreated) { + setPassword(result.getPassword()); } + } - // 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 setPassword(@NonNull String password) { + Account account = getCurrentAccount(); + if (account != null) { + accountManager().setPassword(account, password); + } } /** @@ -127,25 +121,6 @@ private AccountManager accountManager() { 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"); 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 cd97511cd3..0000000000 --- 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/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index 4c6d54641d..dfc69524eb 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.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 void showWelcomeTip(boolean shouldShow) { /** * 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 void showProgress(boolean shouldShow) { } 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 56946018c4..6cf85ed98e 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 @@ -12,18 +12,19 @@ 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; @@ -38,7 +39,6 @@ import fr.free.nrw.commons.notification.NotificationController; import fr.free.nrw.commons.quiz.QuizChecker; 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; @@ -305,7 +305,7 @@ public boolean onCreateOptionsMenu(Menu menu) { @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/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java index a5d7e7ef73..3cf6c6f95e 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.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 @@ */ @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 DeleteHelper(MediaWikiApi mwApi, * @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 Single makeDeletion(Context context, Media media, String reason) * @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 @@ private boolean delete(Media media, String reason) { 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 void askReasonAndExecute(Media media, } } - 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/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index 36aba16682..52dfdbd893 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -5,14 +5,11 @@ import android.content.Context; import android.view.inputmethod.InputMethodManager; +import androidx.collection.LruCache; + import com.google.gson.Gson; -import org.wikipedia.dataclient.WikiSite; - -import io.reactivex.Scheduler; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import org.wikipedia.dataclient.WikiSite; +import org.wikipedia.AppAdapter; import java.util.ArrayList; import java.util.HashMap; @@ -22,7 +19,6 @@ import javax.inject.Named; import javax.inject.Singleton; -import androidx.collection.LruCache; import dagger.Module; import dagger.Provides; import fr.free.nrw.commons.BuildConfig; @@ -37,6 +33,9 @@ import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.wikidata.WikidataEditListener; import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl; +import io.reactivex.Scheduler; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; @Module @SuppressWarnings({"WeakerAccess", "unused"}) @@ -85,7 +84,7 @@ public Map provideLicensesByName(Context context) { @Provides public AccountUtil providesAccountUtil(Context context) { - return new AccountUtil(context); + return new AccountUtil(); } @Provides @@ -188,7 +187,13 @@ public Scheduler providesIoThread(){ @Named(MAIN_THREAD) @Provides - public Scheduler providesMainThread(){ + public Scheduler providesMainThread() { return AndroidSchedulers.mainThread(); } + + @Named("username") + @Provides + public String provideLoggedInUsername() { + return AppAdapter.get().getUserName(); + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java index 0286d96407..dafcab3be5 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java @@ -2,11 +2,16 @@ import android.content.Context; +import androidx.annotation.NonNull; + import com.google.gson.Gson; +import org.wikipedia.csrf.CsrfTokenClient; +import org.wikipedia.dataclient.Service; import org.wikipedia.dataclient.ServiceFactory; import org.wikipedia.dataclient.WikiSite; import org.wikipedia.json.GsonUtil; +import org.wikipedia.login.LoginClient; import java.io.File; import java.util.concurrent.TimeUnit; @@ -14,10 +19,11 @@ import javax.inject.Named; import javax.inject.Singleton; -import androidx.annotation.NonNull; import dagger.Module; import dagger.Provides; import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.actions.PageEditClient; +import fr.free.nrw.commons.actions.PageEditInterface; import fr.free.nrw.commons.category.CategoryInterface; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.media.MediaInterface; @@ -25,6 +31,7 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; import fr.free.nrw.commons.review.ReviewInterface; +import fr.free.nrw.commons.upload.UploadInterface; import okhttp3.Cache; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; @@ -41,7 +48,11 @@ public class NetworkingModule { public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024; - private static final String NAMED_COMMONS_WIKI_SITE = "commons-wikisite"; + public static final String NAMED_COMMONS_WIKI_SITE = "commons-wikisite"; + private static final String NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite"; + + public static final String NAMED_COMMONS_CSRF = "commons-csrf"; + public static final String NAMED_WIKI_DATA_CSRF = "wikidata-csrf"; @Provides @Singleton @@ -71,7 +82,7 @@ public HttpLoggingInterceptor provideHttpLoggingInterceptor() { public MediaWikiApi provideMediaWikiApi(Context context, @Named("default_preferences") JsonKvStore defaultKvStore, Gson gson) { - return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultKvStore); + return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST); } @Provides @@ -88,6 +99,27 @@ public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient, gson); } + @Named(NAMED_COMMONS_CSRF) + @Provides + @Singleton + public CsrfTokenClient provideCommonsCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { + return new CsrfTokenClient(commonsWikiSite, commonsWikiSite); + } + + @Named(NAMED_WIKI_DATA_CSRF) + @Provides + @Singleton + public CsrfTokenClient provideWikidataCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite, + @Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) { + return new CsrfTokenClient(wikidataWikiSite, commonsWikiSite); + } + + @Provides + @Singleton + public LoginClient provideLoginClient() { + return new LoginClient(); + } + @Provides @Named("wikimedia_api_host") @NonNull @@ -104,6 +136,20 @@ public HttpUrl provideToolsForgeUrl() { return HttpUrl.parse(TOOLS_FORGE_URL); } + @Provides + @Singleton + @Named(NAMED_COMMONS_WIKI_SITE) + public WikiSite provideCommonsWikiSite() { + return new WikiSite(BuildConfig.COMMONS_URL); + } + + @Provides + @Singleton + @Named(NAMED_WIKI_DATA_WIKI_SITE) + public WikiSite provideWikidataWikiSite() { + return new WikiSite(BuildConfig.WIKIDATA_URL); + } + /** * Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere. * @return returns a singleton Gson instance @@ -116,9 +162,16 @@ public Gson provideGson() { @Provides @Singleton - @Named("commons-wikisite") - public WikiSite provideCommonsWikiSite() { - return new WikiSite(BuildConfig.COMMONS_URL); + @Named("commons-service") + public Service provideCommonsService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { + return ServiceFactory.get(commonsWikiSite); + } + + @Provides + @Singleton + @Named("wikidata-service") + public Service provideWikidataService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) { + return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, Service.class); } @Provides @@ -127,6 +180,44 @@ public ReviewInterface provideReviewInterface(@Named(NAMED_COMMONS_WIKI_SITE) Wi return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class); } + @Provides + @Singleton + public UploadInterface provideUploadInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { + return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, UploadInterface.class); + } + + @Named("commons-page-edit-service") + @Provides + @Singleton + public PageEditInterface providePageEditService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { + return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, PageEditInterface.class); + } + + @Named("wikidata-page-edit-service") + @Provides + @Singleton + public PageEditInterface provideWikiDataPageEditService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikiDataWikiSite) { + return ServiceFactory.get(wikiDataWikiSite, BuildConfig.WIKIDATA_URL, PageEditInterface.class); + } + + @Named("commons-page-edit") + @Provides + @Singleton + public PageEditClient provideCommonsPageEditClient(@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient, + @Named("commons-page-edit-service") PageEditInterface pageEditInterface, + @Named("commons-service") Service service) { + return new PageEditClient(csrfTokenClient, pageEditInterface, service); + } + + @Named("wikidata-page-edit") + @Provides + @Singleton + public PageEditClient provideWikidataPageEditClient(@Named(NAMED_WIKI_DATA_CSRF) CsrfTokenClient csrfTokenClient, + @Named("wikidata-page-edit-service") PageEditInterface pageEditInterface, + @Named("wikidata-service") Service service) { + return new PageEditClient(csrfTokenClient, pageEditInterface, service); + } + @Provides @Singleton public MediaInterface provideMediaInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java index 6043f6d03d..6f8b4522c7 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java @@ -12,13 +12,17 @@ import java.io.IOException; import javax.inject.Inject; +import javax.inject.Named; import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.actions.PageEditClient; 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 io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; import timber.log.Timber; public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { @@ -28,6 +32,9 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { @Inject ModifierSequenceDao modifierSequenceDao; @Inject SessionManager sessionManager; + @Inject + @Named("commons-page-edit") + PageEditClient commonsPageEditClient; public ModificationsSyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); @@ -61,15 +68,6 @@ public void onPerformSync(Account account, Bundle bundle, String s, ContentProvi return; } - mwApi.setAuthCookie(authCookie); - String editToken; - - try { - editToken = mwApi.getEditToken(); - } catch (IOException e) { - Timber.d("Can not retreive edit token!"); - return; - } allModifications.moveToFirst(); @@ -113,22 +111,15 @@ public void onPerformSync(Account account, Bundle bundle, String s, ContentProvi 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); - } + Disposable disposable = commonsPageEditClient + .edit(contrib.getFilename(), processedPageContent, sequence.getEditSummary()) + .subscribe(editResult -> { + if (!editResult) { + Timber.d("Non success result!"); + } else { + modifierSequenceDao.delete(sequence); + } + }); } allModifications.moveToNext(); } 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 0ef5181806..b88764e935 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,15 +1,10 @@ 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; @@ -20,28 +15,15 @@ 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.Date; -import java.util.List; -import java.util.Locale; 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.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.Single; import timber.log.Timber; @@ -51,17 +33,8 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { private AbstractHttpClient httpClient; private CustomMwApi api; - private CustomMwApi wikidataApi; - private Context context; - private JsonKvStore defaultKvStore; - - private final String ERROR_CODE_BAD_TOKEN = "badtoken"; - public ApacheHttpClientMediaWikiApi(Context context, - String apiURL, - String wikidatApiURL, - JsonKvStore defaultKvStore) { - this.context = context; + public ApacheHttpClientMediaWikiApi(String apiURL) { BasicHttpParams params = new BasicHttpParams(); SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); @@ -74,198 +47,6 @@ public ApacheHttpClientMediaWikiApi(Context context, httpClient.addRequestInterceptor(NetworkInterceptors.getHttpRequestInterceptor()); } api = new CustomMwApi(apiURL, httpClient); - wikidataApi = new CustomMwApi(wikidatApiURL, httpClient); - this.defaultKvStore = defaultKvStore; - } - - /** - * @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 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 @@ -297,89 +78,6 @@ public Single fetchMediaByFilename(String filename) { }); } - @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 LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException { @@ -428,128 +126,6 @@ public String revisionsByFilename(String filename) throws IOException { .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"); - } - - - @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 * 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 d3072f46ef..a4330d1027 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,69 +1,19 @@ package fr.free.nrw.commons.mwapi; -import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; 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 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; - - @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 - List getNotifications(boolean archived) throws IOException; - - @NonNull - boolean markNotificationAsRead(Notification notification) throws IOException; - @Nullable String revisionsByFilename(String filename) throws IOException; @@ -76,8 +26,6 @@ Single uploadFileFinalize(String filename, String filekey, // 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/UploadResult.java b/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java deleted file mode 100644 index 98a476ffac..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java +++ /dev/null @@ -1,90 +0,0 @@ -package fr.free.nrw.commons.mwapi; - -import org.jetbrains.annotations.NotNull; - -import java.util.Date; - -public class UploadResult { - private String errorCode; - private String resultStatus; - private Date dateUploaded; - private String imageUrl; - private String canonicalFilename; - - /** - * Minimal constructor - * - * @param resultStatus Upload result status - * @param errorCode Upload error code - */ - UploadResult(String resultStatus, String errorCode) { - this.resultStatus = resultStatus; - this.errorCode = errorCode; - } - - /** - * Full-fledged constructor - * @param resultStatus Upload result status - * @param dateUploaded Uploaded date - * @param canonicalFilename Uploaded file name - * @param imageUrl Uploaded image file name - */ - UploadResult(String resultStatus, Date dateUploaded, String canonicalFilename, String imageUrl) { - this.resultStatus = resultStatus; - this.dateUploaded = dateUploaded; - this.canonicalFilename = canonicalFilename; - this.imageUrl = imageUrl; - } - - @NotNull - @Override - public String toString() { - return "UploadResult{" + - "errorCode='" + errorCode + '\'' + - ", resultStatus='" + resultStatus + '\'' + - ", dateUploaded='" + (dateUploaded == null ? "" : dateUploaded.toString()) + '\'' + - ", imageUrl='" + imageUrl + '\'' + - ", canonicalFilename='" + canonicalFilename + '\'' + - '}'; - } - - /** - * Gets uploaded date - * @return Upload date - */ - public Date getDateUploaded() { - return dateUploaded; - } - - /** - * Gets image url - * @return Uploaded image url - */ - public String getImageUrl() { - return imageUrl; - } - - /** - * Gets canonical file name - * @return Uploaded file name - */ - public String getCanonicalFilename() { - return canonicalFilename; - } - - /** - * Gets upload error code - * @return Error code - */ - public String getErrorCode() { - return errorCode; - } - - /** - * Gets upload result status - * @return Upload result status - */ - public String getResultStatus() { - return resultStatus; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/UploadStash.java b/app/src/main/java/fr/free/nrw/commons/mwapi/UploadStash.java deleted file mode 100644 index f74b672988..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/UploadStash.java +++ /dev/null @@ -1,70 +0,0 @@ -package fr.free.nrw.commons.mwapi; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public class UploadStash { - @NonNull - private String errorCode; - @NonNull - private String resultStatus; - @NonNull - private String filename; - @NonNull - private String filekey; - - @NonNull - public final String getErrorCode() { - return this.errorCode; - } - - @NonNull - public final String getResultStatus() { - return this.resultStatus; - } - - @NonNull - public final String getFilename() { - return this.filename; - } - - @NonNull - public final String getFilekey() { - return this.filekey; - } - - public UploadStash(@NonNull String errorCode, @NonNull String resultStatus, @NonNull String filename, @NonNull String filekey) { - this.errorCode = errorCode; - this.resultStatus = resultStatus; - this.filename = filename; - this.filekey = filekey; - } - - public String toString() { - return "UploadStash(errorCode=" + this.errorCode + ", resultStatus=" + this.resultStatus + ", filename=" + this.filename + ", filekey=" + this.filekey + ")"; - } - - public int hashCode() { - return ((this.errorCode.hashCode() * 31 + this.resultStatus.hashCode() - ) * 31 + this.filename.hashCode() - ) * 31 + this.filekey.hashCode(); - } - - public boolean equals(@Nullable Object obj) { - if (this != obj) { - if (obj instanceof UploadStash) { - UploadStash that = (UploadStash)obj; - if (this.errorCode.equals(that.errorCode) - && this.resultStatus.equals(that.resultStatus) - && this.filename.equals(that.filename) - && this.filekey.equals(that.filekey)) { - return true; - } - } - - return false; - } else { - return true; - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/Notification.java b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java index fec22f34d5..8052cfaa74 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/Notification.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java @@ -1,5 +1,9 @@ package fr.free.nrw.commons.notification; +import org.wikipedia.util.DateUtil; + +import fr.free.nrw.commons.utils.CommonsDateUtil; + /** * Created by root on 18.12.2017. */ @@ -8,33 +12,44 @@ public class Notification { public NotificationType notificationType; public String notificationText; public String date; - public String description; public String link; public String iconUrl; - public String dateWithYear; public String notificationId; - public Notification(NotificationType notificationType, String notificationText, String date, String description, String link, String iconUrl, String dateWithYear, String notificationId) { + public Notification(NotificationType notificationType, + String notificationText, + String date, + String link, + String iconUrl, + String notificationId) { this.notificationType = notificationType; this.notificationText = notificationText; this.date = date; - this.description = description; this.link = link; this.iconUrl = iconUrl; - this.dateWithYear = dateWithYear; this.notificationId=notificationId; } + public static Notification from(org.wikipedia.notifications.Notification wikiNotification) { + org.wikipedia.notifications.Notification.Contents contents = wikiNotification.getContents(); + String notificationLink = contents == null || contents.getLinks() == null + || contents.getLinks().getPrimary() == null ? "" : contents.getLinks().getPrimary().getUrl(); + return new Notification(NotificationType.UNKNOWN, + contents == null ? "" : contents.getCompactHeader(), + DateUtil.getMonthOnlyDateString(wikiNotification.getTimestamp()), + notificationLink, + "", + String.valueOf(wikiNotification.id())); + } + @Override public String toString() { return "Notification" + "notificationType='" + notificationType + '\'' + ", notificationText='" + notificationText + '\'' + ", date='" + date + '\'' + - ", description='" + description + '\'' + ", link='" + link + '\'' + ", iconUrl='" + iconUrl + '\'' + - ", dateWithYear=" + dateWithYear + ", notificationId='" + notificationId + '\'' + '}'; } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java index b1f775a5b3..bb2078d247 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -5,11 +5,6 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import androidx.constraintlayout.widget.ConstraintLayout; -import com.google.android.material.snackbar.Snackbar; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -19,10 +14,17 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.snackbar.Snackbar; import com.pedrogomez.renderers.RVRendererAdapter; import java.util.Collections; import java.util.List; +import java.util.concurrent.Callable; import javax.inject.Inject; @@ -34,7 +36,9 @@ import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; +import io.reactivex.ObservableSource; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; @@ -78,25 +82,25 @@ protected void onCreate(Bundle savedInstanceState) { @SuppressLint("CheckResult") public void removeNotification(Notification notification) { - compositeDisposable.add(Observable.fromCallable(() -> controller.markAsRead(notification)) + Disposable disposable = Observable.defer((Callable>) + () -> controller.markAsRead(notification)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { - if (result){ + if (result) { notificationList.remove(notification); setAdapter(notificationList); adapter.notifyDataSetChanged(); Snackbar snackbar = Snackbar - .make(relativeLayout,"Notification marked as read", Snackbar.LENGTH_LONG); + .make(relativeLayout, "Notification marked as read", Snackbar.LENGTH_LONG); snackbar.show(); - if (notificationList.size()==0){ + if (notificationList.size() == 0) { setEmptyView(); relativeLayout.setVisibility(View.GONE); no_notification.setVisibility(View.VISIBLE); } - } - else { + } else { adapter.notifyDataSetChanged(); setAdapter(notificationList); Toast.makeText(NotificationActivity.this, "There was some error!", Toast.LENGTH_SHORT).show(); @@ -107,7 +111,8 @@ public void removeNotification(Notification notification) { throwable.printStackTrace(); ViewUtil.showShortSnackbar(relativeLayout, R.string.error_notifications); progressBar.setVisibility(View.GONE); - })); + }); + compositeDisposable.add(disposable); } @@ -140,11 +145,8 @@ private void refresh(boolean archived) { private void addNotifications(boolean archived) { Timber.d("Add notifications"); if (mNotificationWorkerFragment == null) { - compositeDisposable.add(Observable.fromCallable(() -> { - progressBar.setVisibility(View.VISIBLE); - return controller.getNotifications(archived); - - }) + progressBar.setVisibility(View.VISIBLE); + compositeDisposable.add(controller.getNotifications(archived) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(notificationList -> { diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.java new file mode 100644 index 0000000000..8643ceaf47 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.java @@ -0,0 +1,45 @@ +package fr.free.nrw.commons.notification; + +import org.wikipedia.csrf.CsrfTokenClient; +import org.wikipedia.dataclient.Service; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import io.reactivex.Observable; +import io.reactivex.Single; + +import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF; + +@Singleton +public class NotificationClient { + + private final Service service; + private final CsrfTokenClient csrfTokenClient; + + @Inject + public NotificationClient(@Named("commons-service") Service service, @Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) { + this.service = service; + this.csrfTokenClient = csrfTokenClient; + } + + public Single> getNotifications(boolean archived) { + return service.getAllNotifications("wikidatawiki|commonswiki|enwiki", archived ? "read" : "!read", null) + .map(mwQueryResponse -> mwQueryResponse.query().notifications().list()) + .flatMap(Observable::fromIterable) + .map(notification -> Notification.from(notification)) + .toList(); + } + + public Observable markNotificationAsRead(String notificationId) { + try { + return service.markRead(csrfTokenClient.getTokenBlocking(), notificationId, "") + .map(mwQueryResponse -> mwQueryResponse.success()); + } catch (Throwable throwable) { + return Observable.just(false); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java index 112735127d..5d0ac010b7 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java @@ -1,14 +1,12 @@ package fr.free.nrw.commons.notification; -import java.io.IOException; -import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.MediaWikiApi; +import io.reactivex.Observable; +import io.reactivex.Single; /** * Created by root on 19.12.2017. @@ -16,27 +14,19 @@ @Singleton public class NotificationController { - private MediaWikiApi mediaWikiApi; - private SessionManager sessionManager; + private NotificationClient notificationClient; + @Inject - public NotificationController(MediaWikiApi mediaWikiApi, SessionManager sessionManager) { - this.mediaWikiApi = mediaWikiApi; - this.sessionManager = sessionManager; + public NotificationController(NotificationClient notificationClient) { + this.notificationClient = notificationClient; } - public List getNotifications(boolean archived) throws IOException { - if (mediaWikiApi.validateLogin()) { - return mediaWikiApi.getNotifications(archived); - } else { - Boolean authTokenValidated = sessionManager.revalidateAuthToken(); - if (authTokenValidated != null && authTokenValidated) { - return mediaWikiApi.getNotifications(archived); - } - } - return new ArrayList<>(); + public Single> getNotifications(boolean archived) { + return notificationClient.getNotifications(archived); } - public boolean markAsRead(Notification notification) throws IOException{ - return mediaWikiApi.markNotificationAsRead(notification); + + Observable markAsRead(Notification notification) { + return notificationClient.markNotificationAsRead(notification.notificationId); } } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java deleted file mode 100644 index 1c92103cd2..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java +++ /dev/null @@ -1,293 +0,0 @@ -package fr.free.nrw.commons.notification; - -import android.annotation.SuppressLint; -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.ArrayList; -import java.util.List; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.R; - -import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN; - -public class NotificationUtils { - - private static final String COMMONS_WIKI = "commonswiki"; - private static final String WIKIDATA_WIKI = "wikidatawiki"; - private static final String WIKIPEDIA_WIKI = "enwiki"; - - /** - * Returns true if the wiki attribute corresponds to commonswiki - * @param document - * @return boolean representing whether the wiki attribute corresponds to commonswiki - */ - public static boolean isCommonsNotification(Node document) { - if (document == null || !document.hasAttributes()) { - return false; - } - Element element = (Element) document; - return COMMONS_WIKI.equals(element.getAttribute("wiki")); - } - - /** - * Returns true if the wiki attribute corresponds to wikidatawiki - * @param document - * @return boolean representing whether the wiki attribute corresponds to wikidatawiki - */ - public static boolean isWikidataNotification(Node document) { - if (document == null || !document.hasAttributes()) { - return false; - } - Element element = (Element) document; - return WIKIDATA_WIKI.equals(element.getAttribute("wiki")); - } - - /** - * Returns true if the wiki attribute corresponds to enwiki - * @param document - * @return - */ - public static boolean isWikipediaNotification(Node document) { - if (document == null || !document.hasAttributes()) { - return false; - } - Element element = (Element) document; - return WIKIPEDIA_WIKI.equals(element.getAttribute("wiki")); - } - - /** - * Returns document notification type - * @param document - * @return the document's NotificationType - */ - public static NotificationType getNotificationType(Node document) { - Element element = (Element) document; - String type = element.getAttribute("type"); - return NotificationType.handledValueOf(type); - } - - public static String getNotificationId(Node document) { - Element element = (Element) document; - return element.getAttribute("id"); - } - - public static List getNotificationsFromBundle(Context context, Node document) { - Element bundledNotifications = getBundledNotifications(document); - NodeList childNodes = bundledNotifications.getChildNodes(); - - List notifications = new ArrayList<>(); - for (int i = 0; i < childNodes.getLength(); i++) { - Node node = childNodes.item(i); - if (isUsefulNotification(node)) { - notifications.add(getNotificationFromApiResult(context, node)); - } - } - return notifications; - } - - @NonNull - public static List getNotificationsFromList(Context context, NodeList childNodes) { - List notifications = new ArrayList<>(); - for (int i = 0; i < childNodes.getLength(); i++) { - Node node = childNodes.item(i); - if (isUsefulNotification(node)) { - if (isBundledNotification(node)) { - notifications.addAll(getNotificationsFromBundle(context, node)); - } else { - notifications.add(getNotificationFromApiResult(context, node)); - } - } - } - - return notifications; - } - - /** - * Currently the app is interested in showing notifications just from the following three wikis: commons, wikidata, wikipedia - * This function returns true only if the notification belongs to any of the above wikis and is of a known notification type - * @param node - * @return whether a notification is from one of Commons, Wikidata or Wikipedia - */ - private static boolean isUsefulNotification(Node node) { - return (isCommonsNotification(node) - || isWikidataNotification(node) - || isWikipediaNotification(node)) - && !getNotificationType(node).equals(UNKNOWN); - } - - public static boolean isBundledNotification(Node document) { - Element bundleElement = getBundledNotifications(document); - if (bundleElement == null) { - return false; - } - - return bundleElement.getChildNodes().getLength() > 0; - } - - private static Element getBundledNotifications(Node document) { - return (Element) getNode(document, "bundledNotifications"); - } - - public static Notification getNotificationFromApiResult(Context context, Node document) { - NotificationType type = getNotificationType(document); - - String notificationText = ""; - String link = getPrimaryLink(document); - String description = getNotificationDescription(document); - String iconUrl = getNotificationIconUrl(document); - - switch (type) { - case THANK_YOU_EDIT: - notificationText = getThankYouEditDescription(document); - break; - case EDIT_USER_TALK: - notificationText = getNotificationText(document); - break; - case MENTION: - notificationText = getMentionMessage(context, document); - description = getMentionDescription(document); - break; - case WELCOME: - notificationText = getWelcomeMessage(context, document); - break; - } - return new Notification(type, notificationText, getTimestamp(document), description, link, iconUrl, getTimestampWithYear(document), - getNotificationId(document)); - } - - private static String getNotificationText(Node document) { - String notificationBody = getNotificationBody(document); - if (notificationBody == null || notificationBody.trim().equals("")) { - return getNotificationHeader(document); - } - return notificationBody; - } - - private static String getNotificationHeader(Node document) { - Node body = getNode(getModel(document), "header"); - if (body != null) { - String textContent = body.getTextContent(); - return textContent.replace("", "").replace("", ""); - } else { - return ""; - } - } - - private static String getNotificationBody(Node document) { - Node body = getNode(getModel(document), "body"); - if (body != null) { - String textContent = body.getTextContent(); - return textContent.replace("", "").replace("", ""); - } else { - return ""; - } - } - - private static String getMentionDescription(Node document) { - Node body = getNode(getModel(document), "body"); - return body != null ? body.getTextContent() : ""; - } - - /** - * Gets the header node returned in the XML document to form the description for thank you edits - * @param document - * @return - */ - private static String getThankYouEditDescription(Node document) { - Node body = getNode(getModel(document), "header"); - return body != null ? body.getTextContent() : ""; - } - - private static String getNotificationIconUrl(Node document) { - String format = "%s%s"; - Node iconUrl = getNode(getModel(document), "iconUrl"); - if (iconUrl == null) { - return null; - } else { - String url = iconUrl.getTextContent(); - return String.format(format, BuildConfig.COMMONS_URL, url); - } - } - - public static String getMentionMessage(Context context, Node document) { - String format = context.getString(R.string.notifications_mention); - return String.format(format, getAgent(document), getNotificationDescription(document)); - } - - @SuppressLint("StringFormatMatches") - public static String getUserTalkMessage(Context context, Node document) { - String format = context.getString(R.string.notifications_talk_page_message); - return String.format(format, getAgent(document)); - } - - @SuppressLint("StringFormatInvalid") - public static String getWelcomeMessage(Context context, Node document) { - String welcomeMessageFormat = context.getString(R.string.notifications_welcome); - return String.format(welcomeMessageFormat, getAgent(document)); - } - - private static String getPrimaryLink(Node document) { - Node links = getNode(getModel(document), "links"); - Element primaryLink = (Element) getNode(links, "primary"); - if (primaryLink != null) { - return primaryLink.getAttribute("url"); - } - return ""; - } - - private static Node getModel(Node document) { - return getNode(document, "_.2A."); - } - - private static String getAgent(Node document) { - Element agentElement = (Element) getNode(document, "agent"); - if (agentElement != null) { - return agentElement.getAttribute("name"); - } - return ""; - } - - private static String getTimestamp(Node document) { - Element timestampElement = (Element) getNode(document, "timestamp"); - if (timestampElement != null) { - return timestampElement.getAttribute("date"); - } - return ""; - } - - private static String getTimestampWithYear(Node document) { - Element timestampElement = (Element) getNode(document, "timestamp"); - if (timestampElement != null) { - return timestampElement.getAttribute("utcunix"); - } - return ""; - } - - private static String getNotificationDescription(Node document) { - Element titleElement = (Element) getNode(document, "title"); - if (titleElement != null) { - return titleElement.getAttribute("text"); - } - return ""; - } - - @Nullable - public static Node getNode(Node node, String nodeName) { - NodeList childNodes = node.getChildNodes(); - for (int i = 0; i < childNodes.getLength(); i++) { - Node nodeItem = childNodes.item(i); - Element item = (Element) nodeItem; - if (item.getTagName().equals(nodeName)) { - return nodeItem; - } - } - return null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java index c98c7ed3e8..1aaeb6a819 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java @@ -4,8 +4,6 @@ import android.app.Activity; import android.app.NotificationManager; import android.content.Context; -import android.view.Gravity; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -13,17 +11,23 @@ import org.wikipedia.dataclient.mwapi.MwQueryPage; +import java.util.ArrayList; +import java.util.concurrent.Callable; + import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Singleton; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.actions.PageEditClient; +import fr.free.nrw.commons.actions.ThanksClient; import fr.free.nrw.commons.delete.DeleteHelper; import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; +import io.reactivex.ObservableSource; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; @@ -32,13 +36,15 @@ public class ReviewController { private static final int NOTIFICATION_SEND_THANK = 0x102; private static final int NOTIFICATION_CHECK_CATEGORY = 0x101; + protected static ArrayList categories; + @Inject + ThanksClient thanksClient; private final DeleteHelper deleteHelper; @Nullable MwQueryPage.Revision firstRevision; // TODO: maybe we can expand this class to include fileName @Inject - MediaWikiApi mwApi; - @Inject - SessionManager sessionManager; + @Named("commons-page-edit") + PageEditClient pageEditClient; private NotificationManager notificationManager; private NotificationCompat.Builder notificationBuilder; private Media media; @@ -89,40 +95,16 @@ void reportWrongCategory(@NonNull Activity activity, ReviewCallback reviewCallba .getCommonsApplicationComponent() .inject(this); - Toast toast = new Toast(context); - toast.setGravity(Gravity.CENTER, 0, 0); - toast = Toast.makeText(context, context.getString(R.string.check_category_toast, media.getDisplayTitle()), Toast.LENGTH_SHORT); - toast.show(); - - Observable.fromCallable(() -> { - publishProgress(context, 0); - - String editToken; - String authCookie; - String summary = context.getString(R.string.check_category_edit_summary); - - authCookie = sessionManager.getAuthCookie(); - mwApi.setAuthCookie(authCookie); - - try { - editToken = mwApi.getEditToken(); - - if (editToken == null) { - return false; - } - publishProgress(context, 1); - - mwApi.appendEdit(editToken, "\n{{subst:chc}}\n", media.getFilename(), summary); - publishProgress(context, 2); - } catch (Exception e) { - Timber.d(e); - return false; - } - return true; - }) + ViewUtil.showShortToast(context, context.getString(R.string.check_category_toast, media.getDisplayTitle())); + + publishProgress(context, 0); + String summary = context.getString(R.string.check_category_edit_summary); + Observable.defer((Callable>) () -> + pageEditClient.appendEdit(media.getFilename(), "\n{{subst:chc}}\n", summary)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe((result) -> { + publishProgress(context, 2); String message; String title; @@ -136,15 +118,7 @@ void reportWrongCategory(@NonNull Activity activity, ReviewCallback reviewCallba reviewCallback.onFailure(); } - notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) - .setContentTitle(title) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(message)) - .setSmallIcon(R.drawable.ic_launcher) - .setProgress(0, 0, false) - .setOngoing(false) - .setPriority(NotificationCompat.PRIORITY_HIGH); - notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build()); + showNotification(title, message); }, Timber::e); } @@ -172,39 +146,20 @@ void sendThanks(@NonNull Activity activity) { .getInstance(context) .getCommonsApplicationComponent() .inject(this); - Toast toast = new Toast(context); - toast.setGravity(Gravity.CENTER, 0, 0); - toast = Toast.makeText(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle()), Toast.LENGTH_SHORT); - toast.show(); - - Observable.fromCallable(() -> { - publishProgress(context, 0); - - String editToken; - String authCookie; - authCookie = sessionManager.getAuthCookie(); - mwApi.setAuthCookie(authCookie); - - try { - editToken = mwApi.getEditToken(); - if (editToken == null) { - return false; - } - publishProgress(context, 1); - assert firstRevision != null; - mwApi.thank(editToken, firstRevision.getRevisionId()); - publishProgress(context, 2); - } catch (Exception e) { - Timber.d(e); - return false; - } - return true; - }) + ViewUtil.showShortToast(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle())); + + publishProgress(context, 0); + if (firstRevision == null) { + return; + } + + Observable.defer((Callable>) () -> thanksClient.thank(firstRevision.getRevisionId())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe((result) -> { - String message = ""; - String title = ""; + publishProgress(context, 2); + String message; + String title; if (result) { title = context.getString(R.string.send_thank_success_title); message = context.getString(R.string.send_thank_success_message, media.getDisplayTitle()); @@ -213,19 +168,23 @@ void sendThanks(@NonNull Activity activity) { message = context.getString(R.string.send_thank_failure_message, media.getDisplayTitle()); } - notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) - .setContentTitle(title) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(message)) - .setSmallIcon(R.drawable.ic_launcher) - .setProgress(0, 0, false) - .setOngoing(false) - .setPriority(NotificationCompat.PRIORITY_HIGH); - notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build()); + showNotification(title, message); }, Timber::e); } + private void showNotification(String title, String message) { + notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) + .setContentTitle(title) + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(0, 0, false) + .setOngoing(false) + .setPriority(NotificationCompat.PRIORITY_HIGH); + notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build()); + } + public interface ReviewCallback { void onSuccess(); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index 9e4c9d3340..1770bb3e2f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -140,7 +140,7 @@ public static String getMimeType(Context context, Uri uri) { return mimeType; } - public static String getFileExt(String fileName) { + static String getFileExt(String fileName) { //Default filePath extension String extension = ".jpg"; @@ -151,7 +151,7 @@ public static String getFileExt(String fileName) { return extension; } - public static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException { + static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException { return new FileInputStream(filePath); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java new file mode 100644 index 0000000000..87f1b69ed9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java @@ -0,0 +1,67 @@ +package fr.free.nrw.commons.upload; + +import android.content.Context; +import android.net.Uri; + +import org.wikipedia.csrf.CsrfTokenClient; + +import java.io.File; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import fr.free.nrw.commons.contributions.Contribution; +import io.reactivex.Observable; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF; + +@Singleton +public class UploadClient { + + private final UploadInterface uploadInterface; + private final CsrfTokenClient csrfTokenClient; + + @Inject + public UploadClient(UploadInterface uploadInterface, @Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) { + this.uploadInterface = uploadInterface; + this.csrfTokenClient = csrfTokenClient; + } + + Observable uploadFileToStash(Context context, String filename, File file) { + RequestBody requestFile = RequestBody + .create(MediaType.parse(FileUtils.getMimeType(context, Uri.parse(file.getPath()))), file); + + MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", filename, requestFile); + RequestBody fileNameRequestBody = RequestBody.create(okhttp3.MultipartBody.FORM, filename); + RequestBody tokenRequestBody; + try { + tokenRequestBody = RequestBody.create(MultipartBody.FORM, csrfTokenClient.getTokenBlocking()); + return uploadInterface.uploadFileToStash(fileNameRequestBody, tokenRequestBody, filePart) + .map(stashUploadResponse -> stashUploadResponse.getUpload()); + } catch (Throwable throwable) { + throwable.printStackTrace(); + return Observable.error(throwable); + } + } + + Observable uploadFileFromStash(Context context, + Contribution contribution, + String uniqueFileName, + String fileKey) { + try { + return uploadInterface + .uploadFileFromStash(csrfTokenClient.getTokenBlocking(), + contribution.getPageContents(context), + contribution.getEditSummary(), + uniqueFileName, + fileKey).map(uploadResponse -> uploadResponse.getUpload()); + } catch (Throwable throwable) { + throwable.printStackTrace(); + return Observable.error(throwable); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadInterface.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadInterface.java new file mode 100644 index 0000000000..af3ffd77dd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadInterface.java @@ -0,0 +1,34 @@ +package fr.free.nrw.commons.upload; + +import androidx.annotation.NonNull; + +import io.reactivex.Observable; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.Headers; +import retrofit2.http.Multipart; +import retrofit2.http.POST; +import retrofit2.http.Part; + +import static org.wikipedia.dataclient.Service.MW_API_PREFIX; + +public interface UploadInterface { + + @Multipart + @POST(MW_API_PREFIX + "action=upload&stash=1&ignorewarnings=1") + Observable uploadFileToStash(@Part("filename") RequestBody filename, + @Part("token") RequestBody token, + @Part MultipartBody.Part filePart); + + @Headers("Cache-Control: no-cache") + @POST(MW_API_PREFIX + "action=upload&ignorewarnings=1") + @FormUrlEncoded + @NonNull + Observable uploadFileFromStash(@NonNull @Field("token") String token, + @NonNull @Field("text") String text, + @NonNull @Field("comment") String comment, + @NonNull @Field("filename") String filename, + @NonNull @Field("filekey") String filekey); +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadResponse.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadResponse.java new file mode 100644 index 0000000000..80d612c699 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadResponse.java @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.upload; + +public class UploadResponse { + private final UploadResult upload; + + public UploadResponse(UploadResult upload) { + this.upload = upload; + } + + public UploadResult getUpload() { + return upload; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadResult.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadResult.java new file mode 100644 index 0000000000..1dea597d46 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadResult.java @@ -0,0 +1,39 @@ +package fr.free.nrw.commons.upload; + +import org.wikipedia.gallery.ImageInfo; + +public class UploadResult { + private final String result; + private final String filekey; + private final String filename; + private final String sessionkey; + private final ImageInfo imageinfo; + + public UploadResult(String result, String filekey, String filename, String sessionkey, ImageInfo imageinfo) { + this.result = result; + this.filekey = filekey; + this.filename = filename; + this.sessionkey = sessionkey; + this.imageinfo = imageinfo; + } + + public String getResult() { + return result; + } + + public String getFilekey() { + return filekey; + } + + public String getSessionkey() { + return sessionkey; + } + + public ImageInfo getImageinfo() { + return imageinfo; + } + + public String getFilename() { + return filename; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 7e10846a9c..c9e5a2ee80 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -36,8 +36,9 @@ import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.utils.CommonsDateUtil; import fr.free.nrw.commons.wikidata.WikidataEditService; -import io.reactivex.Single; +import io.reactivex.Observable; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; @@ -56,8 +57,8 @@ public class UploadService extends HandlerService { @Inject WikidataEditService wikidataEditService; @Inject SessionManager sessionManager; @Inject ContributionDao contributionDao; - @Inject - MediaClient mediaClient; + @Inject UploadClient uploadClient; + @Inject MediaClient mediaClient; private NotificationManagerCompat notificationManager; private NotificationCompat.Builder curNotification; @@ -208,9 +209,9 @@ private void uploadContribution(Contribution contribution) { return; } String notificationTag = localUri.toString(); - + File file1; try { - File file1 = new File(localUri.getPath()); + file1 = new File(localUri.getPath()); fileInputStream = new FileInputStream(file1); } catch (FileNotFoundException e) { Timber.d("File not found"); @@ -233,22 +234,8 @@ private void uploadContribution(Contribution contribution) { contribution ); - Single.fromCallable(() -> { - if (!mwApi.validateLogin()) { - // Need to revalidate! - if (sessionManager.revalidateAuthToken()) { - Timber.d("Successfully revalidated token!"); - } else { - Timber.d("Unable to revalidate :("); - stopForeground(true); - sessionManager.forceLogin(UploadService.this); - throw new RuntimeException(getString(R.string.authentication_failed)); - } - } - return "Temp_" + contribution.hashCode() + filename; - }).flatMap(stashFilename -> mwApi.uploadFile( - stashFilename, fileInputStream, contribution.getDataLength(), - localUri, contribution.getContentProviderUri(), notificationUpdater)) + Observable.fromCallable(() -> "Temp_" + contribution.hashCode() + filename) + .flatMap(stashFilename -> uploadClient.uploadFileToStash(getApplicationContext(), stashFilename, file1)) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .doFinally(() -> { @@ -267,22 +254,20 @@ private void uploadContribution(Contribution contribution) { Timber.d("Stash upload response 1 is %s", uploadStash.toString()); - String resultStatus = uploadStash.getResultStatus(); + String resultStatus = uploadStash.getResult(); if (!resultStatus.equals("Success")) { Timber.d("Contribution upload failed. Wikidata entity won't be edited"); showFailedNotification(contribution); - return Single.never(); + return Observable.never(); } else { - synchronized (unfinishedUploads) { - Timber.d("making sure of uniqueness of name: %s", filename); - String uniqueFilename = findUniqueFilename(filename); - unfinishedUploads.add(uniqueFilename); - return mwApi.uploadFileFinalize( - uniqueFilename, - uploadStash.getFilekey(), - contribution.getPageContents(getApplicationContext()), - contribution.getEditSummary()); - } + Timber.d("making sure of uniqueness of name: %s", filename); + String uniqueFilename = findUniqueFilename(filename); + unfinishedUploads.add(uniqueFilename); + return uploadClient.uploadFileFromStash( + getApplicationContext(), + contribution, + uniqueFilename, + uploadStash.getFilekey()); } }) .subscribe(uploadResult -> { @@ -290,19 +275,20 @@ private void uploadContribution(Contribution contribution) { notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS); - String resultStatus = uploadResult.getResultStatus(); + String resultStatus = uploadResult.getResult(); if (!resultStatus.equals("Success")) { Timber.d("Contribution upload failed. Wikidata entity won't be edited"); showFailedNotification(contribution); } else { - String canonicalFilename = uploadResult.getCanonicalFilename(); + String canonicalFilename = "File:" + uploadResult.getFilename(); Timber.d("Contribution upload success. Initiating Wikidata edit for entity id %s", contribution.getWikiDataEntityId()); wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), canonicalFilename); contribution.setFilename(canonicalFilename); - contribution.setImageUrl(uploadResult.getImageUrl()); + contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl()); contribution.setState(Contribution.STATE_COMPLETED); - contribution.setDateUploaded(uploadResult.getDateUploaded()); + contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatShort() + .parse(uploadResult.getImageinfo().getTimestamp())); contributionDao.save(contribution); } }, throwable -> { diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataClient.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataClient.java new file mode 100644 index 0000000000..74c05564c3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataClient.java @@ -0,0 +1,40 @@ +package fr.free.nrw.commons.wikidata; + +import org.wikipedia.csrf.CsrfTokenClient; +import org.wikipedia.dataclient.Service; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import io.reactivex.Observable; + +import static fr.free.nrw.commons.di.NetworkingModule.NAMED_WIKI_DATA_CSRF; + +@Singleton +public class WikidataClient { + + private final Service service; + private final CsrfTokenClient csrfTokenClient; + + @Inject + public WikidataClient(@Named("wikidata-service") Service service, + @Named(NAMED_WIKI_DATA_CSRF) CsrfTokenClient csrfTokenClient) { + this.service = service; + this.csrfTokenClient = csrfTokenClient; + } + + public Observable createClaim(String entityId, String property, String snaktype, String value) { + try { + return service.postCreateClaim(entityId, snaktype, property, value, "en", csrfTokenClient.getTokenBlocking()) + .map(mwPostResponse -> { + if (mwPostResponse.getSuccessVal() == 1) { + return 1L; + } + return -1L; + }); + } catch (Throwable throwable) { + return Observable.just(-1L); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java index eac1f7cde7..8bb753cf9c 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java @@ -10,10 +10,10 @@ import javax.inject.Singleton; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.actions.PageEditClient; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; @@ -26,20 +26,26 @@ @Singleton public class WikidataEditService { + private final static String COMMONS_APP_TAG = "wikimedia-commons-app"; + private final static String COMMONS_APP_EDIT_REASON = "Add tag for edits made using Android Commons app"; + private final Context context; - private final MediaWikiApi mediaWikiApi; private final WikidataEditListener wikidataEditListener; private final JsonKvStore directKvStore; + private final WikidataClient wikidataClient; + private final PageEditClient wikiDataPageEditClient; @Inject public WikidataEditService(Context context, - MediaWikiApi mediaWikiApi, WikidataEditListener wikidataEditListener, - @Named("default_preferences") JsonKvStore directKvStore) { + @Named("default_preferences") JsonKvStore directKvStore, + WikidataClient wikidataClient, + @Named("wikidata-page-edit") PageEditClient wikiDataPageEditClient) { this.context = context; - this.mediaWikiApi = mediaWikiApi; this.wikidataEditListener = wikidataEditListener; this.directKvStore = directKvStore; + this.wikidataClient = wikidataClient; + this.wikiDataPageEditClient = wikiDataPageEditClient; } /** @@ -77,13 +83,20 @@ public void createClaimWithLogging(String wikidataEntityId, String fileName) { private void editWikidataProperty(String wikidataEntityId, String fileName) { Timber.d("Upload successful with wiki data entity id as %s", wikidataEntityId); Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId); - Observable.fromCallable(() -> { - String propertyValue = getFileName(fileName); - return mediaWikiApi.wikidataCreateClaim(wikidataEntityId, "P18", "value", propertyValue); - }) + + String propertyValue = getFileName(fileName); + + Timber.d(propertyValue); + wikidataClient.createClaim(wikidataEntityId, "P18", "value", propertyValue) + .flatMap(revisionId -> { + if (revisionId != -1) { + return wikiDataPageEditClient.addEditTag(revisionId, COMMONS_APP_TAG, COMMONS_APP_EDIT_REASON); + } + throw new RuntimeException("Unable to edit wikidata item"); + }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(revisionId -> handleClaimResult(wikidataEntityId, revisionId), throwable -> { + .subscribe(revisionId -> handleClaimResult(wikidataEntityId, String.valueOf(revisionId)), throwable -> { Timber.e(throwable, "Error occurred while making claim"); ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); }); @@ -95,31 +108,12 @@ private void handleClaimResult(String wikidataEntityId, String revisionId) { wikidataEditListener.onSuccessfulWikidataEdit(); } showSuccessToast(); - logEdit(revisionId); } else { Timber.d("Unable to make wiki data edit for entity %s", wikidataEntityId); ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); } } - /** - * Log the Wikidata edit by adding Wikimedia Commons App tag to the edit - * @param revisionId - */ - @SuppressLint("CheckResult") - private void logEdit(String revisionId) { - Observable.fromCallable(() -> mediaWikiApi.addWikidataEditTag(revisionId)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - if (result) { - Timber.d("Wikidata edit was tagged successfully"); - } else { - Timber.d("Wikidata edit couldn't be tagged"); - } - }, throwable -> Timber.e(throwable, "Error occurred while adding tag to the edit")); - } - /** * Show a success toast when the edit is made successfully */ diff --git a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt index 46d7dfbfc9..8df1971a1d 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt @@ -1,19 +1,22 @@ package fr.free.nrw.commons.delete -import android.accounts.Account import android.content.Context import fr.free.nrw.commons.Media -import fr.free.nrw.commons.auth.SessionManager -import fr.free.nrw.commons.mwapi.MediaWikiApi +import fr.free.nrw.commons.actions.PageEditClient import fr.free.nrw.commons.notification.NotificationHelper import fr.free.nrw.commons.utils.ViewUtilWrapper +import io.reactivex.Observable import org.junit.Assert.* import org.junit.Before import org.junit.Test +import org.mockito.ArgumentMatchers import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations +import org.wikipedia.AppAdapter +import javax.inject.Inject +import javax.inject.Named /** * Tests for delete helper @@ -21,17 +24,15 @@ import org.mockito.MockitoAnnotations class DeleteHelperTest { @Mock - internal var mwApi: MediaWikiApi? = null + @field:[Inject Named("commons-page-edit")] + internal var pageEditClient: PageEditClient? = null @Mock - internal var sessionManager: SessionManager? = null + internal var context: Context? = null @Mock internal var notificationHelper: NotificationHelper? = null - @Mock - internal var context: Context? = null - @Mock internal var viewUtil: ViewUtilWrapper? = null @@ -54,9 +55,13 @@ class DeleteHelperTest { */ @Test fun makeDeletion() { - `when`(mwApi?.editToken).thenReturn("token") - `when`(sessionManager?.authCookie).thenReturn("Mock cookie") - `when`(sessionManager?.currentAccount).thenReturn(Account("TestUser", "Test")) + `when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(pageEditClient?.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(pageEditClient?.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(media?.displayTitle).thenReturn("Test file") `when`(media?.filename).thenReturn("Test file.jpg") @@ -68,16 +73,45 @@ class DeleteHelperTest { /** * Test a failed deletion */ - @Test - fun makeDeletionForNullToken() { - `when`(mwApi?.editToken).thenReturn(null) - `when`(sessionManager?.authCookie).thenReturn("Mock cookie") - `when`(sessionManager?.currentAccount).thenReturn(Account("TestUser", "Test")) + @Test(expected = RuntimeException::class) + fun makeDeletionForPrependEditFailure() { + `when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(false)) + `when`(pageEditClient?.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(pageEditClient?.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) `when`(media?.displayTitle).thenReturn("Test file") `when`(media?.filename).thenReturn("Test file.jpg") - val makeDeletion = deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() - assertNotNull(makeDeletion) - assertFalse(makeDeletion!!) + deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() + } + + @Test(expected = RuntimeException::class) + fun makeDeletionForEditFailure() { + `when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(pageEditClient?.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(pageEditClient?.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(false)) + `when`(media?.displayTitle).thenReturn("Test file") + `when`(media?.filename).thenReturn("Test file.jpg") + + deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() + } + + @Test(expected = RuntimeException::class) + fun makeDeletionForAppendEditFailure() { + `when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(pageEditClient?.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(false)) + `when`(pageEditClient?.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(media?.displayTitle).thenReturn("Test file") + `when`(media?.filename).thenReturn("Test file.jpg") + + deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() } } \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt index 2aa5e47f53..42519646fe 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt @@ -2,7 +2,6 @@ package fr.free.nrw.commons.mwapi import android.os.Build import androidx.test.core.app.ApplicationProvider -import com.google.gson.Gson import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.kvstore.JsonKvStore import fr.free.nrw.commons.utils.ConfigUtils @@ -38,7 +37,7 @@ class ApacheHttpClientMediaWikiApiTest { wikidataServer = MockWebServer() okHttpClient = OkHttpClient() sharedPreferences = mock(JsonKvStore::class.java) - testObject = ApacheHttpClientMediaWikiApi(ApplicationProvider.getApplicationContext(), "http://" + server.hostName + ":" + server.port + "/", "http://" + wikidataServer.hostName + ":" + wikidataServer.port + "/", sharedPreferences) + testObject = ApacheHttpClientMediaWikiApi("http://" + server.hostName + ":" + server.port + "/") } @After @@ -46,181 +45,6 @@ class ApacheHttpClientMediaWikiApiTest { server.shutdown() } - @Test - fun authCookiesAreHandled() { - assertEquals("", testObject.authCookie) - - testObject.authCookie = "cookie=chocolate-chip" - - assertEquals("cookie=chocolate-chip", testObject.authCookie) - } - - @Test - fun simpleLoginWithWrongPassword() { - server.enqueue(MockResponse().setBody("")) - server.enqueue(MockResponse().setBody("")) - - val result = testObject.login("foo", "bar") - - assertBasicRequestParameters(server, "POST").let { loginTokenRequest -> - parseBody(loginTokenRequest.body.readUtf8()).let { body -> - assertEquals("xml", body["format"]) - assertEquals("query", body["action"]) - assertEquals("login", body["type"]) - assertEquals("tokens", body["meta"]) - } - } - - assertBasicRequestParameters(server, "POST").let { loginRequest -> - parseBody(loginRequest.body.readUtf8()).let { body -> - assertEquals("1", body["rememberMe"]) - assertEquals("foo", body["username"]) - assertEquals("bar", body["password"]) - assertEquals("baz", body["logintoken"]) - assertEquals("https://commons.wikimedia.org", body["loginreturnurl"]) - assertEquals("xml", body["format"]) - } - } - - assertEquals("wrongpassword", result) - } - - @Test - fun simpleLogin() { - server.enqueue(MockResponse().setBody("")) - server.enqueue(MockResponse().setBody("")) - - val result = testObject.login("foo", "bar") - - assertBasicRequestParameters(server, "POST").let { loginTokenRequest -> - parseBody(loginTokenRequest.body.readUtf8()).let { body -> - assertEquals("xml", body["format"]) - assertEquals("query", body["action"]) - assertEquals("login", body["type"]) - assertEquals("tokens", body["meta"]) - } - } - - assertBasicRequestParameters(server, "POST").let { loginRequest -> - parseBody(loginRequest.body.readUtf8()).let { body -> - assertEquals("1", body["rememberMe"]) - assertEquals("foo", body["username"]) - assertEquals("bar", body["password"]) - assertEquals("baz", body["logintoken"]) - assertEquals("https://commons.wikimedia.org", body["loginreturnurl"]) - assertEquals("xml", body["format"]) - } - } - - assertEquals("PASS", result) - } - - @Test - fun twoFactorLogin() { - server.enqueue(MockResponse().setBody("")) - server.enqueue(MockResponse().setBody("")) - - val result = testObject.login("foo", "bar", "2fa") - - assertBasicRequestParameters(server, "POST").let { loginTokenRequest -> - parseBody(loginTokenRequest.body.readUtf8()).let { body -> - assertEquals("xml", body["format"]) - assertEquals("query", body["action"]) - assertEquals("login", body["type"]) - assertEquals("tokens", body["meta"]) - } - } - - assertBasicRequestParameters(server, "POST").let { loginRequest -> - parseBody(loginRequest.body.readUtf8()).let { body -> - assertEquals("true", body["rememberMe"]) - assertEquals("foo", body["username"]) - assertEquals("bar", body["password"]) - assertEquals("baz", body["logintoken"]) - assertEquals("true", body["logincontinue"]) - assertEquals("2fa", body["OATHToken"]) - assertEquals("xml", body["format"]) - } - } - - assertEquals("PASS", result) - } - - @Test - fun validateLoginForLoggedInUser() { - server.enqueue(MockResponse().setBody("")) - - val result = testObject.validateLogin() - - assertBasicRequestParameters(server, "GET").let { loginTokenRequest -> - parseQueryParams(loginTokenRequest).let { body -> - assertEquals("xml", body["format"]) - assertEquals("query", body["action"]) - assertEquals("userinfo", body["meta"]) - } - } - - assertTrue(result) - } - - @Test - fun validateLoginForLoggedOutUser() { - server.enqueue(MockResponse().setBody("")) - - val result = testObject.validateLogin() - - assertBasicRequestParameters(server, "GET").let { loginTokenRequest -> - parseQueryParams(loginTokenRequest).let { params -> - assertEquals("xml", params["format"]) - assertEquals("query", params["action"]) - assertEquals("userinfo", params["meta"]) - } - } - - assertFalse(result) - } - - @Test - fun editToken() { - server.enqueue(MockResponse().setBody("")) - - val result = testObject.editToken - - assertBasicRequestParameters(server, "POST").let { editTokenRequest -> - parseBody(editTokenRequest.body.readUtf8()).let { body -> - assertEquals("query", body["action"]) - assertEquals("tokens", body["meta"]) - } - } - - assertEquals("baz", result) - } - - @Test - fun getWikidataEditToken() { - server.enqueue(MockResponse().setBody("")) - wikidataServer.enqueue(MockResponse().setBody("")) - - val result = testObject.wikidataCsrfToken - - assertBasicRequestParameters(server, "GET").let { centralAuthTokenRequest -> - parseQueryParams(centralAuthTokenRequest).let { params -> - assertEquals("xml", params["format"]) - assertEquals("centralauthtoken", params["action"]) - } - } - - assertBasicRequestParameters(wikidataServer, "POST").let { editTokenRequest -> - parseBody(editTokenRequest.body.readUtf8()).let { body -> - assertEquals("query", body["action"]) - assertEquals("abc", body["centralauthtoken"]) - assertEquals("tokens", body["meta"]) - } - } - - assertEquals("baz", result) - } - @Test fun isUserBlockedFromCommonsForInfinitelyBlockedUser() { server.enqueue(MockResponse().setBody("")) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt index 8bfb6cb665..fdbb79e2de 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/OkHttpJsonApiClientTest.kt @@ -5,6 +5,7 @@ import fr.free.nrw.commons.Media import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.kvstore.JsonKvStore import fr.free.nrw.commons.utils.CommonsDateUtil +import junit.framework.Assert.assertEquals import okhttp3.HttpUrl import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockResponse From e3ba16dc1ab8b39a4bb8d9b9f02c4237169b4131 Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Mon, 8 Jul 2019 02:42:21 +0530 Subject: [PATCH 2/2] Minor fixes in media and search --- .../main/java/fr/free/nrw/commons/Media.java | 18 +++++------ .../nrw/commons/category/CategoryClient.java | 28 +++++------------ .../category/CategoryImagesActivity.java | 22 ++------------ .../category/CategoryImagesListFragment.java | 6 +++- .../commons/category/CategoryInterface.java | 2 +- .../contributions/ContributionsFragment.java | 30 +++++++++---------- .../categories/SearchCategoryFragment.java | 6 ++-- .../free/nrw/commons/media/MediaClient.java | 24 +++++++++------ .../fr/free/nrw/commons/quiz/QuizChecker.java | 23 +++++++++----- .../commons/category/CategoryClientTest.kt | 8 ++--- 10 files changed, 78 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 62a11a90ae..6a215c6ecf 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -47,17 +47,17 @@ public Media[] newArray(int i) { protected String imageUrl; protected String filename; protected String description; // monolingual description on input... - protected String discussion; - protected long dataLength; + private String discussion; + private long dataLength; protected Date dateCreated; protected @Nullable Date dateUploaded; protected int width; protected int height; protected String license; - protected String licenseUrl; + private String licenseUrl; protected String creator; protected ArrayList categories; // as loaded at runtime? - protected boolean requestedDeletion; + private boolean requestedDeletion; private Map descriptions; // multilingual descriptions as loaded private HashMap tags = new HashMap<>(); private @Nullable LatLng coordinates; @@ -261,7 +261,7 @@ public void setFilename(String filename) { * Sets the discussion of the file. * @param discussion */ - public void setDiscussion(String discussion) { + void setDiscussion(String discussion) { this.discussion = discussion; } @@ -387,7 +387,7 @@ public String getLicense() { return license; } - public void setThumbUrl(String thumbUrl) { + private void setThumbUrl(String thumbUrl) { this.thumbUrl = thumbUrl; } @@ -399,7 +399,7 @@ public String getLicenseUrl() { * Sets the license name of the file. * @param license license name as a String */ - public void setLicenseInformation(String license, String licenseUrl) { + private void setLicenseInformation(String license, String licenseUrl) { this.license = license; if (!licenseUrl.startsWith("http://") && !licenseUrl.startsWith("https://")) { @@ -450,7 +450,7 @@ public void setCategories(List categories) { * Modifies (or sets) media descriptions * @param descriptions Media descriptions */ - void setDescriptions(Map descriptions) { + private void setDescriptions(Map descriptions) { this.descriptions.clear(); this.descriptions.putAll(descriptions); } @@ -521,7 +521,7 @@ public void writeToParcel(Parcel parcel, int flags) { /** * Set requested deletion to true */ - public void setRequestedDeletion(){ + void setRequestedDeletion(){ requestedDeletion = true; } 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 index 329c3635a1..1c73c7373a 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.java @@ -21,11 +21,11 @@ @Singleton public class CategoryClient { - private final CategoryInterface CategoryInterface; + private final CategoryInterface categoryInterface; @Inject - public CategoryClient(CategoryInterface CategoryInterface) { - this.CategoryInterface = CategoryInterface; + public CategoryClient(CategoryInterface categoryInterface) { + this.categoryInterface = categoryInterface; } /** @@ -37,7 +37,7 @@ public CategoryClient(CategoryInterface CategoryInterface) { * @return */ public Observable searchCategories(String filter, int itemLimit, int offset) { - return responseToCategoryName(CategoryInterface.searchCategories(filter, itemLimit, offset)); + return responseToCategoryName(categoryInterface.searchCategories(filter, itemLimit, offset)); } @@ -53,18 +53,6 @@ public Observable searchCategories(String filter, int itemLimit) { } - /** - * 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. * @@ -73,10 +61,9 @@ public Observable searchCategoriesForPrefix(String prefix, int itemLimit * @return */ public Observable searchCategoriesForPrefix(String prefix, int itemLimit) { - return searchCategoriesForPrefix(prefix, itemLimit, 0); + return responseToCategoryName(categoryInterface.searchCategoriesForPrefix(prefix, itemLimit)); } - /** * 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. @@ -85,7 +72,7 @@ public Observable searchCategoriesForPrefix(String prefix, int itemLimit * @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)); + return responseToCategoryName(categoryInterface.getSubCategoryList(categoryName)); } /** @@ -97,7 +84,7 @@ public Observable getSubCategoryList(String categoryName) { */ @NonNull public Observable getParentCategoryList(String categoryName) { - return responseToCategoryName(CategoryInterface.getParentCategoryList(categoryName)); + return responseToCategoryName(categoryInterface.getParentCategoryList(categoryName)); } /** @@ -119,7 +106,6 @@ private Observable responseToCategoryName(Observable re 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/CategoryImagesActivity.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java index 001f817b33..4d70d23e70 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 @@ -1,17 +1,15 @@ 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; @@ -127,20 +125,6 @@ public void onItemClick(AdapterView adapterView, View view, int i, long l) { forceInitBackButton(); } - /** - * Consumers should be simply using this method to use this activity. - * @param context A Context of the application package implementing this class. - * @param title Page title - * @param categoryName Name of the category for displaying its images - */ - public static void startYourself(Context context, String title, String categoryName) { - Intent intent = new Intent(context, CategoryImagesActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtra("title", title); - intent.putExtra("categoryName", categoryName); - context.startActivity(intent); - } - /** * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index * @param i It is the index of which media object is to be returned which is same as 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 2e4f6e0a13..26f2432167 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 @@ -13,13 +13,14 @@ import android.widget.RelativeLayout; import android.widget.TextView; +import androidx.annotation.Nullable; + import java.util.List; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Named; -import androidx.annotation.Nullable; import butterknife.BindView; import butterknife.ButterKnife; import dagger.android.support.DaggerFragment; @@ -90,6 +91,9 @@ private void initViews() { String categoryName = getArguments().getString("categoryName"); if (getArguments() != null && categoryName != null) { this.categoryName = categoryName; + if (!this.categoryName.startsWith("Category:")) { + this.categoryName = "Category:" + categoryName; + } resetQueryContinueValues(categoryName); initList(); setScrollListener(); 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 index f03506ed4f..1f5258f225 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryInterface.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryInterface.java @@ -33,7 +33,7 @@ Observable searchCategories(@Query("gsrsearch") String filter, @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); + @Query("gaclimit") int itemLimit); @GET("w/api.php?action=query&format=json&formatversion=2" + "&generator=categorymembers&gcmtype=subcat" 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 12c9ae6023..a73bd74d56 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 @@ -1,15 +1,10 @@ package fr.free.nrw.commons.contributions; -import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; -import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION; -import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; - import android.Manifest; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; -import android.database.DataSetObserver; import android.os.Bundle; import android.os.IBinder; import android.view.LayoutInflater; @@ -17,12 +12,17 @@ import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager.OnBackStackChangedListener; import androidx.fragment.app.FragmentTransaction; + +import javax.inject.Inject; +import javax.inject.Named; + import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.HandlerService; @@ -57,11 +57,12 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import javax.inject.Inject; -import javax.inject.Named; import timber.log.Timber; +import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; +import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION; +import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; + public class ContributionsFragment extends CommonsDaggerSupportFragment implements @@ -78,15 +79,14 @@ public class ContributionsFragment @Inject CampaignsPresenter presenter; @Inject LocationServiceManager locationManager; - private ArrayList observersWaitingForLoad = new ArrayList<>(); private UploadService uploadService; private boolean isUploadServiceConnected; private CompositeDisposable compositeDisposable = new CompositeDisposable(); 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; @@ -268,7 +268,7 @@ public void onAttach(Context context) { * 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 void showContributionsListFragment() { * 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 void onBackStackChanged() { * 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 void onAuthCookieAcquired(Intent uploadServiceIntent) { * 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/explore/categories/SearchCategoryFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.java index 7cfea064a3..aecc2de05a 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.java @@ -56,10 +56,10 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment { ProgressBar progressBar; @BindView(R.id.imagesNotFound) TextView categoriesNotFoundView; - String query; + private String query; @BindView(R.id.bottomProgressBar) ProgressBar bottomProgressBar; - boolean isLoadingCategories; + private boolean isLoadingCategories; @Inject RecentSearchesDao recentSearchesDao; @Inject MediaWikiApi mwApi; @@ -152,7 +152,7 @@ public void updateCategoryList(String query) { /** * Adds 25 more results to existing search results */ - public void addCategoriesToList(String query) { + private void addCategoriesToList(String query) { if(isLoadingCategories) return; isLoadingCategories=true; this.query = query; 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 index 38e778db73..3e2a1269db 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java @@ -22,9 +22,10 @@ @Singleton public class MediaClient { + private static final int PAGE_SIZE = 10; + private final MediaInterface mediaInterface; - //OkHttpJsonApiClient used JsonKvStore for this. I don't know why. private Map> continuationStore; @Inject @@ -66,10 +67,10 @@ public Single checkFileExistsUsingSha(String fileSha) { * @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()), + return responseToMediaList(mediaInterface + .getMediaListFromCategory(category, + PAGE_SIZE, + getContinuationMap("category_" + category)), "category_" + category); //if false } @@ -83,13 +84,18 @@ public Single> getMediaListFromCategory(String category) { */ 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 + mediaInterface + .getMediaListFromSearch(keyword, + PAGE_SIZE, + getContinuationMap("search_" + keyword)), //if false "search_" + keyword); } + private Map getContinuationMap(String keyword) { + return continuationStore.containsKey(keyword) ? continuationStore.get(keyword) : Collections.emptyMap(); + } + private Single> responseToMediaList(Observable response, String key) { return response.flatMap(mwQueryResponse -> { if (null == mwQueryResponse @@ -101,7 +107,7 @@ private Single> responseToMediaList(Observable resp return Observable.fromIterable(mwQueryResponse.query().pages()); }) .map(Media::from) - .collect(ArrayList::new, List::add); + .collect(ArrayList::new, List::add); } } diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java index 29711ffbfc..d9d7b5b8ee 100644 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java @@ -10,7 +10,6 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.WelcomeActivity; -import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; import fr.free.nrw.commons.utils.DialogUtil; @@ -33,9 +32,12 @@ public class QuizChecker { private boolean isRevertCountFetched; private boolean isUploadCountFetched; + private String userName; + private boolean isFeedbackChecked; + private boolean isQuizDialogShown; + private CompositeDisposable compositeDisposable = new CompositeDisposable(); - private final SessionManager sessionManager; private final OkHttpJsonApiClient okHttpJsonApiClient; private final JsonKvStore revertKvStore; @@ -46,19 +48,22 @@ public class QuizChecker { /** * constructor to set the parameters for quiz - * @param sessionManager + * @param userName * @param okHttpJsonApiClient instance of MediaWikiApi */ @Inject - public QuizChecker(SessionManager sessionManager, + public QuizChecker(@Named("username") String userName, OkHttpJsonApiClient okHttpJsonApiClient, @Named("default_preferences") JsonKvStore revertKvStore) { - this.sessionManager = sessionManager; + this.userName = userName; this.okHttpJsonApiClient = okHttpJsonApiClient; this.revertKvStore = revertKvStore; } public void initQuizCheck(Activity activity) { + if (isFeedbackChecked) { + return; + } setUploadCount(activity); setRevertCount(activity); } @@ -72,7 +77,7 @@ public void cleanup() { */ private void setUploadCount(Activity activity) { compositeDisposable.add(okHttpJsonApiClient - .getUploadCount(sessionManager.getUserName()) + .getUploadCount(userName) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(uploadCount -> setTotalUploadCount(activity, uploadCount), @@ -100,13 +105,14 @@ private void setTotalUploadCount(Activity activity, int uploadCount) { */ private void setRevertCount(Activity activity) { compositeDisposable.add(okHttpJsonApiClient - .getAchievements(sessionManager.getUserName()) + .getAchievements(userName) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( response -> { if (response != null) { setRevertParameter(activity, response.getDeletedUploads()); + isFeedbackChecked = true; } }, throwable -> Timber.e(throwable, "Fetching feedback failed")) ); @@ -147,6 +153,9 @@ private void calculateRevertParameter(Activity activity) { */ @SuppressLint("StringFormatInvalid") private void callQuiz(Activity activity) { + if (isQuizDialogShown) { + return; + } DialogUtil.showAlertDialog(activity, activity.getString(R.string.quiz), activity.getString(R.string.quiz_alert_message, REVERT_PERCENTAGE_FOR_MESSAGE), diff --git a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryClientTest.kt index 7c26cc3230..3cc3ed6c28 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryClientTest.kt @@ -67,12 +67,12 @@ class CategoryClientTest { val mockResponse = Mockito.mock(MwQueryResponse::class.java) Mockito.`when`(mockResponse.query()).thenReturn(mwQueryResult) - Mockito.`when`(categoryInterface!!.searchCategoriesForPrefix(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())) + Mockito.`when`(categoryInterface!!.searchCategoriesForPrefix(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())) .thenReturn(Observable.just(mockResponse)) val actualCategoryName = categoryClient!!.searchCategoriesForPrefix("tes", 10).blockingFirst() assertEquals("Test", actualCategoryName) - val actualCategoryName2 = categoryClient!!.searchCategoriesForPrefix("tes", 10, 10).blockingFirst() + val actualCategoryName2 = categoryClient!!.searchCategoriesForPrefix("tes", 10).blockingFirst() assertEquals("Test", actualCategoryName2) } @@ -83,12 +83,12 @@ class CategoryClientTest { val mockResponse = Mockito.mock(MwQueryResponse::class.java) Mockito.`when`(mockResponse.query()).thenReturn(mwQueryResult) - Mockito.`when`(categoryInterface!!.searchCategoriesForPrefix(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())) + Mockito.`when`(categoryInterface!!.searchCategoriesForPrefix(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())) .thenReturn(Observable.just(mockResponse)) categoryClient!!.searchCategoriesForPrefix("tes", 10).subscribe( { fail("SearchCategories returned element when it shouldn't have.") }, { s -> throw s }) - categoryClient!!.searchCategoriesForPrefix("tes", 10, 10).subscribe( + categoryClient!!.searchCategoriesForPrefix("tes", 10).subscribe( { fail("SearchCategories returned element when it shouldn't have.") }, { s -> throw s }) }