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