mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
Merge branch 'backend-overhaul' into master
This commit is contained in:
commit
0090f24257
76 changed files with 1819 additions and 3475 deletions
|
|
@ -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\""
|
||||
|
|
|
|||
|
|
@ -152,7 +152,8 @@
|
|||
</service>
|
||||
<service
|
||||
android:name=".contributions.ContributionsSyncService"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:process=":sync">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
|
@ -161,17 +162,6 @@
|
|||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/contributions_sync_adapter" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".modifications.ModificationsSyncService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/modifications_sync_adapter" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="org.acra.sender.SenderService"
|
||||
|
|
@ -193,12 +183,7 @@
|
|||
android:exported="false"
|
||||
android:label="@string/provider_contributions"
|
||||
android:syncable="true" />
|
||||
<provider
|
||||
android:name=".modifications.ModificationsContentProvider"
|
||||
android:authorities="${applicationId}.modifications.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_modifications"
|
||||
android:syncable="true" />
|
||||
|
||||
<provider
|
||||
android:name=".category.CategoryContentProvider"
|
||||
android:authorities="${applicationId}.categories.contentprovider"
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ public class CommonsAppAdapter extends AppAdapter {
|
|||
|
||||
@Override
|
||||
public void updateAccount(@NonNull LoginResult result) {
|
||||
// TODO: sessionManager.updateAccount(result);
|
||||
sessionManager.updateAccount(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -69,7 +69,8 @@ public class CommonsAppAdapter extends AppAdapter {
|
|||
if (!preferences.contains(COOKIE_STORE_NAME)) {
|
||||
return null;
|
||||
}
|
||||
return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class, preferences.getString(COOKIE_STORE_NAME, null));
|
||||
return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class,
|
||||
preferences.getString(COOKIE_STORE_NAME, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import android.os.Build;
|
|||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
import com.facebook.imagepipeline.core.ImagePipelineConfig;
|
||||
import com.squareup.leakcanary.LeakCanary;
|
||||
|
|
@ -28,7 +30,6 @@ import java.io.File;
|
|||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
||||
|
|
@ -41,7 +42,6 @@ import fr.free.nrw.commons.di.ApplicationlessInjection;
|
|||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.logging.FileLoggingTree;
|
||||
import fr.free.nrw.commons.logging.LogUtils;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||
import fr.free.nrw.commons.upload.FileUtils;
|
||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
|
|
@ -265,7 +265,6 @@ public class CommonsApplication extends Application {
|
|||
dbOpenHelper.getReadableDatabase().close();
|
||||
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
||||
|
||||
ModifierSequenceDao.Table.onDelete(db);
|
||||
CategoryDao.Table.onDelete(db);
|
||||
ContributionDao.Table.onDelete(db);
|
||||
BookmarkPicturesDao.Table.onDelete(db);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import android.net.Uri;
|
|||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryPage;
|
||||
import org.wikipedia.gallery.ExtMetadata;
|
||||
|
|
@ -20,14 +23,13 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
|
||||
|
||||
public class Media implements Parcelable {
|
||||
|
||||
public static final Media EMPTY = new Media("");
|
||||
public static Creator<Media> CREATOR = new Creator<Media>() {
|
||||
@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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Media> fetchMediaDetails(String filename) {
|
||||
Single<Media> mediaSingle = getMediaFromFileName(filename);
|
||||
Single<Boolean> pageExistsSingle = mediaWikiApi.pageExists("Commons:Deletion_requests/" + filename);
|
||||
Single<Boolean> pageExistsSingle = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + filename);
|
||||
Single<String> 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<Media> getMediaFromFileName(String filename) {
|
||||
return okHttpJsonApiClient.getMedia(filename, false);
|
||||
return mediaClient.getMedia(filename);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<Boolean> 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<Boolean> 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<Boolean> 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<Integer> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Edit> 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<Edit> 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<Edit> postPrependEdit(@NonNull @Field("title") String title,
|
||||
@NonNull @Field("summary") String summary,
|
||||
@NonNull @Field("prependtext") String text,
|
||||
@NonNull @Field("token") String token);
|
||||
}
|
||||
|
|
@ -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<Boolean> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,24 +250,38 @@ 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) {
|
||||
private void doLogin(String username, String password, String twoFactorCode) {
|
||||
progressDialog.show();
|
||||
|
||||
Action action = () -> {
|
||||
try {
|
||||
if (twoFactorCode.isEmpty()) {
|
||||
return mwApi.login(username, password);
|
||||
} else {
|
||||
return mwApi.login(username, password, twoFactorCode);
|
||||
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);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when user skips the login.
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
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;
|
||||
}
|
||||
|
||||
private void removeAccount() {
|
||||
Account account = getCurrentAccount();
|
||||
if (account != null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
accountManager().removeAccountExplicitly(account);
|
||||
} else {
|
||||
if (response != null) {
|
||||
response.onError(ERROR_CODE_REMOTE_EXCEPTION, "");
|
||||
//noinspection deprecation
|
||||
accountManager().removeAccount(account, null, null);
|
||||
}
|
||||
}
|
||||
Timber.d("account creation failure");
|
||||
}
|
||||
|
||||
// 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!
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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<Bookmark> 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<Media> 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();
|
||||
|
|
|
|||
|
|
@ -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<CategoryItem> getTitleCategories(String title) {
|
||||
return mwApi.searchTitles(title, SEARCH_CATS_LIMIT)
|
||||
return categoryClient.searchCategories(title, SEARCH_CATS_LIMIT)
|
||||
.map(name -> new CategoryItem(name, false));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> responseToCategoryName(Observable<MwQueryResponse> responseObservable) {
|
||||
return responseObservable
|
||||
.flatMap(mwQueryResponse -> {
|
||||
MwQueryResult query;
|
||||
List<MwQueryPage> 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:", ""));
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<Media>> getCategoryImages(String categoryName) {
|
||||
return okHttpJsonApiClient.getMediaList("category", categoryName);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> getSubCategoryList(NodeList childNodes) {
|
||||
List<String> 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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<MwQueryResponse> 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<MwQueryResponse> 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<MwQueryResponse> getSubCategoryList(@Query("gcmtitle") String categoryName);
|
||||
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
||||
+ "&generator=categories&prop=info&gcllimit=500")
|
||||
Observable<MwQueryResponse> getParentCategoryList(@Query("titles") String categoryName);
|
||||
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
|
|||
TextView categoriesNotFoundView;
|
||||
|
||||
private String categoryName = null;
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject CategoryClient categoryClient;
|
||||
|
||||
private RVRendererAdapter<String> 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<String>::new, ArrayList::add)
|
||||
.subscribe(this::handleSuccess, this::handleError));
|
||||
} else {
|
||||
compositeDisposable.add(Observable.fromCallable(() -> mwApi.getParentCategoryList(categoryName))
|
||||
compositeDisposable.add(categoryClient.getSubCategoryList("Category:"+categoryName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.collect(ArrayList<String>::new, ArrayList::add)
|
||||
.subscribe(this::handleSuccess, this::handleError));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<Boolean> 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<Boolean> 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;
|
||||
return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary)
|
||||
.flatMap(result -> {
|
||||
if (result) {
|
||||
return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);
|
||||
}
|
||||
|
||||
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;
|
||||
throw new RuntimeException("Failed to nominate for deletion");
|
||||
}).flatMap(result -> {
|
||||
if (result) {
|
||||
return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary);
|
||||
}
|
||||
return true;
|
||||
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<SingleSource<Boolean>>) () ->
|
||||
makeDeletion(context, media, finalReason))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(aBoolean -> {
|
||||
|
|
|
|||
|
|
@ -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<Application
|
|||
|
||||
void inject(ContributionsSyncAdapter syncAdapter);
|
||||
|
||||
void inject(ModificationsSyncAdapter syncAdapter);
|
||||
|
||||
void inject(LoginActivity activity);
|
||||
|
||||
void inject(SettingsFragment fragment);
|
||||
|
|
|
|||
|
|
@ -5,14 +5,11 @@ import android.content.ContentProviderClient;
|
|||
import android.content.Context;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import androidx.collection.LruCache;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
import org.wikipedia.AppAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
|
@ -22,7 +19,6 @@ import java.util.Map;
|
|||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import androidx.collection.LruCache;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
|
|
@ -37,6 +33,9 @@ import fr.free.nrw.commons.upload.UploadController;
|
|||
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||
import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
||||
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
@Module
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
|
|
@ -85,7 +84,7 @@ public class CommonsApplicationModule {
|
|||
|
||||
@Provides
|
||||
public AccountUtil providesAccountUtil(Context context) {
|
||||
return new AccountUtil(context);
|
||||
return new AccountUtil();
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -191,4 +190,10 @@ public class CommonsApplicationModule {
|
|||
public Scheduler providesMainThread() {
|
||||
return AndroidSchedulers.mainThread();
|
||||
}
|
||||
|
||||
@Named("username")
|
||||
@Provides
|
||||
public String provideLoggedInUsername() {
|
||||
return AppAdapter.get().getUserName();
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
|
|||
import fr.free.nrw.commons.category.CategoryContentProvider;
|
||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
|
||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||
|
||||
@Module
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
|
|
@ -16,9 +15,6 @@ public abstract class ContentProviderBuilderModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract ContributionsContentProvider bindContributionsContentProvider();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ModificationsContentProvider bindModificationsContentProvider();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract CategoryContentProvider bindCategoryContentProvider();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,16 @@ package fr.free.nrw.commons.di;
|
|||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import org.wikipedia.csrf.CsrfTokenClient;
|
||||
import org.wikipedia.dataclient.Service;
|
||||
import org.wikipedia.dataclient.ServiceFactory;
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
import org.wikipedia.json.GsonUtil;
|
||||
import org.wikipedia.login.LoginClient;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
|
@ -14,15 +19,19 @@ import java.util.concurrent.TimeUnit;
|
|||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.actions.PageEditClient;
|
||||
import fr.free.nrw.commons.actions.PageEditInterface;
|
||||
import fr.free.nrw.commons.category.CategoryInterface;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.media.MediaInterface;
|
||||
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
import fr.free.nrw.commons.review.ReviewInterface;
|
||||
import fr.free.nrw.commons.upload.UploadInterface;
|
||||
import okhttp3.Cache;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
|
@ -39,6 +48,12 @@ public class NetworkingModule {
|
|||
|
||||
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
public static final String NAMED_COMMONS_WIKI_SITE = "commons-wikisite";
|
||||
private static final String NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite";
|
||||
|
||||
public static final String NAMED_COMMONS_CSRF = "commons-csrf";
|
||||
public static final String NAMED_WIKI_DATA_CSRF = "wikidata-csrf";
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public OkHttpClient provideOkHttpClient(Context context,
|
||||
|
|
@ -67,7 +82,7 @@ public class NetworkingModule {
|
|||
public MediaWikiApi provideMediaWikiApi(Context context,
|
||||
@Named("default_preferences") JsonKvStore defaultKvStore,
|
||||
Gson gson) {
|
||||
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultKvStore, gson);
|
||||
return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -81,10 +96,30 @@ public class NetworkingModule {
|
|||
WIKIDATA_SPARQL_QUERY_URL,
|
||||
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,
|
||||
BuildConfig.WIKIMEDIA_API_HOST,
|
||||
defaultKvStore,
|
||||
gson);
|
||||
}
|
||||
|
||||
@Named(NAMED_COMMONS_CSRF)
|
||||
@Provides
|
||||
@Singleton
|
||||
public CsrfTokenClient provideCommonsCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return new CsrfTokenClient(commonsWikiSite, commonsWikiSite);
|
||||
}
|
||||
|
||||
@Named(NAMED_WIKI_DATA_CSRF)
|
||||
@Provides
|
||||
@Singleton
|
||||
public CsrfTokenClient provideWikidataCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite,
|
||||
@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) {
|
||||
return new CsrfTokenClient(wikidataWikiSite, commonsWikiSite);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public LoginClient provideLoginClient() {
|
||||
return new LoginClient();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("wikimedia_api_host")
|
||||
@NonNull
|
||||
|
|
@ -101,6 +136,20 @@ public class NetworkingModule {
|
|||
return HttpUrl.parse(TOOLS_FORGE_URL);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named(NAMED_COMMONS_WIKI_SITE)
|
||||
public WikiSite provideCommonsWikiSite() {
|
||||
return new WikiSite(BuildConfig.COMMONS_URL);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named(NAMED_WIKI_DATA_WIKI_SITE)
|
||||
public WikiSite provideWikidataWikiSite() {
|
||||
return new WikiSite(BuildConfig.WIKIDATA_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere.
|
||||
* @return returns a singleton Gson instance
|
||||
|
|
@ -113,14 +162,71 @@ public class NetworkingModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("commons-wikisite")
|
||||
public WikiSite provideCommonsWikiSite() {
|
||||
return new WikiSite(BuildConfig.COMMONS_URL);
|
||||
@Named("commons-service")
|
||||
public Service provideCommonsService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public ReviewInterface provideReviewInterface(@Named("commons-wikisite") WikiSite commonsWikiSite) {
|
||||
@Named("wikidata-service")
|
||||
public Service provideWikidataService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) {
|
||||
return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, Service.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public ReviewInterface provideReviewInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public UploadInterface provideUploadInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, UploadInterface.class);
|
||||
}
|
||||
|
||||
@Named("commons-page-edit-service")
|
||||
@Provides
|
||||
@Singleton
|
||||
public PageEditInterface providePageEditService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, PageEditInterface.class);
|
||||
}
|
||||
|
||||
@Named("wikidata-page-edit-service")
|
||||
@Provides
|
||||
@Singleton
|
||||
public PageEditInterface provideWikiDataPageEditService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikiDataWikiSite) {
|
||||
return ServiceFactory.get(wikiDataWikiSite, BuildConfig.WIKIDATA_URL, PageEditInterface.class);
|
||||
}
|
||||
|
||||
@Named("commons-page-edit")
|
||||
@Provides
|
||||
@Singleton
|
||||
public PageEditClient provideCommonsPageEditClient(@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient,
|
||||
@Named("commons-page-edit-service") PageEditInterface pageEditInterface,
|
||||
@Named("commons-service") Service service) {
|
||||
return new PageEditClient(csrfTokenClient, pageEditInterface, service);
|
||||
}
|
||||
|
||||
@Named("wikidata-page-edit")
|
||||
@Provides
|
||||
@Singleton
|
||||
public PageEditClient provideWikidataPageEditClient(@Named(NAMED_WIKI_DATA_CSRF) CsrfTokenClient csrfTokenClient,
|
||||
@Named("wikidata-page-edit-service") PageEditInterface pageEditInterface,
|
||||
@Named("wikidata-service") Service service) {
|
||||
return new PageEditClient(csrfTokenClient, pageEditInterface, service);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public MediaInterface provideMediaInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, MediaInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public CategoryInterface provideCategoryInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, CategoryInterface.class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import javax.inject.Named;
|
|||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.category.CategoryClient;
|
||||
import fr.free.nrw.commons.category.CategoryDetailsActivity;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
|
||||
|
|
@ -58,9 +59,12 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
|
|||
String query;
|
||||
@BindView(R.id.bottomProgressBar)
|
||||
ProgressBar bottomProgressBar;
|
||||
boolean isLoadingCategories;
|
||||
|
||||
@Inject RecentSearchesDao recentSearchesDao;
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject CategoryClient categoryClient;
|
||||
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
JsonKvStore basicKvStore;
|
||||
|
|
@ -135,33 +139,36 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
|
|||
progressBar.setVisibility(GONE);
|
||||
queryList.clear();
|
||||
categoriesAdapter.clear();
|
||||
compositeDisposable.add(Observable.fromCallable(() -> 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<String>::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<String>::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<String> 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<String> 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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
164
app/src/main/java/fr/free/nrw/commons/media/MediaClient.java
Normal file
164
app/src/main/java/fr/free/nrw/commons/media/MediaClient.java
Normal file
|
|
@ -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<String, Map<String, String>> 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<Boolean> 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<Boolean> 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<List<Media>> 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<List<Media>> 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<List<Media>> responseToMediaList(Observable<MwQueryResponse> 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<Media>::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<Media> 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<Media> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MwQueryResponse> 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<MwQueryResponse> 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<MwQueryResponse> getMediaListFromCategory(@Query("gcmtitle") String category, @Query("gcmlimit") int itemLimit, @QueryMap Map<String, String> 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<MwQueryResponse> getMediaListFromSearch(@Query("gsrsearch") String keyword, @Query("gsrlimit") int itemLimit, @QueryMap Map<String, String> 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<MwQueryResponse> 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<MwQueryResponse> getMediaWithGenerator(@Query("titles") String title);
|
||||
}
|
||||
|
|
@ -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.";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<PageModifier> 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<PageModifier> getModifiers() {
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
Uri getContentUri() {
|
||||
return contentUri;
|
||||
}
|
||||
|
||||
void setContentUri(Uri contentUri) {
|
||||
this.contentUri = contentUri;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<ContentProviderClient> clientProvider;
|
||||
|
||||
@Inject
|
||||
public ModifierSequenceDao(@Named("modification") Provider<ContentProviderClient> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <nowiki> 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) + ".";
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Boolean> 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<String> searchCategories(String filterValue, int searchCatsLimit) {
|
||||
List<String> categories = new ArrayList<>();
|
||||
return Single.fromCallable(() -> {
|
||||
List<CustomApiResult> 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<String>();
|
||||
}
|
||||
|
||||
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<String> allCategories(String filterValue, int searchCatsLimit) {
|
||||
return Single.fromCallable(() -> {
|
||||
ArrayList<CustomApiResult> 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<String>();
|
||||
}
|
||||
|
||||
List<String> 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<String> searchTitles(String title, int searchCatsLimit) {
|
||||
return Single.fromCallable((Callable<List<String>>) () -> {
|
||||
ArrayList<CustomApiResult> 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<String> 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<Notification> 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<String> 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<String> 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<String> searchCategory(String query, int offset) {
|
||||
List<CustomApiResult> 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<String> 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<UploadStash> 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<UploadResult> 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
|
||||
|
|
|
|||
|
|
@ -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<Boolean> pageExists(String pageName);
|
||||
|
||||
List<String> getSubCategoryList(String categoryName);
|
||||
|
||||
List<String> getParentCategoryList(String categoryName);
|
||||
|
||||
@NonNull
|
||||
List<String> searchCategory(String title, int offset);
|
||||
|
||||
@NonNull
|
||||
Single<UploadStash> uploadFile(String filename, InputStream file,
|
||||
long dataLength, Uri fileUri, Uri contentProviderUri,
|
||||
final ProgressListener progressListener);
|
||||
|
||||
@NonNull
|
||||
Single<UploadResult> 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<String> parseWikicode(String source);
|
||||
|
||||
@NonNull
|
||||
Single<MediaResult> fetchMediaByFilename(String filename);
|
||||
|
||||
@NonNull
|
||||
Observable<String> searchCategories(String filterValue, int searchCatsLimit);
|
||||
|
||||
@NonNull
|
||||
Observable<String> allCategories(String filter, int searchCatsLimit);
|
||||
|
||||
@NonNull
|
||||
List<Notification> getNotifications(boolean archived) throws IOException;
|
||||
|
||||
@NonNull
|
||||
boolean markNotificationAsRead(Notification notification) throws IOException;
|
||||
|
||||
@NonNull
|
||||
Observable<String> 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<CampaignResponseDTO> getCampaigns();
|
||||
|
||||
boolean thank(String editToken, long revision) throws IOException;
|
||||
|
||||
interface ProgressListener {
|
||||
void onProgress(long transferred, long total);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Map<String, String>>() {
|
||||
}.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<Media> 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<Media> 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<List<Media>> 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<Media> 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<MwQueryPage> 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<String, String> continueValues = getContinueValues(query);
|
||||
if (continueValues != null && continueValues.size() > 0) {
|
||||
for (Map.Entry<String, String> 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<String, String> 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<String, String> getContinueValues(String keyword) {
|
||||
return defaultKvStore.getJson("query_continue_" + keyword, mapType);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +82,8 @@ public class NotificationActivity extends NavigationBaseActivity {
|
|||
|
||||
@SuppressLint("CheckResult")
|
||||
public void removeNotification(Notification notification) {
|
||||
compositeDisposable.add(Observable.fromCallable(() -> controller.markAsRead(notification))
|
||||
Disposable disposable = Observable.defer((Callable<ObservableSource<Boolean>>)
|
||||
() -> controller.markAsRead(notification))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
|
|
@ -95,8 +100,7 @@ public class NotificationActivity extends NavigationBaseActivity {
|
|||
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);
|
||||
|
||||
})
|
||||
compositeDisposable.add(controller.getNotifications(archived)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(notificationList -> {
|
||||
|
|
|
|||
|
|
@ -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<List<Notification>> 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<Boolean> markNotificationAsRead(String notificationId) {
|
||||
try {
|
||||
return service.markRead(csrfTokenClient.getTokenBlocking(), notificationId, "")
|
||||
.map(mwQueryResponse -> mwQueryResponse.success());
|
||||
} catch (Throwable throwable) {
|
||||
return Observable.just(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Notification> 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 boolean markAsRead(Notification notification) throws IOException{
|
||||
return mediaWikiApi.markNotificationAsRead(notification);
|
||||
public Single<List<Notification>> getNotifications(boolean archived) {
|
||||
return notificationClient.getNotifications(archived);
|
||||
}
|
||||
|
||||
Observable<Boolean> markAsRead(Notification notification) {
|
||||
return notificationClient.markNotificationAsRead(notification.notificationId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Notification> getNotificationsFromBundle(Context context, Node document) {
|
||||
Element bundledNotifications = getBundledNotifications(document);
|
||||
NodeList childNodes = bundledNotifications.getChildNodes();
|
||||
|
||||
List<Notification> 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<Notification> getNotificationsFromList(Context context, NodeList childNodes) {
|
||||
List<Notification> 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("<strong>", "").replace("</strong>", "");
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String getNotificationBody(Node document) {
|
||||
Node body = getNode(getModel(document), "body");
|
||||
if (body != null) {
|
||||
String textContent = body.getTextContent();
|
||||
return textContent.replace("<strong>", "").replace("</strong>", "");
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<String> 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;
|
||||
})
|
||||
Observable.defer((Callable<ObservableSource<Boolean>>) () ->
|
||||
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);
|
||||
|
||||
String editToken;
|
||||
String authCookie;
|
||||
authCookie = sessionManager.getAuthCookie();
|
||||
mwApi.setAuthCookie(authCookie);
|
||||
|
||||
try {
|
||||
editToken = mwApi.getEditToken();
|
||||
if (editToken == null) {
|
||||
return false;
|
||||
if (firstRevision == null) {
|
||||
return;
|
||||
}
|
||||
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<ObservableSource<Boolean>>) () -> 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,6 +168,12 @@ public class ReviewController {
|
|||
message = context.getString(R.string.send_thank_failure_message, media.getDisplayTitle());
|
||||
}
|
||||
|
||||
showNotification(title, message);
|
||||
|
||||
}, Timber::e);
|
||||
}
|
||||
|
||||
private void showNotification(String title, String message) {
|
||||
notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||
.setContentTitle(title)
|
||||
.setStyle(new NotificationCompat.BigTextStyle()
|
||||
|
|
@ -222,8 +183,6 @@ public class ReviewController {
|
|||
.setOngoing(false)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH);
|
||||
notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build());
|
||||
|
||||
}, Timber::e);
|
||||
}
|
||||
|
||||
public interface ReviewCallback {
|
||||
|
|
|
|||
|
|
@ -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<RecentChange> 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<Media> 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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<UploadResult> 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<UploadResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UploadResponse> 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<UploadResponse> 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Contribution> {
|
|||
@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<Contribution> {
|
|||
return;
|
||||
}
|
||||
String notificationTag = localUri.toString();
|
||||
|
||||
File file1;
|
||||
try {
|
||||
File file1 = new File(localUri.getPath());
|
||||
file1 = new File(localUri.getPath());
|
||||
fileInputStream = new FileInputStream(file1);
|
||||
} catch (FileNotFoundException e) {
|
||||
Timber.d("File not found");
|
||||
|
|
@ -229,22 +234,8 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
contribution
|
||||
);
|
||||
|
||||
Single.fromCallable(() -> {
|
||||
if (!mwApi.validateLogin()) {
|
||||
// Need to revalidate!
|
||||
if (sessionManager.revalidateAuthToken()) {
|
||||
Timber.d("Successfully revalidated token!");
|
||||
} else {
|
||||
Timber.d("Unable to revalidate :(");
|
||||
stopForeground(true);
|
||||
sessionManager.forceLogin(UploadService.this);
|
||||
throw new RuntimeException(getString(R.string.authentication_failed));
|
||||
}
|
||||
}
|
||||
return "Temp_" + contribution.hashCode() + filename;
|
||||
}).flatMap(stashFilename -> mwApi.uploadFile(
|
||||
stashFilename, fileInputStream, contribution.getDataLength(),
|
||||
localUri, contribution.getContentProviderUri(), notificationUpdater))
|
||||
Observable.fromCallable(() -> "Temp_" + contribution.hashCode() + filename)
|
||||
.flatMap(stashFilename -> uploadClient.uploadFileToStash(getApplicationContext(), stashFilename, file1))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.doFinally(() -> {
|
||||
|
|
@ -263,22 +254,20 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
|
||||
Timber.d("Stash upload response 1 is %s", uploadStash.toString());
|
||||
|
||||
String resultStatus = uploadStash.getResultStatus();
|
||||
String resultStatus = uploadStash.getResult();
|
||||
if (!resultStatus.equals("Success")) {
|
||||
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
|
||||
showFailedNotification(contribution);
|
||||
return Single.never();
|
||||
return Observable.never();
|
||||
} else {
|
||||
synchronized (unfinishedUploads) {
|
||||
Timber.d("making sure of uniqueness of name: %s", filename);
|
||||
String uniqueFilename = findUniqueFilename(filename);
|
||||
unfinishedUploads.add(uniqueFilename);
|
||||
return mwApi.uploadFileFinalize(
|
||||
return uploadClient.uploadFileFromStash(
|
||||
getApplicationContext(),
|
||||
contribution,
|
||||
uniqueFilename,
|
||||
uploadStash.getFilekey(),
|
||||
contribution.getPageContents(getApplicationContext()),
|
||||
contribution.getEditSummary());
|
||||
}
|
||||
uploadStash.getFilekey());
|
||||
}
|
||||
})
|
||||
.subscribe(uploadResult -> {
|
||||
|
|
@ -286,19 +275,20 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
|
||||
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
|
||||
|
||||
String resultStatus = uploadResult.getResultStatus();
|
||||
String resultStatus = uploadResult.getResult();
|
||||
if (!resultStatus.equals("Success")) {
|
||||
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
|
||||
showFailedNotification(contribution);
|
||||
} else {
|
||||
String canonicalFilename = uploadResult.getCanonicalFilename();
|
||||
String canonicalFilename = "File:" + uploadResult.getFilename();
|
||||
Timber.d("Contribution upload success. Initiating Wikidata edit for entity id %s",
|
||||
contribution.getWikiDataEntityId());
|
||||
wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), canonicalFilename);
|
||||
contribution.setFilename(canonicalFilename);
|
||||
contribution.setImageUrl(uploadResult.getImageUrl());
|
||||
contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl());
|
||||
contribution.setState(Contribution.STATE_COMPLETED);
|
||||
contribution.setDateUploaded(uploadResult.getDateUploaded());
|
||||
contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatShort()
|
||||
.parse(uploadResult.getImageinfo().getTimestamp()));
|
||||
contributionDao.save(contribution);
|
||||
}
|
||||
}, throwable -> {
|
||||
|
|
@ -337,7 +327,7 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2");
|
||||
}
|
||||
}
|
||||
if (!mwApi.fileExistsWithName(sequenceFileName)
|
||||
if (!mediaClient.checkPageExistsUsingTitle(sequenceFileName).blockingGet()
|
||||
&& !unfinishedUploads.contains(sequenceFileName)) {
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<Long> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<Media> bookmarkedPictures = bookmarkPicturesController.loadBookmarkedPictures().blockingGet();
|
||||
assertEquals(1, bookmarkedPictures.size());
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
199
app/src/test/kotlin/fr/free/nrw/commons/media/MediaClientTest.kt
Normal file
199
app/src/test/kotlin/fr/free/nrw/commons/media/MediaClientTest.kt
Normal file
|
|
@ -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<String>? = 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<Map<String, String>>? = 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<String, String>())
|
||||
assertEquals(continuationCaptor.allValues[1], mockContinuation)
|
||||
|
||||
assertEquals(media1.filename, "Test")
|
||||
assertEquals(media2.filename, "Test")
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ContentValues>()
|
||||
|
||||
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<SQLiteDatabase>(database).execSQL(DROP_TABLE_STATEMENT)
|
||||
verify<SQLiteDatabase>(database).execSQL(CREATE_TABLE_STATEMENT)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteTable() {
|
||||
onDelete(database)
|
||||
|
||||
inOrder(database) {
|
||||
verify<SQLiteDatabase>(database).execSQL(DROP_TABLE_STATEMENT)
|
||||
verify<SQLiteDatabase>(database).execSQL(CREATE_TABLE_STATEMENT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCursor(modifierJson: String) = MatrixCursor(columns, 1).apply {
|
||||
addRow(listOf("1", mediaUrl, "{\"modifiers\": [$modifierJson]}"))
|
||||
moveToFirst()
|
||||
}
|
||||
}
|
||||
|
|
@ -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("<?xml version=\"1.0\"?><api batchcomplete=\"\"><query><tokens logintoken=\"baz\" /></query></api>"))
|
||||
server.enqueue(MockResponse().setBody("<?xml version=\"1.0\"?><api><clientlogin status=\"FAIL\" message=\"Incorrect password entered. Please try again.\" messagecode=\"wrongpassword\" /></api>"))
|
||||
|
||||
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("<?xml version=\"1.0\"?><api batchcomplete=\"\"><query><tokens logintoken=\"baz\" /></query></api>"))
|
||||
server.enqueue(MockResponse().setBody("<?xml version=\"1.0\"?><api><clientlogin status=\"PASS\" username=\"foo\" /></api>"))
|
||||
|
||||
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("<?xml version=\"1.0\"?><api batchcomplete=\"\"><query><tokens logintoken=\"baz\" /></query></api>"))
|
||||
server.enqueue(MockResponse().setBody("<?xml version=\"1.0\"?><api><clientlogin status=\"PASS\" username=\"foo\" /></api>"))
|
||||
|
||||
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("<?xml version=\"1.0\"?><api><query><userinfo id=\"10\" name=\"foo\"/></query></api>"))
|
||||
|
||||
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("<?xml version=\"1.0\"?><api><query><userinfo id=\"0\" name=\"foo\"/></query></api>"))
|
||||
|
||||
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("<?xml version=\"1.0\"?><api><query><tokens csrftoken=\"baz\" /></query></api>"))
|
||||
|
||||
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("<?xml version=\"1.0\"?><api><centralauthtoken centralauthtoken=\"abc\" /></api>"))
|
||||
wikidataServer.enqueue(MockResponse().setBody("<?xml version=\"1.0\"?><api><query><tokens csrftoken=\"baz\" /></query></api>"))
|
||||
|
||||
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("<?xml version=\"1.0\"?><api batchcomplete=\"\"><query> <normalized><n from=\"File:foo\" to=\"File:Foo\" /></normalized><pages><page _idx=\"-1\" ns=\"6\" title=\"File:Foo\" missing=\"\" imagerepository=\"\" /></pages></query></api>"))
|
||||
|
||||
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("<?xml version=\"1.0\"?><api><query><userinfo id=\"1000\" name=\"testusername\" blockid=\"3000\" blockedby=\"blockerusername\" blockedbyid=\"1001\" blockreason=\"testing\" blockedtimestamp=\"2018-05-24T15:32:09Z\" blockexpiry=\"infinite\"></userinfo></query></api>"))
|
||||
|
|
|
|||
|
|
@ -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<HashMap<String, String>>("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<HashMap<String, String>>("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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue