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 {
|
plugins {
|
||||||
id 'com.github.triplet.play' version '2.2.1' apply false
|
id 'com.github.triplet.play' version '2.2.1' apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: '../gitutils.gradle'
|
apply from: '../gitutils.gradle'
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
|
@ -31,8 +30,8 @@ dependencies {
|
||||||
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1'
|
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1'
|
||||||
implementation 'com.facebook.fresco:fresco:1.13.0'
|
implementation 'com.facebook.fresco:fresco:1.13.0'
|
||||||
implementation 'com.drewnoakes:metadata-extractor:2.11.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 'org.apache.commons:commons-lang3:3.8.1'
|
||||||
|
implementation 'com.dmitrybrant:wikimedia-android-data-client:0.0.25'
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
|
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", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
|
||||||
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
|
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
|
||||||
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.org\""
|
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", "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_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\""
|
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", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
|
||||||
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""
|
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""
|
||||||
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\""
|
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", "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_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\""
|
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>
|
||||||
<service
|
<service
|
||||||
android:name=".contributions.ContributionsSyncService"
|
android:name=".contributions.ContributionsSyncService"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:process=":sync">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.content.SyncAdapter" />
|
<action android:name="android.content.SyncAdapter" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
@ -161,17 +162,6 @@
|
||||||
android:name="android.content.SyncAdapter"
|
android:name="android.content.SyncAdapter"
|
||||||
android:resource="@xml/contributions_sync_adapter" />
|
android:resource="@xml/contributions_sync_adapter" />
|
||||||
</service>
|
</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
|
<service
|
||||||
android:name="org.acra.sender.SenderService"
|
android:name="org.acra.sender.SenderService"
|
||||||
|
|
@ -193,12 +183,7 @@
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/provider_contributions"
|
android:label="@string/provider_contributions"
|
||||||
android:syncable="true" />
|
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
|
<provider
|
||||||
android:name=".category.CategoryContentProvider"
|
android:name=".category.CategoryContentProvider"
|
||||||
android:authorities="${applicationId}.categories.contentprovider"
|
android:authorities="${applicationId}.categories.contentprovider"
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ public class CommonsAppAdapter extends AppAdapter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateAccount(@NonNull LoginResult result) {
|
public void updateAccount(@NonNull LoginResult result) {
|
||||||
// TODO: sessionManager.updateAccount(result);
|
sessionManager.updateAccount(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -69,7 +69,8 @@ public class CommonsAppAdapter extends AppAdapter {
|
||||||
if (!preferences.contains(COOKIE_STORE_NAME)) {
|
if (!preferences.contains(COOKIE_STORE_NAME)) {
|
||||||
return null;
|
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
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import android.os.Build;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||||
import com.facebook.imagepipeline.core.ImagePipelineConfig;
|
import com.facebook.imagepipeline.core.ImagePipelineConfig;
|
||||||
import com.squareup.leakcanary.LeakCanary;
|
import com.squareup.leakcanary.LeakCanary;
|
||||||
|
|
@ -28,7 +30,6 @@ import java.io.File;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
||||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
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.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.logging.FileLoggingTree;
|
import fr.free.nrw.commons.logging.FileLoggingTree;
|
||||||
import fr.free.nrw.commons.logging.LogUtils;
|
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.upload.FileUtils;
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
|
@ -265,7 +265,6 @@ public class CommonsApplication extends Application {
|
||||||
dbOpenHelper.getReadableDatabase().close();
|
dbOpenHelper.getReadableDatabase().close();
|
||||||
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
||||||
|
|
||||||
ModifierSequenceDao.Table.onDelete(db);
|
|
||||||
CategoryDao.Table.onDelete(db);
|
CategoryDao.Table.onDelete(db);
|
||||||
ContributionDao.Table.onDelete(db);
|
ContributionDao.Table.onDelete(db);
|
||||||
BookmarkPicturesDao.Table.onDelete(db);
|
BookmarkPicturesDao.Table.onDelete(db);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import android.net.Uri;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryPage;
|
import org.wikipedia.dataclient.mwapi.MwQueryPage;
|
||||||
import org.wikipedia.gallery.ExtMetadata;
|
import org.wikipedia.gallery.ExtMetadata;
|
||||||
|
|
@ -20,14 +23,13 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||||
import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
|
import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
|
||||||
|
|
||||||
public class Media implements Parcelable {
|
public class Media implements Parcelable {
|
||||||
|
|
||||||
|
public static final Media EMPTY = new Media("");
|
||||||
public static Creator<Media> CREATOR = new Creator<Media>() {
|
public static Creator<Media> CREATOR = new Creator<Media>() {
|
||||||
@Override
|
@Override
|
||||||
public Media createFromParcel(Parcel parcel) {
|
public Media createFromParcel(Parcel parcel) {
|
||||||
|
|
@ -156,9 +158,9 @@ public class Media implements Parcelable {
|
||||||
page.title(),
|
page.title(),
|
||||||
"",
|
"",
|
||||||
0,
|
0,
|
||||||
safeParseDate(metadata.dateTimeOriginal().value()),
|
safeParseDate(metadata.dateTime()),
|
||||||
safeParseDate(metadata.dateTime().value()),
|
safeParseDate(metadata.dateTime()),
|
||||||
StringUtil.fromHtml(metadata.artist().value()).toString()
|
StringUtil.fromHtml(metadata.artist()).toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
|
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
|
||||||
|
|
@ -170,17 +172,17 @@ public class Media implements Parcelable {
|
||||||
language = "default";
|
language = "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription().value()));
|
media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription()));
|
||||||
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories().value()));
|
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories()));
|
||||||
String latitude = metadata.gpsLatitude().value();
|
String latitude = metadata.getGpsLatitude();
|
||||||
String longitude = metadata.gpsLongitude().value();
|
String longitude = metadata.getGpsLongitude();
|
||||||
|
|
||||||
if (!StringUtils.isBlank(latitude) && !StringUtils.isBlank(longitude)) {
|
if (!StringUtils.isBlank(latitude) && !StringUtils.isBlank(longitude)) {
|
||||||
LatLng latLng = new LatLng(Double.parseDouble(latitude), Double.parseDouble(longitude), 0);
|
LatLng latLng = new LatLng(Double.parseDouble(latitude), Double.parseDouble(longitude), 0);
|
||||||
media.setCoordinates(latLng);
|
media.setCoordinates(latLng);
|
||||||
}
|
}
|
||||||
|
|
||||||
media.setLicenseInformation(metadata.licenseShortName().value(), metadata.licenseUrl().value());
|
media.setLicenseInformation(metadata.licenseShortName(), metadata.licenseUrl());
|
||||||
return media;
|
return media;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
|
import androidx.core.text.HtmlCompat;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
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.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
|
|
@ -19,12 +21,15 @@ import timber.log.Timber;
|
||||||
public class MediaDataExtractor {
|
public class MediaDataExtractor {
|
||||||
private final MediaWikiApi mediaWikiApi;
|
private final MediaWikiApi mediaWikiApi;
|
||||||
private final OkHttpJsonApiClient okHttpJsonApiClient;
|
private final OkHttpJsonApiClient okHttpJsonApiClient;
|
||||||
|
private final MediaClient mediaClient;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public MediaDataExtractor(MediaWikiApi mwApi,
|
public MediaDataExtractor(MediaWikiApi mwApi,
|
||||||
OkHttpJsonApiClient okHttpJsonApiClient) {
|
OkHttpJsonApiClient okHttpJsonApiClient,
|
||||||
|
MediaClient mediaClient) {
|
||||||
this.okHttpJsonApiClient = okHttpJsonApiClient;
|
this.okHttpJsonApiClient = okHttpJsonApiClient;
|
||||||
this.mediaWikiApi = mwApi;
|
this.mediaWikiApi = mwApi;
|
||||||
|
this.mediaClient = mediaClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -35,7 +40,7 @@ public class MediaDataExtractor {
|
||||||
*/
|
*/
|
||||||
public Single<Media> fetchMediaDetails(String filename) {
|
public Single<Media> fetchMediaDetails(String filename) {
|
||||||
Single<Media> mediaSingle = getMediaFromFileName(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);
|
Single<String> discussionSingle = getDiscussion(filename);
|
||||||
return Single.zip(mediaSingle, pageExistsSingle, discussionSingle, (media, deletionStatus, discussion) -> {
|
return Single.zip(mediaSingle, pageExistsSingle, discussionSingle, (media, deletionStatus, discussion) -> {
|
||||||
media.setDiscussion(discussion);
|
media.setDiscussion(discussion);
|
||||||
|
|
@ -52,7 +57,7 @@ public class MediaDataExtractor {
|
||||||
* @return return data rich Media object
|
* @return return data rich Media object
|
||||||
*/
|
*/
|
||||||
public Single<Media> getMediaFromFileName(String filename) {
|
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.Account;
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
@ -12,10 +12,8 @@ public class AccountUtil {
|
||||||
|
|
||||||
public static final String AUTH_COOKIE = "authCookie";
|
public static final String AUTH_COOKIE = "authCookie";
|
||||||
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
|
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
public AccountUtil(Context context) {
|
public AccountUtil() {
|
||||||
this.context = context;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -49,5 +47,4 @@ public class AccountUtil {
|
||||||
private static AccountManager accountManager(Context context) {
|
private static AccountManager accountManager(Context context) {
|
||||||
return AccountManager.get(context);
|
return AccountManager.get(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,48 +12,19 @@ import io.reactivex.Observable;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
|
|
||||||
|
|
||||||
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
|
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
protected SessionManager sessionManager;
|
protected SessionManager sessionManager;
|
||||||
@Inject
|
@Inject
|
||||||
MediaWikiApi mediaWikiApi;
|
MediaWikiApi mediaWikiApi;
|
||||||
private String authCookie;
|
|
||||||
|
|
||||||
protected void requestAuthToken() {
|
|
||||||
if (authCookie != null) {
|
|
||||||
onAuthCookieAcquired(authCookie);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
authCookie = sessionManager.getAuthCookie();
|
|
||||||
if (authCookie != null) {
|
|
||||||
onAuthCookieAcquired(authCookie);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
authCookie = savedInstanceState.getString(AUTH_COOKIE);
|
|
||||||
}
|
|
||||||
|
|
||||||
showBlockStatus();
|
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
|
* Makes API call to check if user is blocked from Commons. If the user is blocked, a snackbar
|
||||||
* is created to notify the user
|
* is created to notify the user
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
package fr.free.nrw.commons.auth;
|
package fr.free.nrw.commons.auth;
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountAuthenticatorActivity;
|
import android.accounts.AccountAuthenticatorActivity;
|
||||||
import android.accounts.AccountAuthenticatorResponse;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
@ -20,14 +17,6 @@ import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.TextView;
|
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.ColorRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
|
|
@ -35,6 +24,17 @@ import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatDelegate;
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
import androidx.core.app.NavUtils;
|
import androidx.core.app.NavUtils;
|
||||||
import androidx.core.content.ContextCompat;
|
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.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import butterknife.OnClick;
|
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.theme.NavigationBaseActivity;
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Completable;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
import io.reactivex.functions.Action;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.view.KeyEvent.KEYCODE_ENTER;
|
import static android.view.KeyEvent.KEYCODE_ENTER;
|
||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
|
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 {
|
public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
|
|
||||||
|
|
@ -71,10 +72,17 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
@Inject
|
@Inject
|
||||||
SessionManager sessionManager;
|
SessionManager sessionManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@Named(NAMED_COMMONS_WIKI_SITE)
|
||||||
|
WikiSite commonsWikiSite;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@Named("default_preferences")
|
@Named("default_preferences")
|
||||||
JsonKvStore applicationKvStore;
|
JsonKvStore applicationKvStore;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
LoginClient loginClient;
|
||||||
|
|
||||||
@BindView(R.id.login_button)
|
@BindView(R.id.login_button)
|
||||||
Button loginButton;
|
Button loginButton;
|
||||||
|
|
||||||
|
|
@ -104,13 +112,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
||||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
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
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
@ -211,10 +212,8 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionManager.getCurrentAccount() != null
|
if (sessionManager.getCurrentAccount() != null
|
||||||
&& sessionManager.isUserLoggedIn()
|
&& sessionManager.isUserLoggedIn()) {
|
||||||
&& sessionManager.getCachedAuthCookie() != null) {
|
|
||||||
applicationKvStore.putBoolean("login_skipped", false);
|
applicationKvStore.putBoolean("login_skipped", false);
|
||||||
sessionManager.revalidateAuthToken();
|
|
||||||
startMainActivity();
|
startMainActivity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,7 +243,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
|
|
||||||
@OnClick(R.id.login_button)
|
@OnClick(R.id.login_button)
|
||||||
public void performLogin() {
|
public void performLogin() {
|
||||||
loginCurrentlyInProgress = true;
|
|
||||||
Timber.d("Login to start!");
|
Timber.d("Login to start!");
|
||||||
final String username = usernameEdit.getText().toString();
|
final String username = usernameEdit.getText().toString();
|
||||||
final String rawUsername = usernameEdit.getText().toString().trim();
|
final String rawUsername = usernameEdit.getText().toString().trim();
|
||||||
|
|
@ -252,24 +250,38 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
String twoFactorCode = twoFactorEdit.getText().toString();
|
String twoFactorCode = twoFactorEdit.getText().toString();
|
||||||
|
|
||||||
showLoggingProgressBar();
|
showLoggingProgressBar();
|
||||||
compositeDisposable.add(Observable.fromCallable(() -> login(username, password, twoFactorCode))
|
doLogin(username, password, twoFactorCode);
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(result -> handleLogin(username, rawUsername, password, result)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String login(String username, String password, String twoFactorCode) {
|
private void doLogin(String username, String password, String twoFactorCode) {
|
||||||
|
progressDialog.show();
|
||||||
|
|
||||||
|
Action action = () -> {
|
||||||
try {
|
try {
|
||||||
if (twoFactorCode.isEmpty()) {
|
loginClient.loginBlocking(commonsWikiSite, username, password, twoFactorCode);
|
||||||
return mwApi.login(username, password);
|
} catch (Throwable throwable) {
|
||||||
} else {
|
throwable.printStackTrace();
|
||||||
return mwApi.login(username, password, twoFactorCode);
|
|
||||||
}
|
}
|
||||||
} 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.
|
* This function is called when user skips the login.
|
||||||
|
|
@ -281,18 +293,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
finish();
|
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() {
|
private void showLoggingProgressBar() {
|
||||||
progressDialog = new ProgressDialog(this);
|
progressDialog = new ProgressDialog(this);
|
||||||
progressDialog.setIndeterminate(true);
|
progressDialog.setIndeterminate(true);
|
||||||
|
|
@ -302,67 +302,19 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
progressDialog.show();
|
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();
|
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();
|
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
|
@Override
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
@ -402,30 +354,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
return getDelegate().getMenuInflater();
|
return getDelegate().getMenuInflater();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putBoolean(LOGGING_IN, loginCurrentlyInProgress);
|
|
||||||
outState.putBoolean(ERROR_MESSAGE_SHOWN, errorMessageShown);
|
|
||||||
outState.putString(RESULTANT_ERROR, resultantError);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
|
||||||
super.onRestoreInstanceState(savedInstanceState);
|
|
||||||
loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGGING_IN, false);
|
|
||||||
errorMessageShown = savedInstanceState.getBoolean(ERROR_MESSAGE_SHOWN, false);
|
|
||||||
if (loginCurrentlyInProgress) {
|
|
||||||
performLogin();
|
|
||||||
}
|
|
||||||
if (errorMessageShown) {
|
|
||||||
resultantError = savedInstanceState.getString(RESULTANT_ERROR);
|
|
||||||
if (resultantError != null) {
|
|
||||||
handleOtherResults(resultantError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void askUserForTwoFactorAuth() {
|
public void askUserForTwoFactorAuth() {
|
||||||
progressDialog.dismiss();
|
progressDialog.dismiss();
|
||||||
twoFactorContainer.setVisibility(VISIBLE);
|
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() {
|
public void showSuccessAndDismissDialog() {
|
||||||
showMessage(R.string.login_success, R.color.primaryDarkColor);
|
showMessage(R.string.login_success, R.color.primaryDarkColor);
|
||||||
progressDialog.dismiss();
|
progressDialog.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void emptySensitiveEditFields() {
|
|
||||||
passwordEdit.setText("");
|
|
||||||
twoFactorEdit.setText("");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startMainActivity() {
|
public void startMainActivity() {
|
||||||
NavigationBaseActivity.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
NavigationBaseActivity.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
finish();
|
finish();
|
||||||
|
|
@ -461,6 +391,12 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
errorMessageContainer.setVisibility(VISIBLE);
|
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() {
|
private AppCompatDelegate getDelegate() {
|
||||||
if (delegate == null) {
|
if (delegate == null) {
|
||||||
delegate = AppCompatDelegate.create(this, null);
|
delegate = AppCompatDelegate.create(this, null);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
package fr.free.nrw.commons.auth;
|
package fr.free.nrw.commons.auth;
|
||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.accounts.AccountAuthenticatorResponse;
|
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
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 androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.wikipedia.login.LoginResult;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
@ -20,10 +22,6 @@ import io.reactivex.Completable;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import timber.log.Timber;
|
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.
|
* Manage the current logged in user session.
|
||||||
*/
|
*/
|
||||||
|
|
@ -34,7 +32,6 @@ public class SessionManager {
|
||||||
private Account currentAccount; // Unlike a savings account... ;-)
|
private Account currentAccount; // Unlike a savings account... ;-)
|
||||||
private JsonKvStore defaultKvStore;
|
private JsonKvStore defaultKvStore;
|
||||||
private static final String KEY_RAWUSERNAME = "rawusername";
|
private static final String KEY_RAWUSERNAME = "rawusername";
|
||||||
private Bundle userdata = new Bundle();
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public SessionManager(Context context,
|
public SessionManager(Context context,
|
||||||
|
|
@ -46,43 +43,40 @@ public class SessionManager {
|
||||||
this.defaultKvStore = defaultKvStore;
|
this.defaultKvStore = defaultKvStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private boolean createAccount(@NonNull String userName, @NonNull String password) {
|
||||||
* Creata a new account
|
Account account = getCurrentAccount();
|
||||||
*
|
if (account == null || TextUtils.isEmpty(account.name) || !account.name.equals(userName)) {
|
||||||
* @param response
|
removeAccount();
|
||||||
* @param username
|
account = new Account(userName, BuildConfig.ACCOUNT_TYPE);
|
||||||
* @param rawusername
|
return accountManager().addAccountExplicitly(account, password, null);
|
||||||
* @param password
|
}
|
||||||
*/
|
return true;
|
||||||
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 void removeAccount() {
|
||||||
|
Account account = getCurrentAccount();
|
||||||
|
if (account != null) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||||
|
accountManager().removeAccountExplicitly(account);
|
||||||
} else {
|
} else {
|
||||||
if (response != null) {
|
//noinspection deprecation
|
||||||
response.onError(ERROR_CODE_REMOTE_EXCEPTION, "");
|
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
|
public void updateAccount(LoginResult result) {
|
||||||
ContentResolver.setSyncAutomatically(account, BuildConfig.CONTRIBUTION_AUTHORITY, true); // Enable sync by default!
|
boolean accountCreated = createAccount(result.getUserName(), result.getPassword());
|
||||||
ContentResolver.setSyncAutomatically(account, BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default!
|
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
|
@Nullable
|
||||||
public String getRawUserName() {
|
private String getRawUserName() {
|
||||||
Account account = getCurrentAccount();
|
Account account = getCurrentAccount();
|
||||||
return account == null ? null : accountManager().getUserData(account, KEY_RAWUSERNAME);
|
return account == null ? null : accountManager().getUserData(account, KEY_RAWUSERNAME);
|
||||||
}
|
}
|
||||||
|
|
@ -127,46 +121,14 @@ public class SessionManager {
|
||||||
return AccountManager.get(context);
|
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() {
|
public boolean isUserLoggedIn() {
|
||||||
return defaultKvStore.getBoolean("isUserLoggedIn", false);
|
return defaultKvStore.getBoolean("isUserLoggedIn", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setUserLoggedIn(boolean isLoggedIn) {
|
||||||
|
defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn);
|
||||||
|
}
|
||||||
|
|
||||||
public void forceLogin(Context context) {
|
public void forceLogin(Context context) {
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
LoginActivity.startYourself(context);
|
LoginActivity.startYourself(context);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import javax.inject.Singleton;
|
||||||
|
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.bookmarks.Bookmark;
|
import fr.free.nrw.commons.bookmarks.Bookmark;
|
||||||
|
import fr.free.nrw.commons.media.MediaClient;
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.ObservableSource;
|
import io.reactivex.ObservableSource;
|
||||||
|
|
@ -19,15 +20,14 @@ import io.reactivex.functions.Function;
|
||||||
@Singleton
|
@Singleton
|
||||||
public class BookmarkPicturesController {
|
public class BookmarkPicturesController {
|
||||||
|
|
||||||
private final OkHttpJsonApiClient okHttpJsonApiClient;
|
private final MediaClient mediaClient;
|
||||||
private final BookmarkPicturesDao bookmarkDao;
|
private final BookmarkPicturesDao bookmarkDao;
|
||||||
|
|
||||||
private List<Bookmark> currentBookmarks;
|
private List<Bookmark> currentBookmarks;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public BookmarkPicturesController(OkHttpJsonApiClient okHttpJsonApiClient,
|
public BookmarkPicturesController(MediaClient mediaClient, BookmarkPicturesDao bookmarkDao) {
|
||||||
BookmarkPicturesDao bookmarkDao) {
|
this.mediaClient = mediaClient;
|
||||||
this.okHttpJsonApiClient = okHttpJsonApiClient;
|
|
||||||
this.bookmarkDao = bookmarkDao;
|
this.bookmarkDao = bookmarkDao;
|
||||||
currentBookmarks = new ArrayList<>();
|
currentBookmarks = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +47,7 @@ public class BookmarkPicturesController {
|
||||||
|
|
||||||
private Observable<Media> getMediaFromBookmark(Bookmark bookmark) {
|
private Observable<Media> getMediaFromBookmark(Bookmark bookmark) {
|
||||||
Media dummyMedia = new Media("");
|
Media dummyMedia = new Media("");
|
||||||
return okHttpJsonApiClient.getMedia(bookmark.getMediaName(), false)
|
return mediaClient.getMedia(bookmark.getMediaName())
|
||||||
.map(media -> media == null ? dummyMedia : media)
|
.map(media -> media == null ? dummyMedia : media)
|
||||||
.onErrorReturn(throwable -> dummyMedia)
|
.onErrorReturn(throwable -> dummyMedia)
|
||||||
.toObservable();
|
.toObservable();
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ public class CategoriesModel{
|
||||||
private static final int SEARCH_CATS_LIMIT = 25;
|
private static final int SEARCH_CATS_LIMIT = 25;
|
||||||
|
|
||||||
private final MediaWikiApi mwApi;
|
private final MediaWikiApi mwApi;
|
||||||
|
private final CategoryClient categoryClient;
|
||||||
private final CategoryDao categoryDao;
|
private final CategoryDao categoryDao;
|
||||||
private final JsonKvStore directKvStore;
|
private final JsonKvStore directKvStore;
|
||||||
|
|
||||||
|
|
@ -32,9 +33,11 @@ public class CategoriesModel{
|
||||||
@Inject GpsCategoryModel gpsCategoryModel;
|
@Inject GpsCategoryModel gpsCategoryModel;
|
||||||
@Inject
|
@Inject
|
||||||
public CategoriesModel(MediaWikiApi mwApi,
|
public CategoriesModel(MediaWikiApi mwApi,
|
||||||
|
CategoryClient categoryClient,
|
||||||
CategoryDao categoryDao,
|
CategoryDao categoryDao,
|
||||||
@Named("default_preferences") JsonKvStore directKvStore) {
|
@Named("default_preferences") JsonKvStore directKvStore) {
|
||||||
this.mwApi = mwApi;
|
this.mwApi = mwApi;
|
||||||
|
this.categoryClient = categoryClient;
|
||||||
this.categoryDao = categoryDao;
|
this.categoryDao = categoryDao;
|
||||||
this.directKvStore = directKvStore;
|
this.directKvStore = directKvStore;
|
||||||
this.categoriesCache = new HashMap<>();
|
this.categoriesCache = new HashMap<>();
|
||||||
|
|
@ -134,8 +137,8 @@ public class CategoriesModel{
|
||||||
}
|
}
|
||||||
|
|
||||||
//otherwise, search API for matching categories
|
//otherwise, search API for matching categories
|
||||||
return mwApi
|
return categoryClient
|
||||||
.allCategories(term, SEARCH_CATS_LIMIT)
|
.searchCategoriesForPrefix(term, SEARCH_CATS_LIMIT)
|
||||||
.map(name -> new CategoryItem(name, false));
|
.map(name -> new CategoryItem(name, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,7 +201,7 @@ public class CategoriesModel{
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private Observable<CategoryItem> getTitleCategories(String title) {
|
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));
|
.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.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.DataSetObserver;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
|
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
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.explore.SearchActivity;
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
|
|
@ -28,7 +27,7 @@ import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class CategoryImagesActivity
|
public class CategoryImagesActivity
|
||||||
extends AuthenticatedActivity
|
extends NavigationBaseActivity
|
||||||
implements FragmentManager.OnBackStackChangedListener,
|
implements FragmentManager.OnBackStackChangedListener,
|
||||||
MediaDetailPagerFragment.MediaDetailProvider,
|
MediaDetailPagerFragment.MediaDetailProvider,
|
||||||
AdapterView.OnItemClickListener{
|
AdapterView.OnItemClickListener{
|
||||||
|
|
@ -38,16 +37,6 @@ public class CategoryImagesActivity
|
||||||
private CategoryImagesListFragment categoryImagesListFragment;
|
private CategoryImagesListFragment categoryImagesListFragment;
|
||||||
private MediaDetailPagerFragment mediaDetails;
|
private MediaDetailPagerFragment mediaDetails;
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onAuthCookieAcquired(String authCookie) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onAuthFailure() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called on backPressed of anyFragment in the activity.
|
* This method is called on backPressed of anyFragment in the activity.
|
||||||
* We are changing the icon here from back to hamburger icon.
|
* We are changing the icon here from back to hamburger icon.
|
||||||
|
|
@ -69,7 +58,6 @@ public class CategoryImagesActivity
|
||||||
supportFragmentManager = getSupportFragmentManager();
|
supportFragmentManager = getSupportFragmentManager();
|
||||||
setCategoryImagesFragment();
|
setCategoryImagesFragment();
|
||||||
supportFragmentManager.addOnBackStackChangedListener(this);
|
supportFragmentManager.addOnBackStackChangedListener(this);
|
||||||
requestAuthToken();
|
|
||||||
initDrawer();
|
initDrawer();
|
||||||
setPageTitle();
|
setPageTitle();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.explore.categories.ExploreActivity;
|
import fr.free.nrw.commons.explore.categories.ExploreActivity;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
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.NetworkUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
|
@ -56,7 +57,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
|
||||||
private boolean isLoading = true;
|
private boolean isLoading = true;
|
||||||
private String categoryName = null;
|
private String categoryName = null;
|
||||||
|
|
||||||
@Inject CategoryImageController controller;
|
@Inject MediaClient mediaClient;
|
||||||
@Inject
|
@Inject
|
||||||
@Named("default_preferences")
|
@Named("default_preferences")
|
||||||
JsonKvStore categoryKvStore;
|
JsonKvStore categoryKvStore;
|
||||||
|
|
@ -116,7 +117,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
|
||||||
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
progressBar.setVisibility(VISIBLE);
|
progressBar.setVisibility(VISIBLE);
|
||||||
compositeDisposable.add(controller.getCategoryImages(categoryName)
|
compositeDisposable.add(mediaClient.getMediaListFromCategory(categoryName)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
|
@ -222,7 +223,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
progressBar.setVisibility(VISIBLE);
|
progressBar.setVisibility(VISIBLE);
|
||||||
compositeDisposable.add(controller.getCategoryImages(categoryName)
|
compositeDisposable.add(mediaClient.getMediaListFromCategory(categoryName)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
.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;
|
TextView categoriesNotFoundView;
|
||||||
|
|
||||||
private String categoryName = null;
|
private String categoryName = null;
|
||||||
@Inject MediaWikiApi mwApi;
|
@Inject CategoryClient categoryClient;
|
||||||
|
|
||||||
private RVRendererAdapter<String> categoriesAdapter;
|
private RVRendererAdapter<String> categoriesAdapter;
|
||||||
private boolean isParentCategory = true;
|
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
|
* Clearing categoryAdapter every time new keyword is searched so that user can see only new results
|
||||||
*/
|
*/
|
||||||
public void initSubCategoryList() {
|
public void initSubCategoryList() {
|
||||||
|
|
@ -96,17 +96,19 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
if (!isParentCategory){
|
if (isParentCategory) {
|
||||||
compositeDisposable.add(Observable.fromCallable(() -> mwApi.getSubCategoryList(categoryName))
|
compositeDisposable.add(categoryClient.getParentCategoryList("Category:"+categoryName)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
.collect(ArrayList<String>::new, ArrayList::add)
|
||||||
.subscribe(this::handleSuccess, this::handleError));
|
.subscribe(this::handleSuccess, this::handleError));
|
||||||
}else {
|
} else {
|
||||||
compositeDisposable.add(Observable.fromCallable(() -> mwApi.getParentCategoryList(categoryName))
|
compositeDisposable.add(categoryClient.getSubCategoryList("Category:"+categoryName)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
.collect(ArrayList<String>::new, ArrayList::add)
|
||||||
.subscribe(this::handleSuccess, this::handleError));
|
.subscribe(this::handleSuccess, this::handleError));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,8 @@ public class ContributionsFragment
|
||||||
|
|
||||||
private ContributionsListFragment contributionsListFragment;
|
private ContributionsListFragment contributionsListFragment;
|
||||||
private MediaDetailPagerFragment mediaDetailPagerFragment;
|
private MediaDetailPagerFragment mediaDetailPagerFragment;
|
||||||
public static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
|
private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
|
||||||
public static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
|
static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
|
||||||
|
|
||||||
@BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView;
|
@BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView;
|
||||||
@BindView(R.id.campaigns_view) CampaignView campaignView;
|
@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
|
operations on first time fragment attached to an activity. Then they will be retained
|
||||||
until fragment life time ends.
|
until fragment life time ends.
|
||||||
*/
|
*/
|
||||||
if (((MainActivity)getActivity()).isAuthCookieAcquired && !isFragmentAttachedBefore) {
|
if (!isFragmentAttachedBefore) {
|
||||||
onAuthCookieAcquired(((MainActivity)getActivity()).uploadServiceIntent);
|
onAuthCookieAcquired(((MainActivity)getActivity()).uploadServiceIntent);
|
||||||
isFragmentAttachedBefore = true;
|
isFragmentAttachedBefore = true;
|
||||||
|
|
||||||
|
|
@ -268,7 +268,7 @@ public class ContributionsFragment
|
||||||
* Replace FrameLayout with ContributionsListFragment, user will see contributions list. Creates
|
* Replace FrameLayout with ContributionsListFragment, user will see contributions list. Creates
|
||||||
* new one if null.
|
* new one if null.
|
||||||
*/
|
*/
|
||||||
public void showContributionsListFragment() {
|
private void showContributionsListFragment() {
|
||||||
// show tabs on contribution list is visible
|
// show tabs on contribution list is visible
|
||||||
((MainActivity) getActivity()).showTabs();
|
((MainActivity) getActivity()).showTabs();
|
||||||
// show nearby card view on contributions list is visible
|
// 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.
|
* Replace FrameLayout with MediaDetailPagerFragment, user will see details of selected media.
|
||||||
* Creates new one if null.
|
* Creates new one if null.
|
||||||
*/
|
*/
|
||||||
public void showMediaDetailPagerFragment() {
|
private void showMediaDetailPagerFragment() {
|
||||||
// hide tabs on media detail view is visible
|
// hide tabs on media detail view is visible
|
||||||
((MainActivity)getActivity()).hideTabs();
|
((MainActivity)getActivity()).hideTabs();
|
||||||
// hide nearby card view on media detail is visible
|
// 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
|
* Called when onAuthCookieAcquired is called on authenticated parent activity
|
||||||
* @param uploadServiceIntent
|
* @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
|
// 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
|
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.
|
* mediaDetailPagerFragment, and preserve previous state in back stack.
|
||||||
* Called when user selects a contribution.
|
* Called when user selects a contribution.
|
||||||
*/
|
*/
|
||||||
public void showDetail(int i) {
|
private void showDetail(int i) {
|
||||||
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
|
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
|
||||||
mediaDetailPagerFragment = new MediaDetailPagerFragment();
|
mediaDetailPagerFragment = new MediaDetailPagerFragment();
|
||||||
showMediaDetailPagerFragment();
|
showMediaDetailPagerFragment();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
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.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
|
@ -13,21 +10,29 @@ import android.view.animation.AnimationUtils;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.recyclerview.widget.RecyclerView.LayoutManager;
|
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.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
|
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
import javax.inject.Inject;
|
import fr.free.nrw.commons.wikidata.WikidataClient;
|
||||||
import javax.inject.Named;
|
|
||||||
|
import static android.view.View.GONE;
|
||||||
|
import static android.view.View.VISIBLE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by root on 01.06.2018.
|
* Created by root on 01.06.2018.
|
||||||
|
|
@ -53,6 +58,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
@Inject @Named("default_preferences") JsonKvStore kvStore;
|
@Inject @Named("default_preferences") JsonKvStore kvStore;
|
||||||
@Inject ContributionController controller;
|
@Inject ContributionController controller;
|
||||||
|
@Inject
|
||||||
|
WikidataClient wikidataClient;
|
||||||
|
|
||||||
private Animation fab_close;
|
private Animation fab_close;
|
||||||
private Animation fab_open;
|
private Animation fab_open;
|
||||||
|
|
@ -163,6 +170,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible to set progress bar invisible and visible
|
* Responsible to set progress bar invisible and visible
|
||||||
|
*
|
||||||
* @param shouldShow True when contributions list should be hidden.
|
* @param shouldShow True when contributions list should be hidden.
|
||||||
*/
|
*/
|
||||||
public void showProgress(boolean shouldShow) {
|
public void showProgress(boolean shouldShow) {
|
||||||
|
|
@ -170,7 +178,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showNoContributionsUI(boolean shouldShow) {
|
public void showNoContributionsUI(boolean shouldShow) {
|
||||||
noContributionsYet.setVisibility(shouldShow?VISIBLE:GONE);
|
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onDataSetChanged() {
|
public void onDataSetChanged() {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
|
@ -12,23 +13,23 @@ import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
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.core.view.GravityCompat;
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentPagerAdapter;
|
import androidx.fragment.app.FragmentPagerAdapter;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
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.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.R;
|
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.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||||
import fr.free.nrw.commons.nearby.NearbyFragment;
|
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.NotificationActivity;
|
||||||
import fr.free.nrw.commons.notification.NotificationController;
|
import fr.free.nrw.commons.notification.NotificationController;
|
||||||
import fr.free.nrw.commons.quiz.QuizChecker;
|
import fr.free.nrw.commons.quiz.QuizChecker;
|
||||||
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
import fr.free.nrw.commons.upload.UploadService;
|
import fr.free.nrw.commons.upload.UploadService;
|
||||||
import io.reactivex.Observable;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.content.ContentResolver.requestSync;
|
import static android.content.ContentResolver.requestSync;
|
||||||
|
|
||||||
public class MainActivity extends AuthenticatedActivity implements FragmentManager.OnBackStackChangedListener {
|
public class MainActivity extends NavigationBaseActivity implements FragmentManager.OnBackStackChangedListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SessionManager sessionManager;
|
SessionManager sessionManager;
|
||||||
|
|
@ -63,7 +64,6 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
|
||||||
|
|
||||||
|
|
||||||
public Intent uploadServiceIntent;
|
public Intent uploadServiceIntent;
|
||||||
public boolean isAuthCookieAcquired = false;
|
|
||||||
|
|
||||||
public ContributionsActivityPagerAdapter contributionsActivityPagerAdapter;
|
public ContributionsActivityPagerAdapter contributionsActivityPagerAdapter;
|
||||||
public static final int CONTRIBUTIONS_TAB_POSITION = 0;
|
public static final int CONTRIBUTIONS_TAB_POSITION = 0;
|
||||||
|
|
@ -82,10 +82,10 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
|
||||||
setContentView(R.layout.activity_contributions);
|
setContentView(R.layout.activity_contributions);
|
||||||
ButterKnife.bind(this);
|
ButterKnife.bind(this);
|
||||||
|
|
||||||
requestAuthToken();
|
|
||||||
initDrawer();
|
initDrawer();
|
||||||
setTitle(getString(R.string.navigation_item_home)); // Should I create a new string variable with another name instead?
|
setTitle(getString(R.string.navigation_item_home)); // Should I create a new string variable with another name instead?
|
||||||
|
|
||||||
|
initMain();
|
||||||
|
|
||||||
if (savedInstanceState != null ) {
|
if (savedInstanceState != null ) {
|
||||||
onOrientationChanged = true; // Will be used in nearby fragment to determine significant update of map
|
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());
|
outState.putInt("viewPagerCurrentItem", viewPager.getCurrentItem());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void initMain() {
|
||||||
protected void onAuthCookieAcquired(String authCookie) {
|
//Do not remove this, this triggers the sync service
|
||||||
// Do a sync everytime we get here!
|
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(),BuildConfig.CONTRIBUTION_AUTHORITY,true);
|
||||||
requestSync(sessionManager.getCurrentAccount(), BuildConfig.CONTRIBUTION_AUTHORITY, new Bundle());
|
requestSync(sessionManager.getCurrentAccount(), BuildConfig.CONTRIBUTION_AUTHORITY, new Bundle());
|
||||||
uploadServiceIntent = new Intent(this, UploadService.class);
|
uploadServiceIntent = new Intent(this, UploadService.class);
|
||||||
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
||||||
startService(uploadServiceIntent);
|
startService(uploadServiceIntent);
|
||||||
|
|
||||||
addTabsAndFragments();
|
addTabsAndFragments();
|
||||||
isAuthCookieAcquired = true;
|
|
||||||
if (contributionsActivityPagerAdapter.getItem(0) != null) {
|
if (contributionsActivityPagerAdapter.getItem(0) != null) {
|
||||||
((ContributionsFragment)contributionsActivityPagerAdapter.getItem(0)).onAuthCookieAcquired(uploadServiceIntent);
|
((ContributionsFragment)contributionsActivityPagerAdapter.getItem(0)).onAuthCookieAcquired(uploadServiceIntent);
|
||||||
}
|
}
|
||||||
|
|
@ -232,14 +231,9 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onAuthFailure() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
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 contributionsFragmentTag = ((ContributionsActivityPagerAdapter) viewPager.getAdapter()).makeFragmentName(R.id.pager, 0);
|
||||||
String nearbyFragmentTag = ((ContributionsActivityPagerAdapter) viewPager.getAdapter()).makeFragmentName(R.id.pager, 1);
|
String nearbyFragmentTag = ((ContributionsActivityPagerAdapter) viewPager.getAdapter()).makeFragmentName(R.id.pager, 1);
|
||||||
if (drawer.isDrawerOpen(GravityCompat.START)) {
|
if (drawer.isDrawerOpen(GravityCompat.START)) {
|
||||||
|
|
@ -305,7 +299,7 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
@SuppressLint("CheckResult")
|
||||||
private void setNotificationCount() {
|
private void setNotificationCount() {
|
||||||
compositeDisposable.add(Observable.fromCallable(() -> notificationController.getNotifications(false))
|
compositeDisposable.add(notificationController.getNotifications(false)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(this::initNotificationViews,
|
.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.category.CategoryDao;
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
|
||||||
|
|
||||||
public class DBOpenHelper extends SQLiteOpenHelper {
|
public class DBOpenHelper extends SQLiteOpenHelper {
|
||||||
|
|
||||||
|
|
@ -27,7 +26,6 @@ public class DBOpenHelper extends SQLiteOpenHelper {
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(SQLiteDatabase sqLiteDatabase) {
|
public void onCreate(SQLiteDatabase sqLiteDatabase) {
|
||||||
ContributionDao.Table.onCreate(sqLiteDatabase);
|
ContributionDao.Table.onCreate(sqLiteDatabase);
|
||||||
ModifierSequenceDao.Table.onCreate(sqLiteDatabase);
|
|
||||||
CategoryDao.Table.onCreate(sqLiteDatabase);
|
CategoryDao.Table.onCreate(sqLiteDatabase);
|
||||||
BookmarkPicturesDao.Table.onCreate(sqLiteDatabase);
|
BookmarkPicturesDao.Table.onCreate(sqLiteDatabase);
|
||||||
BookmarkLocationsDao.Table.onCreate(sqLiteDatabase);
|
BookmarkLocationsDao.Table.onCreate(sqLiteDatabase);
|
||||||
|
|
@ -37,7 +35,6 @@ public class DBOpenHelper extends SQLiteOpenHelper {
|
||||||
@Override
|
@Override
|
||||||
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
|
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
|
||||||
ContributionDao.Table.onUpdate(sqLiteDatabase, from, to);
|
ContributionDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||||
ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to);
|
|
||||||
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
|
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||||
BookmarkPicturesDao.Table.onUpdate(sqLiteDatabase, from, to);
|
BookmarkPicturesDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||||
BookmarkLocationsDao.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.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
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.mwapi.MediaWikiApi;
|
|
||||||
import fr.free.nrw.commons.notification.NotificationHelper;
|
import fr.free.nrw.commons.notification.NotificationHelper;
|
||||||
import fr.free.nrw.commons.review.ReviewController;
|
import fr.free.nrw.commons.review.ReviewController;
|
||||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||||
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.SingleSource;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
@ -35,20 +38,20 @@ import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_D
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
public class DeleteHelper {
|
public class DeleteHelper {
|
||||||
private final MediaWikiApi mwApi;
|
|
||||||
private final SessionManager sessionManager;
|
|
||||||
private final NotificationHelper notificationHelper;
|
private final NotificationHelper notificationHelper;
|
||||||
|
private final PageEditClient pageEditClient;
|
||||||
private final ViewUtilWrapper viewUtil;
|
private final ViewUtilWrapper viewUtil;
|
||||||
|
private final String username;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public DeleteHelper(MediaWikiApi mwApi,
|
public DeleteHelper(NotificationHelper notificationHelper,
|
||||||
SessionManager sessionManager,
|
@Named("commons-page-edit") PageEditClient pageEditClient,
|
||||||
NotificationHelper notificationHelper,
|
ViewUtilWrapper viewUtil,
|
||||||
ViewUtilWrapper viewUtil) {
|
@Named("username") String username) {
|
||||||
this.mwApi = mwApi;
|
|
||||||
this.sessionManager = sessionManager;
|
|
||||||
this.notificationHelper = notificationHelper;
|
this.notificationHelper = notificationHelper;
|
||||||
|
this.pageEditClient = pageEditClient;
|
||||||
this.viewUtil = viewUtil;
|
this.viewUtil = viewUtil;
|
||||||
|
this.username = username;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -59,10 +62,11 @@ public class DeleteHelper {
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public Single<Boolean> makeDeletion(Context context, Media media, String reason) {
|
public Single<Boolean> makeDeletion(Context context, Media media, String reason) {
|
||||||
viewUtil.showShortToast(context, context.getString((R.string.delete_helper_make_deletion_toast), media.getDisplayTitle()));
|
viewUtil.showShortToast(context, "Trying to nominate " + media.getDisplayTitle() + " for deletion");
|
||||||
return Single.fromCallable(() -> delete(media, reason))
|
|
||||||
.flatMap(result -> Single.fromCallable(() ->
|
return delete(media, reason)
|
||||||
showDeletionNotification(context, media, result)));
|
.flatMapSingle(result -> Single.just(showDeletionNotification(context, media, result)))
|
||||||
|
.firstOrError();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -71,14 +75,9 @@ public class DeleteHelper {
|
||||||
* @param reason
|
* @param reason
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private boolean delete(Media media, String reason) {
|
private Observable<Boolean> delete(Media media, String reason) {
|
||||||
String editToken;
|
Timber.d("thread is delete %s", Thread.currentThread().getName());
|
||||||
String authCookie;
|
|
||||||
String summary = "Nominating " + media.getFilename() + " for deletion.";
|
String summary = "Nominating " + media.getFilename() + " for deletion.";
|
||||||
|
|
||||||
authCookie = sessionManager.getAuthCookie();
|
|
||||||
mwApi.setAuthCookie(authCookie);
|
|
||||||
|
|
||||||
Calendar calendar = Calendar.getInstance();
|
Calendar calendar = Calendar.getInstance();
|
||||||
String fileDeleteString = "{{delete|reason=" + reason +
|
String fileDeleteString = "{{delete|reason=" + reason +
|
||||||
"|subpage=" + media.getFilename() +
|
"|subpage=" + media.getFilename() +
|
||||||
|
|
@ -99,26 +98,23 @@ public class DeleteHelper {
|
||||||
String userPageString = "\n{{subst:idw|" + media.getFilename() +
|
String userPageString = "\n{{subst:idw|" + media.getFilename() +
|
||||||
"}} ~~~~";
|
"}} ~~~~";
|
||||||
|
|
||||||
try {
|
return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary)
|
||||||
editToken = mwApi.getEditToken();
|
.flatMap(result -> {
|
||||||
|
if (result) {
|
||||||
if(editToken == null) {
|
return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
throw new RuntimeException("Failed to nominate for deletion");
|
||||||
mwApi.prependEdit(editToken, fileDeleteString + "\n",
|
}).flatMap(result -> {
|
||||||
media.getFilename(), summary);
|
if (result) {
|
||||||
mwApi.edit(editToken, subpageString + "\n",
|
return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary);
|
||||||
"Commons:Deletion_requests/" + media.getFilename(), summary);
|
|
||||||
mwApi.appendEdit(editToken, logPageString + "\n",
|
|
||||||
"Commons:Deletion_requests/" + date, summary);
|
|
||||||
mwApi.appendEdit(editToken, userPageString + "\n",
|
|
||||||
"User_Talk:" + media.getCreator(), summary);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.e(e);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
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) {
|
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())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(aBoolean -> {
|
.subscribe(aBoolean -> {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package fr.free.nrw.commons.di;
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionsModule;
|
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import dagger.Component;
|
import dagger.Component;
|
||||||
|
|
@ -10,8 +9,8 @@ import dagger.android.support.AndroidSupportInjectionModule;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.auth.LoginActivity;
|
import fr.free.nrw.commons.auth.LoginActivity;
|
||||||
import fr.free.nrw.commons.contributions.ContributionViewHolder;
|
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.contributions.ContributionsSyncAdapter;
|
||||||
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
|
|
||||||
import fr.free.nrw.commons.nearby.PlaceRenderer;
|
import fr.free.nrw.commons.nearby.PlaceRenderer;
|
||||||
import fr.free.nrw.commons.review.ReviewController;
|
import fr.free.nrw.commons.review.ReviewController;
|
||||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||||
|
|
@ -36,8 +35,6 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
|
||||||
|
|
||||||
void inject(ContributionsSyncAdapter syncAdapter);
|
void inject(ContributionsSyncAdapter syncAdapter);
|
||||||
|
|
||||||
void inject(ModificationsSyncAdapter syncAdapter);
|
|
||||||
|
|
||||||
void inject(LoginActivity activity);
|
void inject(LoginActivity activity);
|
||||||
|
|
||||||
void inject(SettingsFragment fragment);
|
void inject(SettingsFragment fragment);
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,11 @@ import android.content.ContentProviderClient;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
|
||||||
|
import androidx.collection.LruCache;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
import org.wikipedia.AppAdapter;
|
||||||
|
|
||||||
import io.reactivex.Scheduler;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
@ -22,7 +19,6 @@ import java.util.Map;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import androidx.collection.LruCache;
|
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
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.utils.ConfigUtils;
|
||||||
import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
||||||
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
|
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
|
||||||
|
import io.reactivex.Scheduler;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
|
|
@ -85,7 +84,7 @@ public class CommonsApplicationModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
public AccountUtil providesAccountUtil(Context context) {
|
public AccountUtil providesAccountUtil(Context context) {
|
||||||
return new AccountUtil(context);
|
return new AccountUtil();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
@ -188,7 +187,13 @@ public class CommonsApplicationModule {
|
||||||
|
|
||||||
@Named(MAIN_THREAD)
|
@Named(MAIN_THREAD)
|
||||||
@Provides
|
@Provides
|
||||||
public Scheduler providesMainThread(){
|
public Scheduler providesMainThread() {
|
||||||
return AndroidSchedulers.mainThread();
|
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.category.CategoryContentProvider;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
|
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
|
||||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
|
|
@ -16,9 +15,6 @@ public abstract class ContentProviderBuilderModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract ContributionsContentProvider bindContributionsContentProvider();
|
abstract ContributionsContentProvider bindContributionsContentProvider();
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
|
||||||
abstract ModificationsContentProvider bindModificationsContentProvider();
|
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract CategoryContentProvider bindCategoryContentProvider();
|
abstract CategoryContentProvider bindCategoryContentProvider();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,16 @@ package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
|
import org.wikipedia.csrf.CsrfTokenClient;
|
||||||
|
import org.wikipedia.dataclient.Service;
|
||||||
import org.wikipedia.dataclient.ServiceFactory;
|
import org.wikipedia.dataclient.ServiceFactory;
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
import org.wikipedia.dataclient.WikiSite;
|
||||||
import org.wikipedia.json.GsonUtil;
|
import org.wikipedia.json.GsonUtil;
|
||||||
|
import org.wikipedia.login.LoginClient;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
@ -14,15 +19,19 @@ import java.util.concurrent.TimeUnit;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
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.kvstore.JsonKvStore;
|
||||||
|
import fr.free.nrw.commons.media.MediaInterface;
|
||||||
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
|
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||||
import fr.free.nrw.commons.review.ReviewInterface;
|
import fr.free.nrw.commons.review.ReviewInterface;
|
||||||
|
import fr.free.nrw.commons.upload.UploadInterface;
|
||||||
import okhttp3.Cache;
|
import okhttp3.Cache;
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
|
|
@ -39,6 +48,12 @@ public class NetworkingModule {
|
||||||
|
|
||||||
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
|
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
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public OkHttpClient provideOkHttpClient(Context context,
|
public OkHttpClient provideOkHttpClient(Context context,
|
||||||
|
|
@ -67,7 +82,7 @@ public class NetworkingModule {
|
||||||
public MediaWikiApi provideMediaWikiApi(Context context,
|
public MediaWikiApi provideMediaWikiApi(Context context,
|
||||||
@Named("default_preferences") JsonKvStore defaultKvStore,
|
@Named("default_preferences") JsonKvStore defaultKvStore,
|
||||||
Gson gson) {
|
Gson gson) {
|
||||||
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultKvStore, gson);
|
return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
@ -81,10 +96,30 @@ public class NetworkingModule {
|
||||||
WIKIDATA_SPARQL_QUERY_URL,
|
WIKIDATA_SPARQL_QUERY_URL,
|
||||||
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,
|
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,
|
||||||
BuildConfig.WIKIMEDIA_API_HOST,
|
BuildConfig.WIKIMEDIA_API_HOST,
|
||||||
defaultKvStore,
|
|
||||||
gson);
|
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
|
@Provides
|
||||||
@Named("wikimedia_api_host")
|
@Named("wikimedia_api_host")
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|
@ -101,6 +136,20 @@ public class NetworkingModule {
|
||||||
return HttpUrl.parse(TOOLS_FORGE_URL);
|
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.
|
* 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
|
* @return returns a singleton Gson instance
|
||||||
|
|
@ -113,14 +162,71 @@ public class NetworkingModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@Named("commons-wikisite")
|
@Named("commons-service")
|
||||||
public WikiSite provideCommonsWikiSite() {
|
public Service provideCommonsService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||||
return new WikiSite(BuildConfig.COMMONS_URL);
|
return ServiceFactory.get(commonsWikiSite);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@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);
|
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.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.R;
|
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.category.CategoryDetailsActivity;
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
|
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
|
||||||
|
|
@ -58,9 +59,12 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
|
||||||
String query;
|
String query;
|
||||||
@BindView(R.id.bottomProgressBar)
|
@BindView(R.id.bottomProgressBar)
|
||||||
ProgressBar bottomProgressBar;
|
ProgressBar bottomProgressBar;
|
||||||
|
boolean isLoadingCategories;
|
||||||
|
|
||||||
@Inject RecentSearchesDao recentSearchesDao;
|
@Inject RecentSearchesDao recentSearchesDao;
|
||||||
@Inject MediaWikiApi mwApi;
|
@Inject MediaWikiApi mwApi;
|
||||||
|
@Inject CategoryClient categoryClient;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@Named("default_preferences")
|
@Named("default_preferences")
|
||||||
JsonKvStore basicKvStore;
|
JsonKvStore basicKvStore;
|
||||||
|
|
@ -135,33 +139,36 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
|
||||||
progressBar.setVisibility(GONE);
|
progressBar.setVisibility(GONE);
|
||||||
queryList.clear();
|
queryList.clear();
|
||||||
categoriesAdapter.clear();
|
categoriesAdapter.clear();
|
||||||
compositeDisposable.add(Observable.fromCallable(() -> mwApi.searchCategory(query,queryList.size()))
|
compositeDisposable.add(categoryClient.searchCategories(query,25)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
.doOnSubscribe(disposable -> saveQuery(query))
|
.doOnSubscribe(disposable -> saveQuery(query))
|
||||||
|
.collect(ArrayList<String>::new, ArrayList::add)
|
||||||
.subscribe(this::handleSuccess, this::handleError));
|
.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) {
|
public void addCategoriesToList(String query) {
|
||||||
|
if(isLoadingCategories) return;
|
||||||
|
isLoadingCategories=true;
|
||||||
this.query = query;
|
this.query = query;
|
||||||
bottomProgressBar.setVisibility(View.VISIBLE);
|
bottomProgressBar.setVisibility(View.VISIBLE);
|
||||||
progressBar.setVisibility(GONE);
|
progressBar.setVisibility(GONE);
|
||||||
compositeDisposable.add(Observable.fromCallable(() -> mwApi.searchCategory(query,queryList.size()))
|
compositeDisposable.add(categoryClient.searchCategories(query,25, queryList.size())
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
.collect(ArrayList<String>::new, ArrayList::add)
|
||||||
.subscribe(this::handlePaginationSuccess, this::handleError));
|
.subscribe(this::handlePaginationSuccess, this::handleError));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the success scenario
|
* Handles the success scenario
|
||||||
* it initializes the recycler view by adding items to the adapter
|
* it initializes the recycler view by adding items to the adapter
|
||||||
* @param mediaList
|
|
||||||
*/
|
*/
|
||||||
private void handlePaginationSuccess(List<String> mediaList) {
|
private void handlePaginationSuccess(List<String> mediaList) {
|
||||||
queryList.addAll(mediaList);
|
queryList.addAll(mediaList);
|
||||||
|
|
@ -169,6 +176,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
|
||||||
bottomProgressBar.setVisibility(GONE);
|
bottomProgressBar.setVisibility(GONE);
|
||||||
categoriesAdapter.addAll(mediaList);
|
categoriesAdapter.addAll(mediaList);
|
||||||
categoriesAdapter.notifyDataSetChanged();
|
categoriesAdapter.notifyDataSetChanged();
|
||||||
|
isLoadingCategories=false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -176,7 +184,6 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
|
||||||
/**
|
/**
|
||||||
* Handles the success scenario
|
* Handles the success scenario
|
||||||
* it initializes the recycler view by adding items to the adapter
|
* it initializes the recycler view by adding items to the adapter
|
||||||
* @param mediaList
|
|
||||||
*/
|
*/
|
||||||
private void handleSuccess(List<String> mediaList) {
|
private void handleSuccess(List<String> mediaList) {
|
||||||
queryList = mediaList;
|
queryList = mediaList;
|
||||||
|
|
@ -194,7 +201,6 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs and handles API error scenario
|
* Logs and handles API error scenario
|
||||||
* @param throwable
|
|
||||||
*/
|
*/
|
||||||
private void handleError(Throwable throwable) {
|
private void handleError(Throwable throwable) {
|
||||||
Timber.e(throwable, "Error occurred while loading queried categories");
|
Timber.e(throwable, "Error occurred while loading queried categories");
|
||||||
|
|
@ -213,7 +219,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
|
||||||
private void initErrorView() {
|
private void initErrorView() {
|
||||||
progressBar.setVisibility(GONE);
|
progressBar.setVisibility(GONE);
|
||||||
categoriesNotFoundView.setVisibility(VISIBLE);
|
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 androidx.recyclerview.widget.RecyclerView;
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
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.RecentSearch;
|
||||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
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.NetworkUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.Disposable;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
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 timber.log.Timber;
|
||||||
|
|
||||||
import static android.view.View.GONE;
|
import static android.view.View.GONE;
|
||||||
|
|
@ -69,7 +61,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
@Inject RecentSearchesDao recentSearchesDao;
|
@Inject RecentSearchesDao recentSearchesDao;
|
||||||
@Inject
|
@Inject
|
||||||
OkHttpJsonApiClient okHttpJsonApiClient;
|
MediaClient mediaClient;
|
||||||
@Inject
|
@Inject
|
||||||
@Named("default_preferences")
|
@Named("default_preferences")
|
||||||
JsonKvStore defaultKvStore;
|
JsonKvStore defaultKvStore;
|
||||||
|
|
@ -148,7 +140,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
|
||||||
bottomProgressBar.setVisibility(GONE);
|
bottomProgressBar.setVisibility(GONE);
|
||||||
queryList.clear();
|
queryList.clear();
|
||||||
imagesAdapter.clear();
|
imagesAdapter.clear();
|
||||||
compositeDisposable.add(okHttpJsonApiClient.getMediaList("search", query)
|
compositeDisposable.add(mediaClient.getMediaListFromSearch(query)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
|
@ -165,7 +157,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
|
||||||
this.query = query;
|
this.query = query;
|
||||||
bottomProgressBar.setVisibility(View.VISIBLE);
|
bottomProgressBar.setVisibility(View.VISIBLE);
|
||||||
progressBar.setVisibility(GONE);
|
progressBar.setVisibility(GONE);
|
||||||
compositeDisposable.add(okHttpJsonApiClient.getMediaList("search", query)
|
compositeDisposable.add(mediaClient.getMediaListFromSearch(query)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
|
@ -228,7 +220,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
|
||||||
private void initErrorView() {
|
private void initErrorView() {
|
||||||
progressBar.setVisibility(GONE);
|
progressBar.setVisibility(GONE);
|
||||||
imagesNotFoundView.setVisibility(VISIBLE);
|
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;
|
package fr.free.nrw.commons.mwapi;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.apache.http.conn.ClientConnectionManager;
|
import org.apache.http.conn.ClientConnectionManager;
|
||||||
import org.apache.http.conn.scheme.PlainSocketFactory;
|
import org.apache.http.conn.scheme.PlainSocketFactory;
|
||||||
import org.apache.http.conn.scheme.Scheme;
|
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.impl.conn.tsccm.ThreadSafeClientConnManager;
|
||||||
import org.apache.http.params.BasicHttpParams;
|
import org.apache.http.params.BasicHttpParams;
|
||||||
import org.apache.http.params.CoreProtocolPNames;
|
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 org.wikipedia.util.DateUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Date;
|
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.BuildConfig;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.auth.AccountUtil;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.category.CategoryImageUtils;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
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 io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
@ -54,19 +38,8 @@ import timber.log.Timber;
|
||||||
public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
private AbstractHttpClient httpClient;
|
private AbstractHttpClient httpClient;
|
||||||
private CustomMwApi api;
|
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(String apiURL) {
|
||||||
|
|
||||||
public ApacheHttpClientMediaWikiApi(Context context,
|
|
||||||
String apiURL,
|
|
||||||
String wikidatApiURL,
|
|
||||||
JsonKvStore defaultKvStore,
|
|
||||||
Gson gson) {
|
|
||||||
this.context = context;
|
|
||||||
BasicHttpParams params = new BasicHttpParams();
|
BasicHttpParams params = new BasicHttpParams();
|
||||||
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
||||||
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
|
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
|
||||||
|
|
@ -79,217 +52,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
httpClient.addRequestInterceptor(NetworkInterceptors.getHttpRequestInterceptor());
|
httpClient.addRequestInterceptor(NetworkInterceptors.getHttpRequestInterceptor());
|
||||||
}
|
}
|
||||||
api = new CustomMwApi(apiURL, httpClient);
|
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
|
@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
|
@Override
|
||||||
@NonNull
|
@NonNull
|
||||||
public LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException {
|
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");
|
.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
|
* Checks to see if a user is currently blocked from Commons
|
||||||
|
*
|
||||||
* @return whether or not the user is blocked from Commons
|
* @return whether or not the user is blocked from Commons
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -1,94 +1,22 @@
|
||||||
package fr.free.nrw.commons.mwapi;
|
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.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import fr.free.nrw.commons.notification.Notification;
|
|
||||||
import io.reactivex.Observable;
|
import java.io.IOException;
|
||||||
|
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
|
|
||||||
public interface MediaWikiApi {
|
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);
|
Single<String> parseWikicode(String source);
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
Single<MediaResult> fetchMediaByFilename(String filename);
|
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
|
@Nullable
|
||||||
String revisionsByFilename(String filename) throws IOException;
|
String revisionsByFilename(String filename) throws IOException;
|
||||||
|
|
||||||
boolean existingFile(String fileSha1) throws IOException;
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;
|
LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;
|
||||||
|
|
||||||
|
|
@ -98,8 +26,6 @@ public interface MediaWikiApi {
|
||||||
|
|
||||||
// Single<CampaignResponseDTO> getCampaigns();
|
// Single<CampaignResponseDTO> getCampaigns();
|
||||||
|
|
||||||
boolean thank(String editToken, long revision) throws IOException;
|
|
||||||
|
|
||||||
interface ProgressListener {
|
interface ProgressListener {
|
||||||
void onProgress(long transferred, long total);
|
void onProgress(long transferred, long total);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,15 +46,11 @@ import timber.log.Timber;
|
||||||
public class OkHttpJsonApiClient {
|
public class OkHttpJsonApiClient {
|
||||||
private static final String THUMB_SIZE = "640";
|
private static final String THUMB_SIZE = "640";
|
||||||
|
|
||||||
public static final Type mapType = new TypeToken<Map<String, String>>() {
|
|
||||||
}.getType();
|
|
||||||
|
|
||||||
private final OkHttpClient okHttpClient;
|
private final OkHttpClient okHttpClient;
|
||||||
private final HttpUrl wikiMediaToolforgeUrl;
|
private final HttpUrl wikiMediaToolforgeUrl;
|
||||||
private final String sparqlQueryUrl;
|
private final String sparqlQueryUrl;
|
||||||
private final String campaignsUrl;
|
private final String campaignsUrl;
|
||||||
private final String commonsBaseUrl;
|
private final String commonsBaseUrl;
|
||||||
private final JsonKvStore defaultKvStore;
|
|
||||||
private Gson gson;
|
private Gson gson;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -64,14 +60,12 @@ public class OkHttpJsonApiClient {
|
||||||
String sparqlQueryUrl,
|
String sparqlQueryUrl,
|
||||||
String campaignsUrl,
|
String campaignsUrl,
|
||||||
String commonsBaseUrl,
|
String commonsBaseUrl,
|
||||||
JsonKvStore defaultKvStore,
|
|
||||||
Gson gson) {
|
Gson gson) {
|
||||||
this.okHttpClient = okHttpClient;
|
this.okHttpClient = okHttpClient;
|
||||||
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
|
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
|
||||||
this.sparqlQueryUrl = sparqlQueryUrl;
|
this.sparqlQueryUrl = sparqlQueryUrl;
|
||||||
this.campaignsUrl = campaignsUrl;
|
this.campaignsUrl = campaignsUrl;
|
||||||
this.commonsBaseUrl = commonsBaseUrl;
|
this.commonsBaseUrl = commonsBaseUrl;
|
||||||
this.defaultKvStore = defaultKvStore;
|
|
||||||
this.gson = gson;
|
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
|
* Whenever imageInfo is fetched, these common properties can be specified for the API call
|
||||||
* https://www.mediawiki.org/wiki/API:Imageinfo
|
* https://www.mediawiki.org/wiki/API:Imageinfo
|
||||||
|
|
@ -304,124 +248,4 @@ public class OkHttpJsonApiClient {
|
||||||
|
|
||||||
return builder;
|
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;
|
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.
|
* Created by root on 18.12.2017.
|
||||||
*/
|
*/
|
||||||
|
|
@ -8,33 +12,44 @@ public class Notification {
|
||||||
public NotificationType notificationType;
|
public NotificationType notificationType;
|
||||||
public String notificationText;
|
public String notificationText;
|
||||||
public String date;
|
public String date;
|
||||||
public String description;
|
|
||||||
public String link;
|
public String link;
|
||||||
public String iconUrl;
|
public String iconUrl;
|
||||||
public String dateWithYear;
|
|
||||||
public String notificationId;
|
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.notificationType = notificationType;
|
||||||
this.notificationText = notificationText;
|
this.notificationText = notificationText;
|
||||||
this.date = date;
|
this.date = date;
|
||||||
this.description = description;
|
|
||||||
this.link = link;
|
this.link = link;
|
||||||
this.iconUrl = iconUrl;
|
this.iconUrl = iconUrl;
|
||||||
this.dateWithYear = dateWithYear;
|
|
||||||
this.notificationId=notificationId;
|
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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Notification" +
|
return "Notification" +
|
||||||
"notificationType='" + notificationType + '\'' +
|
"notificationType='" + notificationType + '\'' +
|
||||||
", notificationText='" + notificationText + '\'' +
|
", notificationText='" + notificationText + '\'' +
|
||||||
", date='" + date + '\'' +
|
", date='" + date + '\'' +
|
||||||
", description='" + description + '\'' +
|
|
||||||
", link='" + link + '\'' +
|
", link='" + link + '\'' +
|
||||||
", iconUrl='" + iconUrl + '\'' +
|
", iconUrl='" + iconUrl + '\'' +
|
||||||
", dateWithYear=" + dateWithYear +
|
|
||||||
", notificationId='" + notificationId + '\'' +
|
", notificationId='" + notificationId + '\'' +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,6 @@ import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
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.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
@ -19,10 +14,17 @@ import android.widget.RelativeLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
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 com.pedrogomez.renderers.RVRendererAdapter;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
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.NetworkUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.ObservableSource;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
@ -78,25 +82,25 @@ public class NotificationActivity extends NavigationBaseActivity {
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
@SuppressLint("CheckResult")
|
||||||
public void removeNotification(Notification notification) {
|
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())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(result -> {
|
.subscribe(result -> {
|
||||||
if (result){
|
if (result) {
|
||||||
notificationList.remove(notification);
|
notificationList.remove(notification);
|
||||||
setAdapter(notificationList);
|
setAdapter(notificationList);
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
Snackbar snackbar = Snackbar
|
Snackbar snackbar = Snackbar
|
||||||
.make(relativeLayout,"Notification marked as read", Snackbar.LENGTH_LONG);
|
.make(relativeLayout, "Notification marked as read", Snackbar.LENGTH_LONG);
|
||||||
|
|
||||||
snackbar.show();
|
snackbar.show();
|
||||||
if (notificationList.size()==0){
|
if (notificationList.size() == 0) {
|
||||||
setEmptyView();
|
setEmptyView();
|
||||||
relativeLayout.setVisibility(View.GONE);
|
relativeLayout.setVisibility(View.GONE);
|
||||||
no_notification.setVisibility(View.VISIBLE);
|
no_notification.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
setAdapter(notificationList);
|
setAdapter(notificationList);
|
||||||
Toast.makeText(NotificationActivity.this, "There was some error!", Toast.LENGTH_SHORT).show();
|
Toast.makeText(NotificationActivity.this, "There was some error!", Toast.LENGTH_SHORT).show();
|
||||||
|
|
@ -107,7 +111,8 @@ public class NotificationActivity extends NavigationBaseActivity {
|
||||||
throwable.printStackTrace();
|
throwable.printStackTrace();
|
||||||
ViewUtil.showShortSnackbar(relativeLayout, R.string.error_notifications);
|
ViewUtil.showShortSnackbar(relativeLayout, R.string.error_notifications);
|
||||||
progressBar.setVisibility(View.GONE);
|
progressBar.setVisibility(View.GONE);
|
||||||
}));
|
});
|
||||||
|
compositeDisposable.add(disposable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -140,11 +145,8 @@ public class NotificationActivity extends NavigationBaseActivity {
|
||||||
private void addNotifications(boolean archived) {
|
private void addNotifications(boolean archived) {
|
||||||
Timber.d("Add notifications");
|
Timber.d("Add notifications");
|
||||||
if (mNotificationWorkerFragment == null) {
|
if (mNotificationWorkerFragment == null) {
|
||||||
compositeDisposable.add(Observable.fromCallable(() -> {
|
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
return controller.getNotifications(archived);
|
compositeDisposable.add(controller.getNotifications(archived)
|
||||||
|
|
||||||
})
|
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(notificationList -> {
|
.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;
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import io.reactivex.Observable;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import io.reactivex.Single;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by root on 19.12.2017.
|
* Created by root on 19.12.2017.
|
||||||
|
|
@ -16,27 +14,19 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
@Singleton
|
@Singleton
|
||||||
public class NotificationController {
|
public class NotificationController {
|
||||||
|
|
||||||
private MediaWikiApi mediaWikiApi;
|
private NotificationClient notificationClient;
|
||||||
private SessionManager sessionManager;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public NotificationController(MediaWikiApi mediaWikiApi, SessionManager sessionManager) {
|
public NotificationController(NotificationClient notificationClient) {
|
||||||
this.mediaWikiApi = mediaWikiApi;
|
this.notificationClient = notificationClient;
|
||||||
this.sessionManager = sessionManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Notification> getNotifications(boolean archived) throws IOException {
|
public Single<List<Notification>> getNotifications(boolean archived) {
|
||||||
if (mediaWikiApi.validateLogin()) {
|
return notificationClient.getNotifications(archived);
|
||||||
return mediaWikiApi.getNotifications(archived);
|
|
||||||
} else {
|
|
||||||
Boolean authTokenValidated = sessionManager.revalidateAuthToken();
|
|
||||||
if (authTokenValidated != null && authTokenValidated) {
|
|
||||||
return mediaWikiApi.getNotifications(archived);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return new ArrayList<>();
|
Observable<Boolean> markAsRead(Notification notification) {
|
||||||
}
|
return notificationClient.markNotificationAsRead(notification.notificationId);
|
||||||
public boolean markAsRead(Notification notification) throws IOException{
|
|
||||||
return mediaWikiApi.markNotificationAsRead(notification);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
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.delete.DeleteHelper;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
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.DialogUtil;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
public class ReviewActivity extends AuthenticatedActivity {
|
public class ReviewActivity extends NavigationBaseActivity {
|
||||||
|
|
||||||
@BindView(R.id.pager_indicator_review)
|
@BindView(R.id.pager_indicator_review)
|
||||||
public CirclePageIndicator pagerIndicator;
|
public CirclePageIndicator pagerIndicator;
|
||||||
|
|
@ -94,15 +94,6 @@ public class ReviewActivity extends AuthenticatedActivity {
|
||||||
return media;
|
return media;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onAuthCookieAcquired(String authCookie) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onAuthFailure() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.view.Gravity;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
@ -13,17 +11,23 @@ import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryPage;
|
import org.wikipedia.dataclient.mwapi.MwQueryPage;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
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.delete.DeleteHelper;
|
||||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
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.Observable;
|
||||||
|
import io.reactivex.ObservableSource;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
@ -32,13 +36,15 @@ import timber.log.Timber;
|
||||||
public class ReviewController {
|
public class ReviewController {
|
||||||
private static final int NOTIFICATION_SEND_THANK = 0x102;
|
private static final int NOTIFICATION_SEND_THANK = 0x102;
|
||||||
private static final int NOTIFICATION_CHECK_CATEGORY = 0x101;
|
private static final int NOTIFICATION_CHECK_CATEGORY = 0x101;
|
||||||
|
protected static ArrayList<String> categories;
|
||||||
|
@Inject
|
||||||
|
ThanksClient thanksClient;
|
||||||
private final DeleteHelper deleteHelper;
|
private final DeleteHelper deleteHelper;
|
||||||
@Nullable
|
@Nullable
|
||||||
MwQueryPage.Revision firstRevision; // TODO: maybe we can expand this class to include fileName
|
MwQueryPage.Revision firstRevision; // TODO: maybe we can expand this class to include fileName
|
||||||
@Inject
|
@Inject
|
||||||
MediaWikiApi mwApi;
|
@Named("commons-page-edit")
|
||||||
@Inject
|
PageEditClient pageEditClient;
|
||||||
SessionManager sessionManager;
|
|
||||||
private NotificationManager notificationManager;
|
private NotificationManager notificationManager;
|
||||||
private NotificationCompat.Builder notificationBuilder;
|
private NotificationCompat.Builder notificationBuilder;
|
||||||
private Media media;
|
private Media media;
|
||||||
|
|
@ -89,40 +95,16 @@ public class ReviewController {
|
||||||
.getCommonsApplicationComponent()
|
.getCommonsApplicationComponent()
|
||||||
.inject(this);
|
.inject(this);
|
||||||
|
|
||||||
Toast toast = new Toast(context);
|
ViewUtil.showShortToast(context, context.getString(R.string.check_category_toast, media.getDisplayTitle()));
|
||||||
toast.setGravity(Gravity.CENTER, 0, 0);
|
|
||||||
toast = Toast.makeText(context, context.getString(R.string.check_category_toast, media.getDisplayTitle()), Toast.LENGTH_SHORT);
|
|
||||||
toast.show();
|
|
||||||
|
|
||||||
Observable.fromCallable(() -> {
|
|
||||||
publishProgress(context, 0);
|
publishProgress(context, 0);
|
||||||
|
|
||||||
String editToken;
|
|
||||||
String authCookie;
|
|
||||||
String summary = context.getString(R.string.check_category_edit_summary);
|
String summary = context.getString(R.string.check_category_edit_summary);
|
||||||
|
Observable.defer((Callable<ObservableSource<Boolean>>) () ->
|
||||||
authCookie = sessionManager.getAuthCookie();
|
pageEditClient.appendEdit(media.getFilename(), "\n{{subst:chc}}\n", summary))
|
||||||
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;
|
|
||||||
})
|
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe((result) -> {
|
.subscribe((result) -> {
|
||||||
|
publishProgress(context, 2);
|
||||||
String message;
|
String message;
|
||||||
String title;
|
String title;
|
||||||
|
|
||||||
|
|
@ -136,15 +118,7 @@ public class ReviewController {
|
||||||
reviewCallback.onFailure();
|
reviewCallback.onFailure();
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL)
|
showNotification(title, message);
|
||||||
.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());
|
|
||||||
|
|
||||||
}, Timber::e);
|
}, Timber::e);
|
||||||
}
|
}
|
||||||
|
|
@ -172,39 +146,20 @@ public class ReviewController {
|
||||||
.getInstance(context)
|
.getInstance(context)
|
||||||
.getCommonsApplicationComponent()
|
.getCommonsApplicationComponent()
|
||||||
.inject(this);
|
.inject(this);
|
||||||
Toast toast = new Toast(context);
|
ViewUtil.showShortToast(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle()));
|
||||||
toast.setGravity(Gravity.CENTER, 0, 0);
|
|
||||||
toast = Toast.makeText(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle()), Toast.LENGTH_SHORT);
|
|
||||||
toast.show();
|
|
||||||
|
|
||||||
Observable.fromCallable(() -> {
|
|
||||||
publishProgress(context, 0);
|
publishProgress(context, 0);
|
||||||
|
if (firstRevision == null) {
|
||||||
String editToken;
|
return;
|
||||||
String authCookie;
|
|
||||||
authCookie = sessionManager.getAuthCookie();
|
|
||||||
mwApi.setAuthCookie(authCookie);
|
|
||||||
|
|
||||||
try {
|
|
||||||
editToken = mwApi.getEditToken();
|
|
||||||
if (editToken == null) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
publishProgress(context, 1);
|
|
||||||
assert firstRevision != null;
|
Observable.defer((Callable<ObservableSource<Boolean>>) () -> thanksClient.thank(firstRevision.getRevisionId()))
|
||||||
mwApi.thank(editToken, firstRevision.getRevisionId());
|
|
||||||
publishProgress(context, 2);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.d(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe((result) -> {
|
.subscribe((result) -> {
|
||||||
String message = "";
|
publishProgress(context, 2);
|
||||||
String title = "";
|
String message;
|
||||||
|
String title;
|
||||||
if (result) {
|
if (result) {
|
||||||
title = context.getString(R.string.send_thank_success_title);
|
title = context.getString(R.string.send_thank_success_title);
|
||||||
message = context.getString(R.string.send_thank_success_message, media.getDisplayTitle());
|
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());
|
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)
|
notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.setStyle(new NotificationCompat.BigTextStyle()
|
.setStyle(new NotificationCompat.BigTextStyle()
|
||||||
|
|
@ -222,8 +183,6 @@ public class ReviewController {
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH);
|
.setPriority(NotificationCompat.PRIORITY_HIGH);
|
||||||
notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build());
|
notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build());
|
||||||
|
|
||||||
}, Timber::e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ReviewCallback {
|
public interface ReviewCallback {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import fr.free.nrw.commons.Media;
|
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.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
|
|
@ -24,24 +25,21 @@ public class ReviewHelper {
|
||||||
|
|
||||||
private static final String[] imageExtensions = new String[]{".jpg", ".jpeg", ".png"};
|
private static final String[] imageExtensions = new String[]{".jpg", ".jpeg", ".png"};
|
||||||
|
|
||||||
private final OkHttpJsonApiClient okHttpJsonApiClient;
|
private final MediaClient mediaClient;
|
||||||
private final MediaWikiApi mediaWikiApi;
|
|
||||||
private final ReviewInterface reviewInterface;
|
private final ReviewInterface reviewInterface;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ReviewHelper(OkHttpJsonApiClient okHttpJsonApiClient,
|
public ReviewHelper(MediaClient mediaClient, ReviewInterface reviewInterface) {
|
||||||
MediaWikiApi mediaWikiApi,
|
this.mediaClient = mediaClient;
|
||||||
ReviewInterface reviewInterface) {
|
|
||||||
this.okHttpJsonApiClient = okHttpJsonApiClient;
|
|
||||||
this.mediaWikiApi = mediaWikiApi;
|
|
||||||
this.reviewInterface = reviewInterface;
|
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
|
* 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
|
* 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.
|
* its best to fetch for just last 1 hour.
|
||||||
|
*
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private Observable<RecentChange> getRecentChanges() {
|
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
|
* Returns a proper Media object if the file is not already nominated for deletion
|
||||||
* Else it returns an empty Media object
|
* Else it returns an empty Media object
|
||||||
|
*
|
||||||
* @param recentChange
|
* @param recentChange
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private Single<Media> getRandomMediaFromRecentChange(RecentChange recentChange) {
|
private Single<Media> getRandomMediaFromRecentChange(RecentChange recentChange) {
|
||||||
return Single.just(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 -> {
|
.flatMap(isDeleted -> {
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
return Single.just(new Media(""));
|
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
|
* Gets the first revision of the file from filename
|
||||||
|
*
|
||||||
* @param filename
|
* @param filename
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
|
@ -110,6 +110,7 @@ public class ReviewHelper {
|
||||||
* Checks if the change is reviewable or not.
|
* Checks if the change is reviewable or not.
|
||||||
* - checks the type and revisionId of the change
|
* - checks the type and revisionId of the change
|
||||||
* - checks supported image extensions
|
* - checks supported image extensions
|
||||||
|
*
|
||||||
* @param recentChange
|
* @param recentChange
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ public class FileUtils {
|
||||||
return mimeType;
|
return mimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getFileExt(String fileName) {
|
static String getFileExt(String fileName) {
|
||||||
//Default filePath extension
|
//Default filePath extension
|
||||||
String extension = ".jpg";
|
String extension = ".jpg";
|
||||||
|
|
||||||
|
|
@ -151,7 +151,7 @@ public class FileUtils {
|
||||||
return extension;
|
return extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
|
static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
|
||||||
return new FileInputStream(filePath);
|
return new FileInputStream(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import java.io.IOException;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.media.MediaClient;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
import fr.free.nrw.commons.nearby.Place;
|
||||||
import fr.free.nrw.commons.utils.ImageUtils;
|
import fr.free.nrw.commons.utils.ImageUtils;
|
||||||
|
|
@ -34,18 +35,20 @@ public class ImageProcessingService {
|
||||||
private final MediaWikiApi mwApi;
|
private final MediaWikiApi mwApi;
|
||||||
private final ReadFBMD readFBMD;
|
private final ReadFBMD readFBMD;
|
||||||
private final EXIFReader EXIFReader;
|
private final EXIFReader EXIFReader;
|
||||||
|
private final MediaClient mediaClient;
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ImageProcessingService(FileUtilsWrapper fileUtilsWrapper,
|
public ImageProcessingService(FileUtilsWrapper fileUtilsWrapper,
|
||||||
ImageUtilsWrapper imageUtilsWrapper,
|
ImageUtilsWrapper imageUtilsWrapper,
|
||||||
MediaWikiApi mwApi, ReadFBMD readFBMD, EXIFReader EXIFReader,
|
MediaWikiApi mwApi, ReadFBMD readFBMD, EXIFReader EXIFReader,
|
||||||
Context context) {
|
MediaClient mediaClient, Context context) {
|
||||||
this.fileUtilsWrapper = fileUtilsWrapper;
|
this.fileUtilsWrapper = fileUtilsWrapper;
|
||||||
this.imageUtilsWrapper = imageUtilsWrapper;
|
this.imageUtilsWrapper = imageUtilsWrapper;
|
||||||
this.mwApi = mwApi;
|
this.mwApi = mwApi;
|
||||||
this.readFBMD = readFBMD;
|
this.readFBMD = readFBMD;
|
||||||
this.EXIFReader = EXIFReader;
|
this.EXIFReader = EXIFReader;
|
||||||
|
this.mediaClient = mediaClient;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,7 +131,7 @@ public class ImageProcessingService {
|
||||||
return Single.just(EMPTY_TITLE);
|
return Single.just(EMPTY_TITLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Single.fromCallable(() -> mwApi.fileExistsWithName(uploadItem.getFileName()))
|
return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.getFileName())
|
||||||
.map(doesFileExist -> {
|
.map(doesFileExist -> {
|
||||||
Timber.d("Result for valid title is %s", doesFileExist);
|
Timber.d("Result for valid title is %s", doesFileExist);
|
||||||
return doesFileExist ? FILE_NAME_EXISTS : IMAGE_OK;
|
return doesFileExist ? FILE_NAME_EXISTS : IMAGE_OK;
|
||||||
|
|
@ -146,7 +149,7 @@ public class ImageProcessingService {
|
||||||
return Single.fromCallable(() ->
|
return Single.fromCallable(() ->
|
||||||
fileUtilsWrapper.getFileInputStream(filePath))
|
fileUtilsWrapper.getFileInputStream(filePath))
|
||||||
.map(fileUtilsWrapper::getSHA1)
|
.map(fileUtilsWrapper::getSHA1)
|
||||||
.map(mwApi::existingFile)
|
.flatMap(mediaClient::checkFileExistsUsingSha)
|
||||||
.map(b -> {
|
.map(b -> {
|
||||||
Timber.d("Result for duplicate image %s", b);
|
Timber.d("Result for duplicate image %s", b);
|
||||||
return b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK;
|
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.graphics.BitmapFactory;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
import androidx.core.app.NotificationManagerCompat;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
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.ContributionDao;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
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.mwapi.MediaWikiApi;
|
||||||
|
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||||
import fr.free.nrw.commons.wikidata.WikidataEditService;
|
import fr.free.nrw.commons.wikidata.WikidataEditService;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
@ -54,6 +57,8 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
@Inject WikidataEditService wikidataEditService;
|
@Inject WikidataEditService wikidataEditService;
|
||||||
@Inject SessionManager sessionManager;
|
@Inject SessionManager sessionManager;
|
||||||
@Inject ContributionDao contributionDao;
|
@Inject ContributionDao contributionDao;
|
||||||
|
@Inject UploadClient uploadClient;
|
||||||
|
@Inject MediaClient mediaClient;
|
||||||
|
|
||||||
private NotificationManagerCompat notificationManager;
|
private NotificationManagerCompat notificationManager;
|
||||||
private NotificationCompat.Builder curNotification;
|
private NotificationCompat.Builder curNotification;
|
||||||
|
|
@ -204,9 +209,9 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String notificationTag = localUri.toString();
|
String notificationTag = localUri.toString();
|
||||||
|
File file1;
|
||||||
try {
|
try {
|
||||||
File file1 = new File(localUri.getPath());
|
file1 = new File(localUri.getPath());
|
||||||
fileInputStream = new FileInputStream(file1);
|
fileInputStream = new FileInputStream(file1);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
Timber.d("File not found");
|
Timber.d("File not found");
|
||||||
|
|
@ -229,22 +234,8 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
contribution
|
contribution
|
||||||
);
|
);
|
||||||
|
|
||||||
Single.fromCallable(() -> {
|
Observable.fromCallable(() -> "Temp_" + contribution.hashCode() + filename)
|
||||||
if (!mwApi.validateLogin()) {
|
.flatMap(stashFilename -> uploadClient.uploadFileToStash(getApplicationContext(), stashFilename, file1))
|
||||||
// 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))
|
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.doFinally(() -> {
|
.doFinally(() -> {
|
||||||
|
|
@ -263,22 +254,20 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
|
|
||||||
Timber.d("Stash upload response 1 is %s", uploadStash.toString());
|
Timber.d("Stash upload response 1 is %s", uploadStash.toString());
|
||||||
|
|
||||||
String resultStatus = uploadStash.getResultStatus();
|
String resultStatus = uploadStash.getResult();
|
||||||
if (!resultStatus.equals("Success")) {
|
if (!resultStatus.equals("Success")) {
|
||||||
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
|
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
|
||||||
showFailedNotification(contribution);
|
showFailedNotification(contribution);
|
||||||
return Single.never();
|
return Observable.never();
|
||||||
} else {
|
} else {
|
||||||
synchronized (unfinishedUploads) {
|
|
||||||
Timber.d("making sure of uniqueness of name: %s", filename);
|
Timber.d("making sure of uniqueness of name: %s", filename);
|
||||||
String uniqueFilename = findUniqueFilename(filename);
|
String uniqueFilename = findUniqueFilename(filename);
|
||||||
unfinishedUploads.add(uniqueFilename);
|
unfinishedUploads.add(uniqueFilename);
|
||||||
return mwApi.uploadFileFinalize(
|
return uploadClient.uploadFileFromStash(
|
||||||
|
getApplicationContext(),
|
||||||
|
contribution,
|
||||||
uniqueFilename,
|
uniqueFilename,
|
||||||
uploadStash.getFilekey(),
|
uploadStash.getFilekey());
|
||||||
contribution.getPageContents(getApplicationContext()),
|
|
||||||
contribution.getEditSummary());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.subscribe(uploadResult -> {
|
.subscribe(uploadResult -> {
|
||||||
|
|
@ -286,19 +275,20 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
|
|
||||||
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
|
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
|
||||||
|
|
||||||
String resultStatus = uploadResult.getResultStatus();
|
String resultStatus = uploadResult.getResult();
|
||||||
if (!resultStatus.equals("Success")) {
|
if (!resultStatus.equals("Success")) {
|
||||||
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
|
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
|
||||||
showFailedNotification(contribution);
|
showFailedNotification(contribution);
|
||||||
} else {
|
} else {
|
||||||
String canonicalFilename = uploadResult.getCanonicalFilename();
|
String canonicalFilename = "File:" + uploadResult.getFilename();
|
||||||
Timber.d("Contribution upload success. Initiating Wikidata edit for entity id %s",
|
Timber.d("Contribution upload success. Initiating Wikidata edit for entity id %s",
|
||||||
contribution.getWikiDataEntityId());
|
contribution.getWikiDataEntityId());
|
||||||
wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), canonicalFilename);
|
wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), canonicalFilename);
|
||||||
contribution.setFilename(canonicalFilename);
|
contribution.setFilename(canonicalFilename);
|
||||||
contribution.setImageUrl(uploadResult.getImageUrl());
|
contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl());
|
||||||
contribution.setState(Contribution.STATE_COMPLETED);
|
contribution.setState(Contribution.STATE_COMPLETED);
|
||||||
contribution.setDateUploaded(uploadResult.getDateUploaded());
|
contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatShort()
|
||||||
|
.parse(uploadResult.getImageinfo().getTimestamp()));
|
||||||
contributionDao.save(contribution);
|
contributionDao.save(contribution);
|
||||||
}
|
}
|
||||||
}, throwable -> {
|
}, throwable -> {
|
||||||
|
|
@ -337,7 +327,7 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2");
|
sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!mwApi.fileExistsWithName(sequenceFileName)
|
if (!mediaClient.checkPageExistsUsingTitle(sequenceFileName).blockingGet()
|
||||||
&& !unfinishedUploads.contains(sequenceFileName)) {
|
&& !unfinishedUploads.contains(sequenceFileName)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import androidx.annotation.Nullable;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
import fr.free.nrw.commons.contributions.MainActivity;
|
||||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
|
import fr.free.nrw.commons.media.MediaClient;
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
|
@ -42,7 +43,7 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
|
||||||
|
|
||||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||||
|
|
||||||
@Inject OkHttpJsonApiClient okHttpJsonApiClient;
|
@Inject MediaClient mediaClient;
|
||||||
|
|
||||||
void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
|
void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
|
||||||
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.pic_of_day_app_widget);
|
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.pic_of_day_app_widget);
|
||||||
|
|
@ -67,7 +68,7 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
|
||||||
RemoteViews views,
|
RemoteViews views,
|
||||||
AppWidgetManager appWidgetManager,
|
AppWidgetManager appWidgetManager,
|
||||||
int appWidgetId) {
|
int appWidgetId) {
|
||||||
compositeDisposable.add(okHttpJsonApiClient.getPictureOfTheDay()
|
compositeDisposable.add(mediaClient.getPictureOfTheDay()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.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 javax.inject.Singleton;
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
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.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import io.reactivex.Observable;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
@ -26,20 +26,26 @@ import timber.log.Timber;
|
||||||
@Singleton
|
@Singleton
|
||||||
public class WikidataEditService {
|
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 Context context;
|
||||||
private final MediaWikiApi mediaWikiApi;
|
|
||||||
private final WikidataEditListener wikidataEditListener;
|
private final WikidataEditListener wikidataEditListener;
|
||||||
private final JsonKvStore directKvStore;
|
private final JsonKvStore directKvStore;
|
||||||
|
private final WikidataClient wikidataClient;
|
||||||
|
private final PageEditClient wikiDataPageEditClient;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public WikidataEditService(Context context,
|
public WikidataEditService(Context context,
|
||||||
MediaWikiApi mediaWikiApi,
|
|
||||||
WikidataEditListener wikidataEditListener,
|
WikidataEditListener wikidataEditListener,
|
||||||
@Named("default_preferences") JsonKvStore directKvStore) {
|
@Named("default_preferences") JsonKvStore directKvStore,
|
||||||
|
WikidataClient wikidataClient,
|
||||||
|
@Named("wikidata-page-edit") PageEditClient wikiDataPageEditClient) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.mediaWikiApi = mediaWikiApi;
|
|
||||||
this.wikidataEditListener = wikidataEditListener;
|
this.wikidataEditListener = wikidataEditListener;
|
||||||
this.directKvStore = directKvStore;
|
this.directKvStore = directKvStore;
|
||||||
|
this.wikidataClient = wikidataClient;
|
||||||
|
this.wikiDataPageEditClient = wikiDataPageEditClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -77,13 +83,20 @@ public class WikidataEditService {
|
||||||
private void editWikidataProperty(String wikidataEntityId, String fileName) {
|
private void editWikidataProperty(String wikidataEntityId, String fileName) {
|
||||||
Timber.d("Upload successful with wiki data entity id as %s", wikidataEntityId);
|
Timber.d("Upload successful with wiki data entity id as %s", wikidataEntityId);
|
||||||
Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId);
|
Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId);
|
||||||
Observable.fromCallable(() -> {
|
|
||||||
String propertyValue = getFileName(fileName);
|
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())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.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");
|
Timber.e(throwable, "Error occurred while making claim");
|
||||||
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
||||||
});
|
});
|
||||||
|
|
@ -95,31 +108,12 @@ public class WikidataEditService {
|
||||||
wikidataEditListener.onSuccessfulWikidataEdit();
|
wikidataEditListener.onSuccessfulWikidataEdit();
|
||||||
}
|
}
|
||||||
showSuccessToast();
|
showSuccessToast();
|
||||||
logEdit(revisionId);
|
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Unable to make wiki data edit for entity %s", wikidataEntityId);
|
Timber.d("Unable to make wiki data edit for entity %s", wikidataEntityId);
|
||||||
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
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
|
* Show a success toast when the edit is made successfully
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons
|
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.MediaResult
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi
|
import fr.free.nrw.commons.mwapi.MediaWikiApi
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||||
|
|
@ -23,7 +24,7 @@ class MediaDataExtractorTest {
|
||||||
internal var mwApi: MediaWikiApi? = null
|
internal var mwApi: MediaWikiApi? = null
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal var okHttpJsonApiClient: OkHttpJsonApiClient? = null
|
internal var mediaClient: MediaClient? = null
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
var mediaDataExtractor: MediaDataExtractor? = null
|
var mediaDataExtractor: MediaDataExtractor? = null
|
||||||
|
|
@ -42,10 +43,10 @@ class MediaDataExtractorTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun fetchMediaDetails() {
|
fun fetchMediaDetails() {
|
||||||
`when`(okHttpJsonApiClient?.getMedia(ArgumentMatchers.anyString(), ArgumentMatchers.anyBoolean()))
|
`when`(mediaClient?.getMedia(ArgumentMatchers.anyString()))
|
||||||
.thenReturn(Single.just(mock(Media::class.java)))
|
.thenReturn(Single.just(mock(Media::class.java)))
|
||||||
|
|
||||||
`when`(mwApi?.pageExists(ArgumentMatchers.anyString()))
|
`when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
|
||||||
.thenReturn(Single.just(true))
|
.thenReturn(Single.just(true))
|
||||||
|
|
||||||
val mediaResult = mock(MediaResult::class.java)
|
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.Media;
|
||||||
import fr.free.nrw.commons.bookmarks.Bookmark;
|
import fr.free.nrw.commons.bookmarks.Bookmark;
|
||||||
|
import fr.free.nrw.commons.media.MediaClient;
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
|
|
||||||
|
|
@ -29,7 +30,7 @@ import static org.mockito.Mockito.when;
|
||||||
public class BookmarkPicturesControllerTest {
|
public class BookmarkPicturesControllerTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
OkHttpJsonApiClient okHttpJsonApiClient;
|
MediaClient mediaClient;
|
||||||
@Mock
|
@Mock
|
||||||
BookmarkPicturesDao bookmarkDao;
|
BookmarkPicturesDao bookmarkDao;
|
||||||
|
|
||||||
|
|
@ -46,7 +47,7 @@ public class BookmarkPicturesControllerTest {
|
||||||
Media mockMedia = getMockMedia();
|
Media mockMedia = getMockMedia();
|
||||||
when(bookmarkDao.getAllBookmarks())
|
when(bookmarkDao.getAllBookmarks())
|
||||||
.thenReturn(getMockBookmarkList());
|
.thenReturn(getMockBookmarkList());
|
||||||
when(okHttpJsonApiClient.getMedia(anyString(), anyBoolean()))
|
when(mediaClient.getMedia(anyString()))
|
||||||
.thenReturn(Single.just(mockMedia));
|
.thenReturn(Single.just(mockMedia));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,9 +76,9 @@ public class BookmarkPicturesControllerTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void loadBookmarkedPicturesForNullMedia() {
|
public void loadBookmarkedPicturesForNullMedia() {
|
||||||
when(okHttpJsonApiClient.getMedia("File:Test1.jpg", false))
|
when(mediaClient.getMedia("File:Test1.jpg"))
|
||||||
.thenReturn(Single.error(new NullPointerException("Error occurred")));
|
.thenReturn(Single.error(new NullPointerException("Error occurred")));
|
||||||
when(okHttpJsonApiClient.getMedia("File:Test2.jpg", false))
|
when(mediaClient.getMedia("File:Test2.jpg"))
|
||||||
.thenReturn(Single.just(getMockMedia()));
|
.thenReturn(Single.just(getMockMedia()));
|
||||||
List<Media> bookmarkedPictures = bookmarkPicturesController.loadBookmarkedPictures().blockingGet();
|
List<Media> bookmarkedPictures = bookmarkPicturesController.loadBookmarkedPictures().blockingGet();
|
||||||
assertEquals(1, bookmarkedPictures.size());
|
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
|
package fr.free.nrw.commons.delete
|
||||||
|
|
||||||
import android.accounts.Account
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import fr.free.nrw.commons.Media
|
import fr.free.nrw.commons.Media
|
||||||
import fr.free.nrw.commons.auth.SessionManager
|
import fr.free.nrw.commons.actions.PageEditClient
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi
|
|
||||||
import fr.free.nrw.commons.notification.NotificationHelper
|
import fr.free.nrw.commons.notification.NotificationHelper
|
||||||
import fr.free.nrw.commons.utils.ViewUtilWrapper
|
import fr.free.nrw.commons.utils.ViewUtilWrapper
|
||||||
|
import io.reactivex.Observable
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.mockito.ArgumentMatchers
|
||||||
import org.mockito.InjectMocks
|
import org.mockito.InjectMocks
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.Mockito.`when`
|
import org.mockito.Mockito.`when`
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
|
import org.wikipedia.AppAdapter
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for delete helper
|
* Tests for delete helper
|
||||||
|
|
@ -21,17 +24,15 @@ import org.mockito.MockitoAnnotations
|
||||||
class DeleteHelperTest {
|
class DeleteHelperTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal var mwApi: MediaWikiApi? = null
|
@field:[Inject Named("commons-page-edit")]
|
||||||
|
internal var pageEditClient: PageEditClient? = null
|
||||||
@Mock
|
|
||||||
internal var sessionManager: SessionManager? = null
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
internal var notificationHelper: NotificationHelper? = null
|
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal var context: Context? = null
|
internal var context: Context? = null
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
internal var notificationHelper: NotificationHelper? = null
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal var viewUtil: ViewUtilWrapper? = null
|
internal var viewUtil: ViewUtilWrapper? = null
|
||||||
|
|
||||||
|
|
@ -54,9 +55,13 @@ class DeleteHelperTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun makeDeletion() {
|
fun makeDeletion() {
|
||||||
`when`(mwApi?.editToken).thenReturn("token")
|
`when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
|
||||||
`when`(sessionManager?.authCookie).thenReturn("Mock cookie")
|
.thenReturn(Observable.just(true))
|
||||||
`when`(sessionManager?.currentAccount).thenReturn(Account("TestUser", "Test"))
|
`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?.displayTitle).thenReturn("Test file")
|
||||||
`when`(media?.filename).thenReturn("Test file.jpg")
|
`when`(media?.filename).thenReturn("Test file.jpg")
|
||||||
|
|
||||||
|
|
@ -68,16 +73,45 @@ class DeleteHelperTest {
|
||||||
/**
|
/**
|
||||||
* Test a failed deletion
|
* Test a failed deletion
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test(expected = RuntimeException::class)
|
||||||
fun makeDeletionForNullToken() {
|
fun makeDeletionForPrependEditFailure() {
|
||||||
`when`(mwApi?.editToken).thenReturn(null)
|
`when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
|
||||||
`when`(sessionManager?.authCookie).thenReturn("Mock cookie")
|
.thenReturn(Observable.just(false))
|
||||||
`when`(sessionManager?.currentAccount).thenReturn(Account("TestUser", "Test"))
|
`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?.displayTitle).thenReturn("Test file")
|
||||||
`when`(media?.filename).thenReturn("Test file.jpg")
|
`when`(media?.filename).thenReturn("Test file.jpg")
|
||||||
|
|
||||||
val makeDeletion = deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet()
|
deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet()
|
||||||
assertNotNull(makeDeletion)
|
}
|
||||||
assertFalse(makeDeletion!!)
|
|
||||||
|
@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 android.os.Build
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import com.google.gson.Gson
|
|
||||||
import fr.free.nrw.commons.TestCommonsApplication
|
import fr.free.nrw.commons.TestCommonsApplication
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils
|
import fr.free.nrw.commons.utils.ConfigUtils
|
||||||
|
|
@ -38,7 +37,7 @@ class ApacheHttpClientMediaWikiApiTest {
|
||||||
wikidataServer = MockWebServer()
|
wikidataServer = MockWebServer()
|
||||||
okHttpClient = OkHttpClient()
|
okHttpClient = OkHttpClient()
|
||||||
sharedPreferences = mock(JsonKvStore::class.java)
|
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
|
@After
|
||||||
|
|
@ -46,199 +45,6 @@ class ApacheHttpClientMediaWikiApiTest {
|
||||||
server.shutdown()
|
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
|
@Test
|
||||||
fun isUserBlockedFromCommonsForInfinitelyBlockedUser() {
|
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>"))
|
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.Media
|
||||||
import fr.free.nrw.commons.TestCommonsApplication
|
import fr.free.nrw.commons.TestCommonsApplication
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient.mapType
|
|
||||||
import fr.free.nrw.commons.utils.CommonsDateUtil
|
import fr.free.nrw.commons.utils.CommonsDateUtil
|
||||||
import junit.framework.Assert.assertEquals
|
import junit.framework.Assert.assertEquals
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
|
@ -18,7 +17,6 @@ import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
import org.mockito.Mockito.`when`
|
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
@ -55,7 +53,7 @@ class OkHttpJsonApiClientTest {
|
||||||
val sparqlUrl = "http://" + sparqlServer.hostName + ":" + sparqlServer.port + "/"
|
val sparqlUrl = "http://" + sparqlServer.hostName + ":" + sparqlServer.port + "/"
|
||||||
val campaignsUrl = "http://" + campaignsServer.hostName + ":" + campaignsServer.port + "/"
|
val campaignsUrl = "http://" + campaignsServer.hostName + ":" + campaignsServer.port + "/"
|
||||||
val serverUrl = "http://" + server.hostName + ":" + server.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()
|
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
|
* Generate a MockResponse object which contains a list of media pages
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package fr.free.nrw.commons.review
|
package fr.free.nrw.commons.review
|
||||||
|
|
||||||
import fr.free.nrw.commons.Media
|
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.MediaWikiApi
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
|
|
@ -28,9 +29,7 @@ class ReviewHelperTest {
|
||||||
@Mock
|
@Mock
|
||||||
internal var reviewInterface: ReviewInterface? = null
|
internal var reviewInterface: ReviewInterface? = null
|
||||||
@Mock
|
@Mock
|
||||||
internal var okHttpJsonApiClient: OkHttpJsonApiClient? = null
|
internal var mediaClient: MediaClient? = null
|
||||||
@Mock
|
|
||||||
internal var mediaWikiApi: MediaWikiApi? = null
|
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
var reviewHelper: ReviewHelper? = null
|
var reviewHelper: ReviewHelper? = null
|
||||||
|
|
@ -65,7 +64,7 @@ class ReviewHelperTest {
|
||||||
|
|
||||||
val media = mock(Media::class.java)
|
val media = mock(Media::class.java)
|
||||||
`when`(media.filename).thenReturn("File:Test.jpg")
|
`when`(media.filename).thenReturn("File:Test.jpg")
|
||||||
`when`(okHttpJsonApiClient?.getMedia(ArgumentMatchers.anyString(), ArgumentMatchers.anyBoolean()))
|
`when`(mediaClient?.getMedia(ArgumentMatchers.anyString()))
|
||||||
.thenReturn(Single.just(media))
|
.thenReturn(Single.just(media))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,7 +73,7 @@ class ReviewHelperTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun getRandomMedia() {
|
fun getRandomMedia() {
|
||||||
`when`(mediaWikiApi?.pageExists(ArgumentMatchers.anyString()))
|
`when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
|
||||||
.thenReturn(Single.just(false))
|
.thenReturn(Single.just(false))
|
||||||
|
|
||||||
val randomMedia = reviewHelper?.randomMedia?.blockingGet()
|
val randomMedia = reviewHelper?.randomMedia?.blockingGet()
|
||||||
|
|
@ -89,7 +88,7 @@ class ReviewHelperTest {
|
||||||
*/
|
*/
|
||||||
@Test(expected = RuntimeException::class)
|
@Test(expected = RuntimeException::class)
|
||||||
fun getRandomMediaWithWithAllMediaNominatedForDeletion() {
|
fun getRandomMediaWithWithAllMediaNominatedForDeletion() {
|
||||||
`when`(mediaWikiApi?.pageExists(ArgumentMatchers.anyString()))
|
`when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
|
||||||
.thenReturn(Single.just(true))
|
.thenReturn(Single.just(true))
|
||||||
val media = reviewHelper?.randomMedia?.blockingGet()
|
val media = reviewHelper?.randomMedia?.blockingGet()
|
||||||
assertNull(media)
|
assertNull(media)
|
||||||
|
|
@ -101,11 +100,11 @@ class ReviewHelperTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun getRandomMediaWithWithOneMediaNominatedForDeletion() {
|
fun getRandomMediaWithWithOneMediaNominatedForDeletion() {
|
||||||
`when`(mediaWikiApi?.pageExists("Commons:Deletion_requests/File:Test1.jpeg"))
|
`when`(mediaClient?.checkPageExistsUsingTitle("Commons:Deletion_requests/File:Test1.jpeg"))
|
||||||
.thenReturn(Single.just(true))
|
.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))
|
.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))
|
.thenReturn(Single.just(true))
|
||||||
|
|
||||||
val media = reviewHelper?.randomMedia?.blockingGet()
|
val media = reviewHelper?.randomMedia?.blockingGet()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package fr.free.nrw.commons.upload
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import fr.free.nrw.commons.location.LatLng
|
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.mwapi.MediaWikiApi
|
||||||
import fr.free.nrw.commons.nearby.Place
|
import fr.free.nrw.commons.nearby.Place
|
||||||
import fr.free.nrw.commons.utils.ImageUtils
|
import fr.free.nrw.commons.utils.ImageUtils
|
||||||
|
|
@ -28,6 +29,8 @@ class u {
|
||||||
internal var readFBMD: ReadFBMD?=null
|
internal var readFBMD: ReadFBMD?=null
|
||||||
@Mock
|
@Mock
|
||||||
internal var readEXIF: EXIFReader?=null
|
internal var readEXIF: EXIFReader?=null
|
||||||
|
@Mock
|
||||||
|
internal var mediaClient: MediaClient? = null
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
var imageProcessingService: ImageProcessingService? = null
|
var imageProcessingService: ImageProcessingService? = null
|
||||||
|
|
@ -55,6 +58,7 @@ class u {
|
||||||
`when`(uploadItem.title).thenReturn(mockTitle)
|
`when`(uploadItem.title).thenReturn(mockTitle)
|
||||||
|
|
||||||
`when`(uploadItem.place).thenReturn(mockPlace)
|
`when`(uploadItem.place).thenReturn(mockPlace)
|
||||||
|
`when`(uploadItem.fileName).thenReturn("File:jpg")
|
||||||
|
|
||||||
`when`(fileUtilsWrapper!!.getFileInputStream(ArgumentMatchers.anyString()))
|
`when`(fileUtilsWrapper!!.getFileInputStream(ArgumentMatchers.anyString()))
|
||||||
.thenReturn(mock(FileInputStream::class.java))
|
.thenReturn(mock(FileInputStream::class.java))
|
||||||
|
|
@ -74,10 +78,10 @@ class u {
|
||||||
.thenReturn(mock(FileInputStream::class.java))
|
.thenReturn(mock(FileInputStream::class.java))
|
||||||
`when`(fileUtilsWrapper!!.getSHA1(any(FileInputStream::class.java)))
|
`when`(fileUtilsWrapper!!.getSHA1(any(FileInputStream::class.java)))
|
||||||
.thenReturn("fileSha")
|
.thenReturn("fileSha")
|
||||||
`when`(mwApi!!.existingFile(ArgumentMatchers.anyString()))
|
`when`(mediaClient!!.checkFileExistsUsingSha(ArgumentMatchers.anyString()))
|
||||||
.thenReturn(false)
|
.thenReturn(Single.just(false))
|
||||||
`when`(mwApi!!.fileExistsWithName(ArgumentMatchers.anyString()))
|
`when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
|
||||||
.thenReturn(false)
|
.thenReturn(Single.just(false))
|
||||||
`when`(readFBMD?.processMetadata(ArgumentMatchers.any(),ArgumentMatchers.any()))
|
`when`(readFBMD?.processMetadata(ArgumentMatchers.any(),ArgumentMatchers.any()))
|
||||||
.thenReturn(Single.just(ImageUtils.IMAGE_OK))
|
.thenReturn(Single.just(ImageUtils.IMAGE_OK))
|
||||||
`when`(readEXIF?.processMetadata(ArgumentMatchers.anyString()))
|
`when`(readEXIF?.processMetadata(ArgumentMatchers.anyString()))
|
||||||
|
|
@ -93,8 +97,8 @@ class u {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun validateImageForDuplicateImage() {
|
fun validateImageForDuplicateImage() {
|
||||||
`when`(mwApi!!.existingFile(ArgumentMatchers.anyString()))
|
`when`(mediaClient!!.checkFileExistsUsingSha(ArgumentMatchers.anyString()))
|
||||||
.thenReturn(true)
|
.thenReturn(Single.just(true))
|
||||||
val validateImage = imageProcessingService!!.validateImage(uploadItem, false)
|
val validateImage = imageProcessingService!!.validateImage(uploadItem, false)
|
||||||
assertEquals(ImageUtils.IMAGE_DUPLICATE, validateImage.blockingGet())
|
assertEquals(ImageUtils.IMAGE_DUPLICATE, validateImage.blockingGet())
|
||||||
}
|
}
|
||||||
|
|
@ -123,16 +127,16 @@ class u {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun validateImageForFileNameExistsWithCheckTitleOff() {
|
fun validateImageForFileNameExistsWithCheckTitleOff() {
|
||||||
`when`(mwApi!!.fileExistsWithName(ArgumentMatchers.anyString()))
|
`when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
|
||||||
.thenReturn(true)
|
.thenReturn(Single.just(true))
|
||||||
val validateImage = imageProcessingService!!.validateImage(uploadItem, false)
|
val validateImage = imageProcessingService!!.validateImage(uploadItem, false)
|
||||||
assertEquals(ImageUtils.IMAGE_OK, validateImage.blockingGet())
|
assertEquals(ImageUtils.IMAGE_OK, validateImage.blockingGet())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun validateImageForFileNameExistsWithCheckTitleOn() {
|
fun validateImageForFileNameExistsWithCheckTitleOn() {
|
||||||
`when`(mwApi!!.fileExistsWithName(ArgumentMatchers.nullable(String::class.java)))
|
`when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
|
||||||
.thenReturn(true)
|
.thenReturn(Single.just(true))
|
||||||
val validateImage = imageProcessingService!!.validateImage(uploadItem, true)
|
val validateImage = imageProcessingService!!.validateImage(uploadItem, true)
|
||||||
assertEquals(ImageUtils.FILE_NAME_EXISTS, validateImage.blockingGet())
|
assertEquals(ImageUtils.FILE_NAME_EXISTS, validateImage.blockingGet())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue